diff --git a/extensions/tlon/src/urbit/channel-client.ts b/extensions/tlon/src/urbit/channel-client.ts deleted file mode 100644 index 499860075b3..00000000000 --- a/extensions/tlon/src/urbit/channel-client.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { randomUUID } from "node:crypto"; -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk"; -import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js"; -import { getUrbitContext, normalizeUrbitCookie } from "./context.js"; -import { urbitFetch } from "./fetch.js"; - -export type UrbitChannelClientOptions = { - ship?: string; - ssrfPolicy?: SsrFPolicy; - lookupFn?: LookupFn; - fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; -}; - -export class UrbitChannelClient { - readonly baseUrl: string; - readonly cookie: string; - readonly ship: string; - readonly ssrfPolicy?: SsrFPolicy; - readonly lookupFn?: LookupFn; - readonly fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; - - private channelId: string | null = null; - - constructor(url: string, cookie: string, options: UrbitChannelClientOptions = {}) { - 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 get channelPath(): string { - const id = this.channelId; - if (!id) { - throw new Error("Channel not opened"); - } - return `/~/channel/${id}`; - } - - async open(): Promise { - if (this.channelId) { - return; - } - - const channelId = `${Math.floor(Date.now() / 1000)}-${randomUUID()}`; - this.channelId = channelId; - - try { - await ensureUrbitChannelOpen( - { - baseUrl: this.baseUrl, - cookie: this.cookie, - ship: this.ship, - channelId, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - }, - { - createBody: [], - createAuditContext: "tlon-urbit-channel-open", - }, - ); - } catch (error) { - this.channelId = null; - throw error; - } - } - - async poke(params: { app: string; mark: string; json: unknown }): Promise { - await this.open(); - const channelId = this.channelId; - if (!channelId) { - throw new Error("Channel not opened"); - } - return await pokeUrbitChannel( - { - baseUrl: this.baseUrl, - cookie: this.cookie, - ship: this.ship, - channelId, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - }, - { ...params, auditContext: "tlon-urbit-poke" }, - ); - } - - async scry(path: string): Promise { - return await scryUrbitPath( - { - baseUrl: this.baseUrl, - cookie: this.cookie, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - }, - { path, auditContext: "tlon-urbit-scry" }, - ); - } - - async getOurName(): Promise { - const { response, release } = await urbitFetch({ - baseUrl: this.baseUrl, - path: "/~/name", - init: { - method: "GET", - headers: { Cookie: this.cookie }, - }, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - timeoutMs: 30_000, - auditContext: "tlon-urbit-name", - }); - - try { - if (!response.ok) { - throw new Error(`Name request failed: ${response.status}`); - } - const text = await response.text(); - return text.trim(); - } finally { - await release(); - } - } - - async close(): Promise { - if (!this.channelId) { - return; - } - const channelPath = this.channelPath; - this.channelId = null; - - try { - const { response, release } = await urbitFetch({ - baseUrl: this.baseUrl, - path: channelPath, - init: { method: "DELETE", headers: { Cookie: this.cookie } }, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - timeoutMs: 30_000, - auditContext: "tlon-urbit-channel-close", - }); - try { - void response.body?.cancel(); - } finally { - await release(); - } - } catch { - // ignore cleanup errors - } - } -} diff --git a/extensions/tlon/src/urbit/channel-ops.ts b/extensions/tlon/src/urbit/channel-ops.ts deleted file mode 100644 index 077e8d01816..00000000000 --- a/extensions/tlon/src/urbit/channel-ops.ts +++ /dev/null @@ -1,164 +0,0 @@ -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; -}; - -export async function pokeUrbitChannel( - deps: UrbitChannelDeps, - params: { app: string; mark: string; json: unknown; auditContext: string }, -): Promise { - const pokeId = Date.now(); - const pokeData = { - id: pokeId, - action: "poke", - ship: deps.ship, - app: params.app, - mark: params.mark, - json: params.json, - }; - - 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([pokeData]), - }, - ssrfPolicy: deps.ssrfPolicy, - lookupFn: deps.lookupFn, - fetchImpl: deps.fetchImpl, - timeoutMs: 30_000, - auditContext: params.auditContext, - }); - - try { - if (!response.ok && response.status !== 204) { - const errorText = await response.text().catch(() => ""); - throw new Error(`Poke failed: ${response.status}${errorText ? ` - ${errorText}` : ""}`); - } - return pokeId; - } finally { - await release(); - } -} - -export async function scryUrbitPath( - deps: Pick, - params: { path: string; auditContext: string }, -): Promise { - const scryPath = `/~/scry${params.path}`; - const { response, release } = await urbitFetch({ - baseUrl: deps.baseUrl, - path: scryPath, - init: { - method: "GET", - headers: { Cookie: deps.cookie }, - }, - ssrfPolicy: deps.ssrfPolicy, - lookupFn: deps.lookupFn, - fetchImpl: deps.fetchImpl, - timeoutMs: 30_000, - auditContext: params.auditContext, - }); - - try { - if (!response.ok) { - throw new Error(`Scry failed: ${response.status} for path ${params.path}`); - } - return await response.json(); - } finally { - await release(); - } -} - -export async function createUrbitChannel( - deps: UrbitChannelDeps, - params: { body: unknown; auditContext: string }, -): Promise { - 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 { - 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 { - await createUrbitChannel(deps, { - body: params.createBody, - auditContext: params.createAuditContext, - }); - await wakeUrbitChannel(deps); -} diff --git a/extensions/tlon/src/urbit/context.ts b/extensions/tlon/src/urbit/context.ts deleted file mode 100644 index 90c2721c7b8..00000000000 --- a/extensions/tlon/src/urbit/context.ts +++ /dev/null @@ -1,47 +0,0 @@ -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; -}