refactor(tlon): centralize Urbit request helpers
This commit is contained in:
@@ -17,6 +17,7 @@ import { formatTargetHint, normalizeShip, parseTlonTarget } from "./targets.js";
|
||||
import { resolveTlonAccount, listTlonAccountIds } from "./types.js";
|
||||
import { authenticate } from "./urbit/auth.js";
|
||||
import { UrbitChannelClient } from "./urbit/channel-client.js";
|
||||
import { ssrfPolicyFromAllowPrivateNetwork } from "./urbit/context.js";
|
||||
import { buildMediaText, sendDm, sendGroupMessage } from "./urbit/send.js";
|
||||
|
||||
const TLON_CHANNEL_ID = "tlon" as const;
|
||||
@@ -123,7 +124,7 @@ const tlonOutbound: ChannelOutboundAdapter = {
|
||||
throw new Error(`Invalid Tlon target. Use ${formatTargetHint()}`);
|
||||
}
|
||||
|
||||
const ssrfPolicy = account.allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
|
||||
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.allowPrivateNetwork);
|
||||
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
|
||||
const api = new UrbitChannelClient(account.url, cookie, {
|
||||
ship: account.ship.replace(/^~/, ""),
|
||||
@@ -345,7 +346,7 @@ export const tlonPlugin: ChannelPlugin = {
|
||||
return { ok: false, error: "Not configured" };
|
||||
}
|
||||
try {
|
||||
const ssrfPolicy = account.allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
|
||||
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.allowPrivateNetwork);
|
||||
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
|
||||
const api = new UrbitChannelClient(account.url, cookie, {
|
||||
ship: account.ship.replace(/^~/, ""),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getTlonRuntime } from "../runtime.js";
|
||||
import { normalizeShip, parseChannelNest } from "../targets.js";
|
||||
import { resolveTlonAccount } from "../types.js";
|
||||
import { authenticate } from "../urbit/auth.js";
|
||||
import { ssrfPolicyFromAllowPrivateNetwork } from "../urbit/context.js";
|
||||
import { sendDm, sendGroupMessage } from "../urbit/send.js";
|
||||
import { UrbitSSEClient } from "../urbit/sse-client.js";
|
||||
import { fetchAllChannels } from "./discovery.js";
|
||||
@@ -113,7 +114,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
|
||||
|
||||
let api: UrbitSSEClient | null = null;
|
||||
try {
|
||||
const ssrfPolicy = account.allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
|
||||
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.allowPrivateNetwork);
|
||||
runtime.log?.(`[tlon] Attempting authentication to ${account.url}...`);
|
||||
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
|
||||
api = new UrbitSSEClient(account.url, cookie, {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk";
|
||||
import { UrbitAuthError } from "./errors.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
export type UrbitAuthenticateOptions = {
|
||||
@@ -31,14 +32,14 @@ export async function authenticate(
|
||||
|
||||
try {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Login failed with status ${response.status}`);
|
||||
throw new UrbitAuthError("auth_failed", `Login failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
// Some Urbit setups require the response body to be read before cookie headers finalize.
|
||||
await response.text().catch(() => {});
|
||||
const cookie = response.headers.get("set-cookie");
|
||||
if (!cookie) {
|
||||
throw new Error("No authentication cookie received");
|
||||
throw new UrbitAuthError("missing_cookie", "No authentication cookie received");
|
||||
}
|
||||
return cookie;
|
||||
} finally {
|
||||
|
||||
41
extensions/tlon/src/urbit/base-url.test.ts
Normal file
41
extensions/tlon/src/urbit/base-url.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
|
||||
describe("validateUrbitBaseUrl", () => {
|
||||
it("adds https:// when scheme is missing and strips path/query fragments", () => {
|
||||
const result = validateUrbitBaseUrl("example.com/foo?bar=baz");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.baseUrl).toBe("https://example.com");
|
||||
expect(result.hostname).toBe("example.com");
|
||||
});
|
||||
|
||||
it("rejects non-http schemes", () => {
|
||||
const result = validateUrbitBaseUrl("file:///etc/passwd");
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toContain("http:// or https://");
|
||||
});
|
||||
|
||||
it("rejects embedded credentials", () => {
|
||||
const result = validateUrbitBaseUrl("https://user:pass@example.com");
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toContain("credentials");
|
||||
});
|
||||
|
||||
it("normalizes a trailing dot in the hostname for origin construction", () => {
|
||||
const result = validateUrbitBaseUrl("https://example.com./foo");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.baseUrl).toBe("https://example.com");
|
||||
expect(result.hostname).toBe("example.com");
|
||||
});
|
||||
|
||||
it("preserves port in the normalized origin", () => {
|
||||
const result = validateUrbitBaseUrl("http://example.com:8080/~/login");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.baseUrl).toBe("http://example.com:8080");
|
||||
});
|
||||
});
|
||||
@@ -36,8 +36,16 @@ export function validateUrbitBaseUrl(raw: string): UrbitBaseUrlValidation {
|
||||
return { ok: false, error: "Invalid hostname" };
|
||||
}
|
||||
|
||||
// Normalize to origin so callers can't smuggle paths/query fragments into the base URL.
|
||||
return { ok: true, baseUrl: parsed.origin, hostname };
|
||||
// Normalize to origin so callers can't smuggle paths/query fragments into the base URL,
|
||||
// and strip a trailing dot from the hostname (DNS root label).
|
||||
const isIpv6 = hostname.includes(":");
|
||||
const host = parsed.port
|
||||
? `${isIpv6 ? `[${hostname}]` : hostname}:${parsed.port}`
|
||||
: isIpv6
|
||||
? `[${hostname}]`
|
||||
: hostname;
|
||||
|
||||
return { ok: true, baseUrl: `${parsed.protocol}//${host}`, hostname };
|
||||
}
|
||||
|
||||
export function isBlockedUrbitHostname(hostname: string): boolean {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { ensureUrbitChannelOpen } from "./channel-ops.js";
|
||||
import { getUrbitContext, normalizeUrbitCookie } from "./context.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
export type UrbitChannelClientOptions = {
|
||||
@@ -20,28 +21,15 @@ export class UrbitChannelClient {
|
||||
private channelId: string | null = null;
|
||||
|
||||
constructor(url: string, cookie: string, options: UrbitChannelClientOptions = {}) {
|
||||
const validated = validateUrbitBaseUrl(url);
|
||||
if (!validated.ok) {
|
||||
throw new Error(validated.error);
|
||||
}
|
||||
|
||||
this.baseUrl = validated.baseUrl;
|
||||
this.cookie = cookie.split(";")[0];
|
||||
this.ship = (
|
||||
options.ship?.replace(/^~/, "") ?? this.resolveShipFromHostname(validated.hostname)
|
||||
).trim();
|
||||
const ctx = getUrbitContext(url, options.ship);
|
||||
this.baseUrl = ctx.baseUrl;
|
||||
this.cookie = normalizeUrbitCookie(cookie);
|
||||
this.ship = ctx.ship;
|
||||
this.ssrfPolicy = options.ssrfPolicy;
|
||||
this.lookupFn = options.lookupFn;
|
||||
this.fetchImpl = options.fetchImpl;
|
||||
}
|
||||
|
||||
private resolveShipFromHostname(hostname: string): string {
|
||||
if (hostname.includes(".")) {
|
||||
return hostname.split(".")[0] ?? hostname;
|
||||
}
|
||||
return hostname;
|
||||
}
|
||||
|
||||
private get channelPath(): string {
|
||||
const id = this.channelId;
|
||||
if (!id) {
|
||||
@@ -55,73 +43,28 @@ export class UrbitChannelClient {
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelId = `${Math.floor(Date.now() / 1000)}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
const channelId = `${Math.floor(Date.now() / 1000)}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
this.channelId = channelId;
|
||||
|
||||
// Create the channel.
|
||||
{
|
||||
const { response, release } = await urbitFetch({
|
||||
baseUrl: this.baseUrl,
|
||||
path: this.channelPath,
|
||||
init: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: this.cookie,
|
||||
},
|
||||
body: JSON.stringify([]),
|
||||
try {
|
||||
await ensureUrbitChannelOpen(
|
||||
{
|
||||
baseUrl: this.baseUrl,
|
||||
cookie: this.cookie,
|
||||
ship: this.ship,
|
||||
channelId,
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
},
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
timeoutMs: 30_000,
|
||||
auditContext: "tlon-urbit-channel-open",
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new Error(`Channel creation failed: ${response.status}`);
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
// Wake the channel (matches urbit/http-api behavior).
|
||||
{
|
||||
const { response, release } = await urbitFetch({
|
||||
baseUrl: this.baseUrl,
|
||||
path: this.channelPath,
|
||||
init: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: this.cookie,
|
||||
},
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: Date.now(),
|
||||
action: "poke",
|
||||
ship: this.ship,
|
||||
app: "hood",
|
||||
mark: "helm-hi",
|
||||
json: "Opening API channel",
|
||||
},
|
||||
]),
|
||||
{
|
||||
createBody: [],
|
||||
createAuditContext: "tlon-urbit-channel-open",
|
||||
},
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
timeoutMs: 30_000,
|
||||
auditContext: "tlon-urbit-channel-wake",
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new Error(`Channel activation failed: ${response.status}`);
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.channelId = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
92
extensions/tlon/src/urbit/channel-ops.ts
Normal file
92
extensions/tlon/src/urbit/channel-ops.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk";
|
||||
import { UrbitHttpError } from "./errors.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
export type UrbitChannelDeps = {
|
||||
baseUrl: string;
|
||||
cookie: string;
|
||||
ship: string;
|
||||
channelId: string;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
lookupFn?: LookupFn;
|
||||
fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
};
|
||||
|
||||
export async function createUrbitChannel(
|
||||
deps: UrbitChannelDeps,
|
||||
params: { body: unknown; auditContext: string },
|
||||
): Promise<void> {
|
||||
const { response, release } = await urbitFetch({
|
||||
baseUrl: deps.baseUrl,
|
||||
path: `/~/channel/${deps.channelId}`,
|
||||
init: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: deps.cookie,
|
||||
},
|
||||
body: JSON.stringify(params.body),
|
||||
},
|
||||
ssrfPolicy: deps.ssrfPolicy,
|
||||
lookupFn: deps.lookupFn,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
timeoutMs: 30_000,
|
||||
auditContext: params.auditContext,
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new UrbitHttpError({ operation: "Channel creation", status: response.status });
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function wakeUrbitChannel(deps: UrbitChannelDeps): Promise<void> {
|
||||
const { response, release } = await urbitFetch({
|
||||
baseUrl: deps.baseUrl,
|
||||
path: `/~/channel/${deps.channelId}`,
|
||||
init: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: deps.cookie,
|
||||
},
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: Date.now(),
|
||||
action: "poke",
|
||||
ship: deps.ship,
|
||||
app: "hood",
|
||||
mark: "helm-hi",
|
||||
json: "Opening API channel",
|
||||
},
|
||||
]),
|
||||
},
|
||||
ssrfPolicy: deps.ssrfPolicy,
|
||||
lookupFn: deps.lookupFn,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
timeoutMs: 30_000,
|
||||
auditContext: "tlon-urbit-channel-wake",
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new UrbitHttpError({ operation: "Channel activation", status: response.status });
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureUrbitChannelOpen(
|
||||
deps: UrbitChannelDeps,
|
||||
params: { createBody: unknown; createAuditContext: string },
|
||||
): Promise<void> {
|
||||
await createUrbitChannel(deps, {
|
||||
body: params.createBody,
|
||||
auditContext: params.createAuditContext,
|
||||
});
|
||||
await wakeUrbitChannel(deps);
|
||||
}
|
||||
47
extensions/tlon/src/urbit/context.ts
Normal file
47
extensions/tlon/src/urbit/context.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { SsrFPolicy } from "openclaw/plugin-sdk";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { UrbitUrlError } from "./errors.js";
|
||||
|
||||
export type UrbitContext = {
|
||||
baseUrl: string;
|
||||
hostname: string;
|
||||
ship: string;
|
||||
};
|
||||
|
||||
export function resolveShipFromHostname(hostname: string): string {
|
||||
const trimmed = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
if (trimmed.includes(".")) {
|
||||
return trimmed.split(".")[0] ?? trimmed;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function normalizeUrbitShip(ship: string | undefined, hostname: string): string {
|
||||
const raw = ship?.replace(/^~/, "") ?? resolveShipFromHostname(hostname);
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
export function normalizeUrbitCookie(cookie: string): string {
|
||||
return cookie.split(";")[0] ?? cookie;
|
||||
}
|
||||
|
||||
export function getUrbitContext(url: string, ship?: string): UrbitContext {
|
||||
const validated = validateUrbitBaseUrl(url);
|
||||
if (!validated.ok) {
|
||||
throw new UrbitUrlError(validated.error);
|
||||
}
|
||||
return {
|
||||
baseUrl: validated.baseUrl,
|
||||
hostname: validated.hostname,
|
||||
ship: normalizeUrbitShip(ship, validated.hostname),
|
||||
};
|
||||
}
|
||||
|
||||
export function ssrfPolicyFromAllowPrivateNetwork(
|
||||
allowPrivateNetwork: boolean | null | undefined,
|
||||
): SsrFPolicy | undefined {
|
||||
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
|
||||
}
|
||||
51
extensions/tlon/src/urbit/errors.ts
Normal file
51
extensions/tlon/src/urbit/errors.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export type UrbitErrorCode =
|
||||
| "invalid_url"
|
||||
| "http_error"
|
||||
| "auth_failed"
|
||||
| "missing_cookie"
|
||||
| "channel_not_open";
|
||||
|
||||
export class UrbitError extends Error {
|
||||
readonly code: UrbitErrorCode;
|
||||
|
||||
constructor(code: UrbitErrorCode, message: string, options?: { cause?: unknown }) {
|
||||
super(message, options);
|
||||
this.name = "UrbitError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export class UrbitUrlError extends UrbitError {
|
||||
constructor(message: string, options?: { cause?: unknown }) {
|
||||
super("invalid_url", message, options);
|
||||
this.name = "UrbitUrlError";
|
||||
}
|
||||
}
|
||||
|
||||
export class UrbitHttpError extends UrbitError {
|
||||
readonly status: number;
|
||||
readonly operation: string;
|
||||
readonly bodyText?: string;
|
||||
|
||||
constructor(params: { operation: string; status: number; bodyText?: string; cause?: unknown }) {
|
||||
const suffix = params.bodyText ? ` - ${params.bodyText}` : "";
|
||||
super("http_error", `${params.operation} failed: ${params.status}${suffix}`, {
|
||||
cause: params.cause,
|
||||
});
|
||||
this.name = "UrbitHttpError";
|
||||
this.status = params.status;
|
||||
this.operation = params.operation;
|
||||
this.bodyText = params.bodyText;
|
||||
}
|
||||
}
|
||||
|
||||
export class UrbitAuthError extends UrbitError {
|
||||
constructor(
|
||||
code: "auth_failed" | "missing_cookie",
|
||||
message: string,
|
||||
options?: { cause?: unknown },
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.name = "UrbitAuthError";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { UrbitUrlError } from "./errors.js";
|
||||
|
||||
export type UrbitFetchOptions = {
|
||||
baseUrl: string;
|
||||
@@ -19,7 +20,7 @@ export type UrbitFetchOptions = {
|
||||
export async function urbitFetch(params: UrbitFetchOptions) {
|
||||
const validated = validateUrbitBaseUrl(params.baseUrl);
|
||||
if (!validated.ok) {
|
||||
throw new Error(validated.error);
|
||||
throw new UrbitUrlError(validated.error);
|
||||
}
|
||||
|
||||
const url = new URL(params.path, validated.baseUrl).toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk";
|
||||
import { Readable } from "node:stream";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { ensureUrbitChannelOpen } from "./channel-ops.js";
|
||||
import { getUrbitContext, normalizeUrbitCookie } from "./context.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
export type UrbitSseLogger = {
|
||||
@@ -54,14 +55,10 @@ export class UrbitSSEClient {
|
||||
streamRelease: (() => Promise<void>) | null = null;
|
||||
|
||||
constructor(url: string, cookie: string, options: UrbitSseOptions = {}) {
|
||||
const validated = validateUrbitBaseUrl(url);
|
||||
if (!validated.ok) {
|
||||
throw new Error(validated.error);
|
||||
}
|
||||
|
||||
this.url = validated.baseUrl;
|
||||
this.cookie = cookie.split(";")[0];
|
||||
this.ship = options.ship?.replace(/^~/, "") ?? this.resolveShipFromHostname(validated.hostname);
|
||||
const ctx = getUrbitContext(url, options.ship);
|
||||
this.url = ctx.baseUrl;
|
||||
this.cookie = normalizeUrbitCookie(cookie);
|
||||
this.ship = ctx.ship;
|
||||
this.channelId = `${Math.floor(Date.now() / 1000)}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
this.channelUrl = new URL(`/~/channel/${this.channelId}`, this.url).toString();
|
||||
this.onReconnect = options.onReconnect ?? null;
|
||||
@@ -75,13 +72,6 @@ export class UrbitSSEClient {
|
||||
this.fetchImpl = options.fetchImpl;
|
||||
}
|
||||
|
||||
private resolveShipFromHostname(hostname: string): string {
|
||||
if (hostname.includes(".")) {
|
||||
return hostname.split(".")[0] ?? hostname;
|
||||
}
|
||||
return hostname;
|
||||
}
|
||||
|
||||
async subscribe(params: {
|
||||
app: string;
|
||||
path: string;
|
||||
@@ -150,70 +140,21 @@ export class UrbitSSEClient {
|
||||
}
|
||||
|
||||
async connect() {
|
||||
{
|
||||
const { response, release } = await urbitFetch({
|
||||
await ensureUrbitChannelOpen(
|
||||
{
|
||||
baseUrl: this.url,
|
||||
path: `/~/channel/${this.channelId}`,
|
||||
init: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: this.cookie,
|
||||
},
|
||||
body: JSON.stringify(this.subscriptions),
|
||||
},
|
||||
cookie: this.cookie,
|
||||
ship: this.ship,
|
||||
channelId: this.channelId,
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
timeoutMs: 30_000,
|
||||
auditContext: "tlon-urbit-channel-create",
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new Error(`Channel creation failed: ${response.status}`);
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const { response, release } = await urbitFetch({
|
||||
baseUrl: this.url,
|
||||
path: `/~/channel/${this.channelId}`,
|
||||
init: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: this.cookie,
|
||||
},
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: Date.now(),
|
||||
action: "poke",
|
||||
ship: this.ship,
|
||||
app: "hood",
|
||||
mark: "helm-hi",
|
||||
json: "Opening API channel",
|
||||
},
|
||||
]),
|
||||
},
|
||||
ssrfPolicy: this.ssrfPolicy,
|
||||
lookupFn: this.lookupFn,
|
||||
fetchImpl: this.fetchImpl,
|
||||
timeoutMs: 30_000,
|
||||
auditContext: "tlon-urbit-channel-wake",
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new Error(`Channel activation failed: ${response.status}`);
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
createBody: this.subscriptions,
|
||||
createAuditContext: "tlon-urbit-channel-create",
|
||||
},
|
||||
);
|
||||
|
||||
await this.openStream();
|
||||
this.isConnected = true;
|
||||
|
||||
Reference in New Issue
Block a user