@kehto/services 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cvm-nostr-transport.d.ts +1 -1
- package/dist/cvm-nostr-transport.js.map +1 -1
- package/dist/index.d.ts +498 -2
- package/dist/index.js +705 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -94,4 +94,4 @@ declare function createNostrCvmTransport(options?: NostrCvmTransportOptions): Cv
|
|
|
94
94
|
dispose(): void;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
export { type CvmRelayPool, type NostrCvmTransportOptions, createNostrCvmTransport };
|
|
97
|
+
export { type CvmRelayPool, type CvmSubCloser, type NostrCvmTransportOptions, type NostrEventLike, type NostrFilterLike, createNostrCvmTransport };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cvm-nostr-transport.ts"],"sourcesContent":["/**\n * cvm-nostr-transport.ts — concrete ContextVM transport for NAP-CVM.\n *\n * Implements {@link CvmTransport} over Nostr, exactly as validated against live\n * ContextVM servers (e.g. Relatr):\n *\n * - MCP JSON-RPC messages ride in kind-25910 event `content`.\n * - Requests are CEP-4 gift-wrapped: the inner kind-25910 event is signed with\n * the shell's ephemeral client key, NIP-44-encrypted to the server, and\n * placed in a kind-21059 (ephemeral) / 1059 (regular) wrap signed by a fresh\n * random key, `p`-tagged to the server. Responses arrive the same way,\n * `p`-tagged to the client, and are correlated by the inner JSON-RPC `id`.\n * - Discovery reads kind-11316 (server) + kind-11317 (tools) announcements.\n *\n * Shipped on a separate entry (`@kehto/services/cvm-nostr-transport`) so the\n * `nostr-tools` dependency stays out of the core `@kehto/services` bundle.\n *\n * The client key is ephemeral and shell-owned: napplets never see keys, relay\n * sockets, or NIP-44 material (NAP-CVM §Security).\n *\n * @example\n * ```ts\n * import { createNostrCvmTransport } from '@kehto/services/cvm-nostr-transport';\n * const transport = createNostrCvmTransport({\n * defaultRelays: ['wss://relay.contextvm.org', 'wss://relay2.contextvm.org'],\n * });\n * ```\n */\n\nimport { SimplePool } from 'nostr-tools/pool';\nimport { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';\nimport * as nip44 from 'nostr-tools/nip44';\nimport type { Event as NostrToolsEvent, Filter as NostrToolsFilter } from 'nostr-tools';\n\nimport type { CvmTransport } from './cvm-service.js';\nimport type {\n CvmDiscoverQuery,\n CvmRequestOptions,\n CvmServer,\n CvmServerRef,\n McpMessage,\n} from './cvm-types.js';\n\n/** ContextVM unified transport event kind. */\nconst KIND_CVM = 25910;\n/** CEP-4 gift-wrap kinds: ephemeral (CEP-19) and regular. */\nconst KIND_GIFT_WRAP_EPHEMERAL = 21059;\nconst KIND_GIFT_WRAP_REGULAR = 1059;\n/** CEP-6 announcement kinds. */\nconst KIND_ANNOUNCE_SERVER = 11316;\nconst KIND_ANNOUNCE_TOOLS = 11317;\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_DISCOVER_TIMEOUT_MS = 6_000;\nconst MCP_PROTOCOL_VERSION = '2025-11-25';\nconst SEEN_WRAP_LIMIT = 512;\n\n/** Minimal signed Nostr event. */\ninterface NostrEventLike {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** A Nostr REQ filter (subset). */\ninterface NostrFilterLike {\n kinds?: number[];\n authors?: string[];\n limit?: number;\n ['#p']?: string[];\n [key: string]: unknown;\n}\n\n/** Subscription handle returned by the relay pool. */\ninterface CvmSubCloser {\n close(): void;\n}\n\n/**\n * Minimal relay-pool surface used by this transport — structurally satisfied\n * by `nostr-tools` `SimplePool`. Injectable for testing.\n */\nexport interface CvmRelayPool {\n subscribe(\n relays: string[],\n filter: NostrFilterLike,\n params: { onevent?: (event: NostrEventLike) => void; oneose?: () => void },\n ): CvmSubCloser;\n publish(relays: string[], event: NostrEventLike): unknown;\n}\n\n/** Options for {@link createNostrCvmTransport}. */\nexport interface NostrCvmTransportOptions {\n /** Relays used when a server reference carries no relay hints. */\n defaultRelays?: string[];\n /** Default per-request timeout in milliseconds. */\n timeoutMs?: number;\n /** Whether to CEP-4 gift-wrap requests. Default true (most servers require it). */\n encrypt?: boolean;\n /** Use ephemeral (kind 21059) gift wraps when encrypting. Default true. */\n ephemeralWrap?: boolean;\n /** Relay pool to use. Defaults to a fresh `nostr-tools` `SimplePool`. */\n pool?: CvmRelayPool;\n /** Client secret key (32 bytes). Defaults to a generated ephemeral key. */\n clientSecretKey?: Uint8Array;\n /** Client info advertised during MCP `initialize`. */\n clientInfo?: { name: string; version: string };\n}\n\ntype EventHandler = (server: CvmServerRef, message: McpMessage) => void;\n\ninterface PendingRequest {\n resolve(message: McpMessage): void;\n reject(error: Error): void;\n timer: ReturnType<typeof setTimeout>;\n /** The caller's original JSON-RPC id, restored on the response. */\n originalId: string | number | undefined;\n}\n\ninterface ServerSession {\n relays: string[];\n initialized: boolean;\n initializing: Promise<void> | null;\n}\n\nlet correlationCounter = 0;\nfunction nextCorrelationId(): string {\n correlationCounter += 1;\n return `cvm-${correlationCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;\n}\n\nfunction randomizedPastTimestamp(): number {\n // NIP-59: randomize within the past two days to reduce timing metadata.\n const jitter = Math.floor(Math.random() * 172_800);\n return Math.floor(Date.now() / 1000) - jitter;\n}\n\nfunction tagValue(tags: string[][], name: string): string | undefined {\n return tags.find((tag) => tag[0] === name)?.[1];\n}\n\n/** Adapt a `nostr-tools` SimplePool to the {@link CvmRelayPool} surface. */\nfunction simplePoolAdapter(sp: SimplePool): CvmRelayPool {\n return {\n subscribe(relays, filter, params) {\n return sp.subscribe(relays, filter as NostrToolsFilter, {\n onevent: params.onevent,\n oneose: params.oneose,\n });\n },\n publish(relays, event) {\n return sp.publish(relays, event as NostrToolsEvent);\n },\n };\n}\n\n/**\n * Create a Nostr-backed ContextVM transport.\n *\n * @param options - Relay set, timeouts, encryption mode, and optional injected\n * pool/keys (the injected pool + key make the transport deterministic in tests).\n * @returns A {@link CvmTransport} plus a `dispose()` to tear down subscriptions.\n */\nexport function createNostrCvmTransport(\n options: NostrCvmTransportOptions = {},\n): CvmTransport & { dispose(): void } {\n const defaultRelays = options.defaultRelays ?? [];\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const encrypt = options.encrypt ?? true;\n const wrapKind = options.ephemeralWrap === false ? KIND_GIFT_WRAP_REGULAR : KIND_GIFT_WRAP_EPHEMERAL;\n const pool: CvmRelayPool = options.pool ?? simplePoolAdapter(new SimplePool());\n const clientSecretKey = options.clientSecretKey ?? generateSecretKey();\n const clientPubkey = getPublicKey(clientSecretKey);\n const clientInfo = options.clientInfo ?? { name: 'kehto-cvm', version: '1.0.0' };\n\n const sessions = new Map<string, ServerSession>();\n const pending = new Map<string, PendingRequest>();\n const eventHandlers = new Set<EventHandler>();\n const relayRefcount = new Map<string, number>();\n const seenWraps = new Set<string>();\n let inbound: CvmSubCloser | null = null;\n let subscribedRelays = '';\n\n function resolveRelays(server: CvmServerRef): string[] {\n const relays = server.relays && server.relays.length > 0 ? server.relays : defaultRelays;\n if (relays.length === 0) throw new Error('server not found');\n return [...new Set(relays)];\n }\n\n function holdRelays(relays: string[]): void {\n for (const url of relays) relayRefcount.set(url, (relayRefcount.get(url) ?? 0) + 1);\n refreshSubscription();\n }\n\n function releaseRelays(relays: string[]): void {\n for (const url of relays) {\n const count = (relayRefcount.get(url) ?? 0) - 1;\n if (count <= 0) relayRefcount.delete(url);\n else relayRefcount.set(url, count);\n }\n refreshSubscription();\n }\n\n function refreshSubscription(): void {\n const relays = [...relayRefcount.keys()].sort();\n const key = relays.join(',');\n if (key === subscribedRelays) return;\n inbound?.close();\n subscribedRelays = key;\n inbound = relays.length === 0\n ? null\n : pool.subscribe(\n relays,\n { kinds: encrypt ? [KIND_GIFT_WRAP_REGULAR, KIND_GIFT_WRAP_EPHEMERAL] : [KIND_CVM], ['#p']: [clientPubkey] },\n { onevent: handleInbound },\n );\n }\n\n function rememberWrap(id: string): boolean {\n if (seenWraps.has(id)) return false;\n seenWraps.add(id);\n if (seenWraps.size > SEEN_WRAP_LIMIT) {\n const oldest = seenWraps.values().next().value;\n if (oldest !== undefined) seenWraps.delete(oldest);\n }\n return true;\n }\n\n function handleInbound(event: NostrEventLike): void {\n if (!rememberWrap(event.id)) return;\n let serverPubkey: string;\n let mcp: McpMessage;\n try {\n if (encrypt) {\n const conversationKey = nip44.getConversationKey(clientSecretKey, event.pubkey);\n const inner = JSON.parse(nip44.decrypt(event.content, conversationKey)) as NostrEventLike;\n serverPubkey = inner.pubkey;\n mcp = JSON.parse(inner.content) as McpMessage;\n } else {\n serverPubkey = event.pubkey;\n mcp = JSON.parse(event.content) as McpMessage;\n }\n } catch {\n return; // not addressed to us / undecryptable / malformed — ignore.\n }\n\n const id = mcp.id;\n if (id != null && pending.has(String(id))) {\n const entry = pending.get(String(id))!;\n pending.delete(String(id));\n clearTimeout(entry.timer);\n entry.resolve({ ...mcp, id: entry.originalId });\n return;\n }\n // Uncorrelated server message (notification) → fan out as a CVM event.\n if (mcp.method !== undefined && sessions.has(serverPubkey)) {\n const server: CvmServerRef = { pubkey: serverPubkey };\n for (const handler of eventHandlers) handler(server, mcp);\n }\n }\n\n function publishMcp(server: CvmServerRef, relays: string[], message: McpMessage): void {\n const inner = finalizeEvent(\n { kind: KIND_CVM, created_at: Math.floor(Date.now() / 1000), tags: [['p', server.pubkey]], content: JSON.stringify(message) },\n clientSecretKey,\n ) as NostrEventLike;\n if (!encrypt) {\n pool.publish(relays, inner);\n return;\n }\n const wrapSecretKey = generateSecretKey();\n const conversationKey = nip44.getConversationKey(wrapSecretKey, server.pubkey);\n // Ephemeral wraps (kind 21059) are not stored; relays reject backdated\n // ephemeral events as \"expired\", so they MUST carry a current timestamp.\n // Regular wraps (kind 1059) are backdated per NIP-59 to blur timing metadata.\n const createdAt =\n wrapKind === KIND_GIFT_WRAP_EPHEMERAL ? Math.floor(Date.now() / 1000) : randomizedPastTimestamp();\n const wrap = finalizeEvent(\n {\n kind: wrapKind,\n created_at: createdAt,\n tags: [['p', server.pubkey]],\n content: nip44.encrypt(JSON.stringify(inner), conversationKey),\n },\n wrapSecretKey,\n ) as NostrEventLike;\n pool.publish(relays, wrap);\n }\n\n function sendCorrelated(\n server: CvmServerRef,\n relays: string[],\n message: McpMessage,\n timeout: number,\n ): Promise<McpMessage> {\n const correlationId = nextCorrelationId();\n const originalId = message.id;\n const outgoing: McpMessage = { ...message, id: correlationId };\n return new Promise<McpMessage>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(correlationId);\n reject(new Error('relay timeout'));\n }, timeout);\n pending.set(correlationId, { resolve, reject, timer, originalId });\n try {\n publishMcp(server, relays, outgoing);\n } catch (err) {\n pending.delete(correlationId);\n clearTimeout(timer);\n reject(err instanceof Error ? err : new Error('publish failed'));\n }\n });\n }\n\n function getSession(server: CvmServerRef): ServerSession {\n let session = sessions.get(server.pubkey);\n if (!session) {\n const relays = resolveRelays(server);\n session = { relays, initialized: false, initializing: null };\n sessions.set(server.pubkey, session);\n holdRelays(relays);\n }\n return session;\n }\n\n async function ensureInitialized(server: CvmServerRef, session: ServerSession, timeout: number): Promise<void> {\n if (session.initialized) return;\n if (session.initializing) return session.initializing;\n session.initializing = (async () => {\n try {\n await sendCorrelated(\n server,\n session.relays,\n {\n jsonrpc: '2.0',\n id: 'init',\n method: 'initialize',\n params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo },\n },\n timeout,\n );\n // notifications/initialized completes the handshake; no response expected.\n publishMcp(server, session.relays, { jsonrpc: '2.0', method: 'notifications/initialized' });\n session.initialized = true;\n } catch {\n throw new Error('initialization failed');\n } finally {\n session.initializing = null;\n }\n })();\n return session.initializing;\n }\n\n return {\n async discover(query?: CvmDiscoverQuery): Promise<CvmServer[]> {\n const relays = query?.relays && query.relays.length > 0 ? query.relays : defaultRelays;\n if (relays.length === 0) return [];\n const announces = new Map<string, NostrEventLike>();\n const toolLists = new Map<string, NostrEventLike>();\n await new Promise<void>((resolve) => {\n const sub = pool.subscribe(\n relays,\n { kinds: [KIND_ANNOUNCE_SERVER, KIND_ANNOUNCE_TOOLS], limit: query?.limit ? query.limit * 4 : 100 },\n {\n onevent(event) {\n if (event.kind === KIND_ANNOUNCE_SERVER) announces.set(event.pubkey, event);\n else if (event.kind === KIND_ANNOUNCE_TOOLS) toolLists.set(event.pubkey, event);\n },\n oneose() { resolve(); },\n },\n );\n setTimeout(() => { sub.close(); resolve(); }, DEFAULT_DISCOVER_TIMEOUT_MS);\n });\n\n const servers: CvmServer[] = [];\n for (const [pubkey, event] of announces) {\n const name = tagValue(event.tags, 'name');\n const description = tagValue(event.tags, 'about');\n const server: CvmServer = {\n pubkey,\n relays: [...relays],\n ...(name ? { name } : {}),\n ...(description ? { description } : {}),\n paymentRequired: false,\n };\n const tools = toolLists.get(pubkey);\n if (tools) {\n const names = tools.tags.filter((tag) => tag[0] === 'i' && typeof tag[2] === 'string').map((tag) => tag[2]);\n if (names.length > 0) server.capabilities = names;\n }\n servers.push(server);\n }\n\n const search = query?.search?.toLowerCase();\n const filtered = search\n ? servers.filter((s) => `${s.name ?? ''} ${s.description ?? ''}`.toLowerCase().includes(search))\n : servers;\n return query?.limit ? filtered.slice(0, query.limit) : filtered;\n },\n\n async request(server: CvmServerRef, message: McpMessage, requestOptions?: CvmRequestOptions): Promise<McpMessage> {\n const session = getSession(server);\n const timeout = requestOptions?.timeoutMs ?? timeoutMs;\n if (requestOptions?.initialize) await ensureInitialized(server, session, timeout);\n return sendCorrelated(server, session.relays, message, timeout);\n },\n\n async close(server: CvmServerRef): Promise<void> {\n const session = sessions.get(server.pubkey);\n if (!session) return;\n sessions.delete(server.pubkey);\n releaseRelays(session.relays);\n },\n\n onEvent(handler: EventHandler): { close(): void } {\n eventHandlers.add(handler);\n return {\n close() {\n eventHandlers.delete(handler);\n },\n };\n },\n\n dispose(): void {\n inbound?.close();\n inbound = null;\n for (const entry of pending.values()) {\n clearTimeout(entry.timer);\n entry.reject(new Error('transport disposed'));\n }\n pending.clear();\n sessions.clear();\n relayRefcount.clear();\n eventHandlers.clear();\n subscribedRelays = '';\n },\n };\n}\n"],"mappings":"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","names":["encrypt"]}
|
|
1
|
+
{"version":3,"sources":["../src/cvm-nostr-transport.ts"],"sourcesContent":["/**\n * cvm-nostr-transport.ts — concrete ContextVM transport for NAP-CVM.\n *\n * Implements {@link CvmTransport} over Nostr, exactly as validated against live\n * ContextVM servers (e.g. Relatr):\n *\n * - MCP JSON-RPC messages ride in kind-25910 event `content`.\n * - Requests are CEP-4 gift-wrapped: the inner kind-25910 event is signed with\n * the shell's ephemeral client key, NIP-44-encrypted to the server, and\n * placed in a kind-21059 (ephemeral) / 1059 (regular) wrap signed by a fresh\n * random key, `p`-tagged to the server. Responses arrive the same way,\n * `p`-tagged to the client, and are correlated by the inner JSON-RPC `id`.\n * - Discovery reads kind-11316 (server) + kind-11317 (tools) announcements.\n *\n * Shipped on a separate entry (`@kehto/services/cvm-nostr-transport`) so the\n * `nostr-tools` dependency stays out of the core `@kehto/services` bundle.\n *\n * The client key is ephemeral and shell-owned: napplets never see keys, relay\n * sockets, or NIP-44 material (NAP-CVM §Security).\n *\n * @example\n * ```ts\n * import { createNostrCvmTransport } from '@kehto/services/cvm-nostr-transport';\n * const transport = createNostrCvmTransport({\n * defaultRelays: ['wss://relay.contextvm.org', 'wss://relay2.contextvm.org'],\n * });\n * ```\n */\n\nimport { SimplePool } from 'nostr-tools/pool';\nimport { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';\nimport * as nip44 from 'nostr-tools/nip44';\nimport type { Event as NostrToolsEvent, Filter as NostrToolsFilter } from 'nostr-tools';\n\nimport type { CvmTransport } from './cvm-service.js';\nimport type {\n CvmDiscoverQuery,\n CvmRequestOptions,\n CvmServer,\n CvmServerRef,\n McpMessage,\n} from './cvm-types.js';\n\n/** ContextVM unified transport event kind. */\nconst KIND_CVM = 25910;\n/** CEP-4 gift-wrap kinds: ephemeral (CEP-19) and regular. */\nconst KIND_GIFT_WRAP_EPHEMERAL = 21059;\nconst KIND_GIFT_WRAP_REGULAR = 1059;\n/** CEP-6 announcement kinds. */\nconst KIND_ANNOUNCE_SERVER = 11316;\nconst KIND_ANNOUNCE_TOOLS = 11317;\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_DISCOVER_TIMEOUT_MS = 6_000;\nconst MCP_PROTOCOL_VERSION = '2025-11-25';\nconst SEEN_WRAP_LIMIT = 512;\n\n/** Minimal signed Nostr event. */\nexport interface NostrEventLike {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** A Nostr REQ filter (subset). */\nexport interface NostrFilterLike {\n kinds?: number[];\n authors?: string[];\n limit?: number;\n ['#p']?: string[];\n [key: string]: unknown;\n}\n\n/** Subscription handle returned by the relay pool. */\nexport interface CvmSubCloser {\n close(): void;\n}\n\n/**\n * Minimal relay-pool surface used by this transport — structurally satisfied\n * by `nostr-tools` `SimplePool`. Injectable for testing.\n */\nexport interface CvmRelayPool {\n subscribe(\n relays: string[],\n filter: NostrFilterLike,\n params: { onevent?: (event: NostrEventLike) => void; oneose?: () => void },\n ): CvmSubCloser;\n publish(relays: string[], event: NostrEventLike): unknown;\n}\n\n/** Options for {@link createNostrCvmTransport}. */\nexport interface NostrCvmTransportOptions {\n /** Relays used when a server reference carries no relay hints. */\n defaultRelays?: string[];\n /** Default per-request timeout in milliseconds. */\n timeoutMs?: number;\n /** Whether to CEP-4 gift-wrap requests. Default true (most servers require it). */\n encrypt?: boolean;\n /** Use ephemeral (kind 21059) gift wraps when encrypting. Default true. */\n ephemeralWrap?: boolean;\n /** Relay pool to use. Defaults to a fresh `nostr-tools` `SimplePool`. */\n pool?: CvmRelayPool;\n /** Client secret key (32 bytes). Defaults to a generated ephemeral key. */\n clientSecretKey?: Uint8Array;\n /** Client info advertised during MCP `initialize`. */\n clientInfo?: { name: string; version: string };\n}\n\ntype EventHandler = (server: CvmServerRef, message: McpMessage) => void;\n\ninterface PendingRequest {\n resolve(message: McpMessage): void;\n reject(error: Error): void;\n timer: ReturnType<typeof setTimeout>;\n /** The caller's original JSON-RPC id, restored on the response. */\n originalId: string | number | undefined;\n}\n\ninterface ServerSession {\n relays: string[];\n initialized: boolean;\n initializing: Promise<void> | null;\n}\n\nlet correlationCounter = 0;\nfunction nextCorrelationId(): string {\n correlationCounter += 1;\n return `cvm-${correlationCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;\n}\n\nfunction randomizedPastTimestamp(): number {\n // NIP-59: randomize within the past two days to reduce timing metadata.\n const jitter = Math.floor(Math.random() * 172_800);\n return Math.floor(Date.now() / 1000) - jitter;\n}\n\nfunction tagValue(tags: string[][], name: string): string | undefined {\n return tags.find((tag) => tag[0] === name)?.[1];\n}\n\n/** Adapt a `nostr-tools` SimplePool to the {@link CvmRelayPool} surface. */\nfunction simplePoolAdapter(sp: SimplePool): CvmRelayPool {\n return {\n subscribe(relays, filter, params) {\n return sp.subscribe(relays, filter as NostrToolsFilter, {\n onevent: params.onevent,\n oneose: params.oneose,\n });\n },\n publish(relays, event) {\n return sp.publish(relays, event as NostrToolsEvent);\n },\n };\n}\n\n/**\n * Create a Nostr-backed ContextVM transport.\n *\n * @param options - Relay set, timeouts, encryption mode, and optional injected\n * pool/keys (the injected pool + key make the transport deterministic in tests).\n * @returns A {@link CvmTransport} plus a `dispose()` to tear down subscriptions.\n */\nexport function createNostrCvmTransport(\n options: NostrCvmTransportOptions = {},\n): CvmTransport & { dispose(): void } {\n const defaultRelays = options.defaultRelays ?? [];\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const encrypt = options.encrypt ?? true;\n const wrapKind = options.ephemeralWrap === false ? KIND_GIFT_WRAP_REGULAR : KIND_GIFT_WRAP_EPHEMERAL;\n const pool: CvmRelayPool = options.pool ?? simplePoolAdapter(new SimplePool());\n const clientSecretKey = options.clientSecretKey ?? generateSecretKey();\n const clientPubkey = getPublicKey(clientSecretKey);\n const clientInfo = options.clientInfo ?? { name: 'kehto-cvm', version: '1.0.0' };\n\n const sessions = new Map<string, ServerSession>();\n const pending = new Map<string, PendingRequest>();\n const eventHandlers = new Set<EventHandler>();\n const relayRefcount = new Map<string, number>();\n const seenWraps = new Set<string>();\n let inbound: CvmSubCloser | null = null;\n let subscribedRelays = '';\n\n function resolveRelays(server: CvmServerRef): string[] {\n const relays = server.relays && server.relays.length > 0 ? server.relays : defaultRelays;\n if (relays.length === 0) throw new Error('server not found');\n return [...new Set(relays)];\n }\n\n function holdRelays(relays: string[]): void {\n for (const url of relays) relayRefcount.set(url, (relayRefcount.get(url) ?? 0) + 1);\n refreshSubscription();\n }\n\n function releaseRelays(relays: string[]): void {\n for (const url of relays) {\n const count = (relayRefcount.get(url) ?? 0) - 1;\n if (count <= 0) relayRefcount.delete(url);\n else relayRefcount.set(url, count);\n }\n refreshSubscription();\n }\n\n function refreshSubscription(): void {\n const relays = [...relayRefcount.keys()].sort();\n const key = relays.join(',');\n if (key === subscribedRelays) return;\n inbound?.close();\n subscribedRelays = key;\n inbound = relays.length === 0\n ? null\n : pool.subscribe(\n relays,\n { kinds: encrypt ? [KIND_GIFT_WRAP_REGULAR, KIND_GIFT_WRAP_EPHEMERAL] : [KIND_CVM], ['#p']: [clientPubkey] },\n { onevent: handleInbound },\n );\n }\n\n function rememberWrap(id: string): boolean {\n if (seenWraps.has(id)) return false;\n seenWraps.add(id);\n if (seenWraps.size > SEEN_WRAP_LIMIT) {\n const oldest = seenWraps.values().next().value;\n if (oldest !== undefined) seenWraps.delete(oldest);\n }\n return true;\n }\n\n function handleInbound(event: NostrEventLike): void {\n if (!rememberWrap(event.id)) return;\n let serverPubkey: string;\n let mcp: McpMessage;\n try {\n if (encrypt) {\n const conversationKey = nip44.getConversationKey(clientSecretKey, event.pubkey);\n const inner = JSON.parse(nip44.decrypt(event.content, conversationKey)) as NostrEventLike;\n serverPubkey = inner.pubkey;\n mcp = JSON.parse(inner.content) as McpMessage;\n } else {\n serverPubkey = event.pubkey;\n mcp = JSON.parse(event.content) as McpMessage;\n }\n } catch {\n return; // not addressed to us / undecryptable / malformed — ignore.\n }\n\n const id = mcp.id;\n if (id != null && pending.has(String(id))) {\n const entry = pending.get(String(id))!;\n pending.delete(String(id));\n clearTimeout(entry.timer);\n entry.resolve({ ...mcp, id: entry.originalId });\n return;\n }\n // Uncorrelated server message (notification) → fan out as a CVM event.\n if (mcp.method !== undefined && sessions.has(serverPubkey)) {\n const server: CvmServerRef = { pubkey: serverPubkey };\n for (const handler of eventHandlers) handler(server, mcp);\n }\n }\n\n function publishMcp(server: CvmServerRef, relays: string[], message: McpMessage): void {\n const inner = finalizeEvent(\n { kind: KIND_CVM, created_at: Math.floor(Date.now() / 1000), tags: [['p', server.pubkey]], content: JSON.stringify(message) },\n clientSecretKey,\n ) as NostrEventLike;\n if (!encrypt) {\n pool.publish(relays, inner);\n return;\n }\n const wrapSecretKey = generateSecretKey();\n const conversationKey = nip44.getConversationKey(wrapSecretKey, server.pubkey);\n // Ephemeral wraps (kind 21059) are not stored; relays reject backdated\n // ephemeral events as \"expired\", so they MUST carry a current timestamp.\n // Regular wraps (kind 1059) are backdated per NIP-59 to blur timing metadata.\n const createdAt =\n wrapKind === KIND_GIFT_WRAP_EPHEMERAL ? Math.floor(Date.now() / 1000) : randomizedPastTimestamp();\n const wrap = finalizeEvent(\n {\n kind: wrapKind,\n created_at: createdAt,\n tags: [['p', server.pubkey]],\n content: nip44.encrypt(JSON.stringify(inner), conversationKey),\n },\n wrapSecretKey,\n ) as NostrEventLike;\n pool.publish(relays, wrap);\n }\n\n function sendCorrelated(\n server: CvmServerRef,\n relays: string[],\n message: McpMessage,\n timeout: number,\n ): Promise<McpMessage> {\n const correlationId = nextCorrelationId();\n const originalId = message.id;\n const outgoing: McpMessage = { ...message, id: correlationId };\n return new Promise<McpMessage>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(correlationId);\n reject(new Error('relay timeout'));\n }, timeout);\n pending.set(correlationId, { resolve, reject, timer, originalId });\n try {\n publishMcp(server, relays, outgoing);\n } catch (err) {\n pending.delete(correlationId);\n clearTimeout(timer);\n reject(err instanceof Error ? err : new Error('publish failed'));\n }\n });\n }\n\n function getSession(server: CvmServerRef): ServerSession {\n let session = sessions.get(server.pubkey);\n if (!session) {\n const relays = resolveRelays(server);\n session = { relays, initialized: false, initializing: null };\n sessions.set(server.pubkey, session);\n holdRelays(relays);\n }\n return session;\n }\n\n async function ensureInitialized(server: CvmServerRef, session: ServerSession, timeout: number): Promise<void> {\n if (session.initialized) return;\n if (session.initializing) return session.initializing;\n session.initializing = (async () => {\n try {\n await sendCorrelated(\n server,\n session.relays,\n {\n jsonrpc: '2.0',\n id: 'init',\n method: 'initialize',\n params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo },\n },\n timeout,\n );\n // notifications/initialized completes the handshake; no response expected.\n publishMcp(server, session.relays, { jsonrpc: '2.0', method: 'notifications/initialized' });\n session.initialized = true;\n } catch {\n throw new Error('initialization failed');\n } finally {\n session.initializing = null;\n }\n })();\n return session.initializing;\n }\n\n return {\n async discover(query?: CvmDiscoverQuery): Promise<CvmServer[]> {\n const relays = query?.relays && query.relays.length > 0 ? query.relays : defaultRelays;\n if (relays.length === 0) return [];\n const announces = new Map<string, NostrEventLike>();\n const toolLists = new Map<string, NostrEventLike>();\n await new Promise<void>((resolve) => {\n const sub = pool.subscribe(\n relays,\n { kinds: [KIND_ANNOUNCE_SERVER, KIND_ANNOUNCE_TOOLS], limit: query?.limit ? query.limit * 4 : 100 },\n {\n onevent(event) {\n if (event.kind === KIND_ANNOUNCE_SERVER) announces.set(event.pubkey, event);\n else if (event.kind === KIND_ANNOUNCE_TOOLS) toolLists.set(event.pubkey, event);\n },\n oneose() { resolve(); },\n },\n );\n setTimeout(() => { sub.close(); resolve(); }, DEFAULT_DISCOVER_TIMEOUT_MS);\n });\n\n const servers: CvmServer[] = [];\n for (const [pubkey, event] of announces) {\n const name = tagValue(event.tags, 'name');\n const description = tagValue(event.tags, 'about');\n const server: CvmServer = {\n pubkey,\n relays: [...relays],\n ...(name ? { name } : {}),\n ...(description ? { description } : {}),\n paymentRequired: false,\n };\n const tools = toolLists.get(pubkey);\n if (tools) {\n const names = tools.tags.filter((tag) => tag[0] === 'i' && typeof tag[2] === 'string').map((tag) => tag[2]);\n if (names.length > 0) server.capabilities = names;\n }\n servers.push(server);\n }\n\n const search = query?.search?.toLowerCase();\n const filtered = search\n ? servers.filter((s) => `${s.name ?? ''} ${s.description ?? ''}`.toLowerCase().includes(search))\n : servers;\n return query?.limit ? filtered.slice(0, query.limit) : filtered;\n },\n\n async request(server: CvmServerRef, message: McpMessage, requestOptions?: CvmRequestOptions): Promise<McpMessage> {\n const session = getSession(server);\n const timeout = requestOptions?.timeoutMs ?? timeoutMs;\n if (requestOptions?.initialize) await ensureInitialized(server, session, timeout);\n return sendCorrelated(server, session.relays, message, timeout);\n },\n\n async close(server: CvmServerRef): Promise<void> {\n const session = sessions.get(server.pubkey);\n if (!session) return;\n sessions.delete(server.pubkey);\n releaseRelays(session.relays);\n },\n\n onEvent(handler: EventHandler): { close(): void } {\n eventHandlers.add(handler);\n return {\n close() {\n eventHandlers.delete(handler);\n },\n };\n },\n\n dispose(): void {\n inbound?.close();\n inbound = null;\n for (const entry of pending.values()) {\n clearTimeout(entry.timer);\n entry.reject(new Error('transport disposed'));\n }\n pending.clear();\n sessions.clear();\n relayRefcount.clear();\n eventHandlers.clear();\n subscribedRelays = '';\n },\n };\n}\n"],"mappings":"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","names":["encrypt"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServiceHandler, Signer } from '@kehto/runtime';
|
|
2
|
-
import { NostrEvent, NappletMessage, NostrFilter } from '@napplet/core';
|
|
2
|
+
import { NostrEvent, NappletMessage, NostrFilter, EventTemplate } from '@napplet/core';
|
|
3
3
|
import { MediaMetadata, MediaAction } from '@napplet/nub/media/types';
|
|
4
4
|
export { MediaAction } from '@napplet/nub/media/types';
|
|
5
5
|
import { NotifySendMessage } from '@napplet/nub/notify/types';
|
|
@@ -1532,4 +1532,500 @@ type ResourceService = ServiceHandler;
|
|
|
1532
1532
|
*/
|
|
1533
1533
|
declare function createResourceService(options: ResourceServiceOptions): ResourceService;
|
|
1534
1534
|
|
|
1535
|
-
|
|
1535
|
+
/**
|
|
1536
|
+
* outbox-service.ts — NAP-OUTBOX (outbox-aware relay routing) reference service.
|
|
1537
|
+
*
|
|
1538
|
+
* Shell-side handler for the NAP-OUTBOX wire protocol. It is a pure envelope
|
|
1539
|
+
* router: it validates `outbox.*` envelopes, delegates the actual relay
|
|
1540
|
+
* discovery / routing / dedup / publish-fanout work to an injected
|
|
1541
|
+
* {@link OutboxRouter}, and posts the correlated result / lifecycle messages
|
|
1542
|
+
* (echoing the request `id` or `subId`) back to the napplet.
|
|
1543
|
+
*
|
|
1544
|
+
* The router is injected (options-as-bridge) so this service has no Nostr
|
|
1545
|
+
* dependency and is fully unit-testable. A concrete relay-pool-backed router
|
|
1546
|
+
* ships alongside as {@link createRelayPoolOutboxRouter}.
|
|
1547
|
+
*
|
|
1548
|
+
* ──────────────────────────── Responsibilities ────────────────────────────
|
|
1549
|
+
* Inbound: outbox.query, outbox.subscribe, outbox.close, outbox.publish,
|
|
1550
|
+
* outbox.resolveRelays
|
|
1551
|
+
* Outbound: outbox.query.result, outbox.event, outbox.eose, outbox.closed,
|
|
1552
|
+
* outbox.publish.result, outbox.resolveRelays.result
|
|
1553
|
+
*
|
|
1554
|
+
* The shell owns relay discovery, routing, fallback, deduplication, signature
|
|
1555
|
+
* validation, signing, and publish fanout policy — all of which live behind
|
|
1556
|
+
* the {@link OutboxRouter}. This service only marshals the wire protocol.
|
|
1557
|
+
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```ts
|
|
1560
|
+
* import { createOutboxService, createRelayPoolOutboxRouter } from '@kehto/services';
|
|
1561
|
+
*
|
|
1562
|
+
* const router = createRelayPoolOutboxRouter({ relayPool, loadRelayLists, fallbackRelays });
|
|
1563
|
+
* runtime.registerService('outbox', createOutboxService({ router }));
|
|
1564
|
+
* ```
|
|
1565
|
+
*
|
|
1566
|
+
* @packageDocumentation
|
|
1567
|
+
*/
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Relay-selection strategy:
|
|
1571
|
+
* - `outbox` — query/publish via author write relays (the outbox model)
|
|
1572
|
+
* - `inbox` — query/publish via recipient read relays (the inbox model)
|
|
1573
|
+
* - `auto` — let the shell choose per its policy and relay intelligence
|
|
1574
|
+
*/
|
|
1575
|
+
type OutboxStrategy = 'outbox' | 'inbox' | 'auto';
|
|
1576
|
+
/** Options for a one-shot outbox query. */
|
|
1577
|
+
interface OutboxQueryOptions {
|
|
1578
|
+
/** Explicit author hints (augment/override authors derived from filters). */
|
|
1579
|
+
authors?: string[];
|
|
1580
|
+
/** Relay hints; treated as a hint subject to shell validation, not a bypass. */
|
|
1581
|
+
relays?: string[];
|
|
1582
|
+
/** Relay-selection strategy. */
|
|
1583
|
+
strategy?: OutboxStrategy;
|
|
1584
|
+
/** Maximum events to collect. */
|
|
1585
|
+
limit?: number;
|
|
1586
|
+
/** Wall-clock budget for the query, in milliseconds. */
|
|
1587
|
+
timeoutMs?: number;
|
|
1588
|
+
}
|
|
1589
|
+
/** Options for a live outbox subscription. */
|
|
1590
|
+
interface OutboxSubscribeOptions extends OutboxQueryOptions {
|
|
1591
|
+
/** Keep the subscription open for real-time events after EOSE. */
|
|
1592
|
+
live?: boolean;
|
|
1593
|
+
}
|
|
1594
|
+
/** Options for an outbox publish. */
|
|
1595
|
+
interface OutboxPublishOptions {
|
|
1596
|
+
/** Relay hints; treated as a hint subject to shell validation. */
|
|
1597
|
+
relays?: string[];
|
|
1598
|
+
/** Recipient authors whose inbox relays should be included for directed events. */
|
|
1599
|
+
targetAuthors?: string[];
|
|
1600
|
+
/** Relay-selection strategy. */
|
|
1601
|
+
strategy?: OutboxStrategy;
|
|
1602
|
+
}
|
|
1603
|
+
/** A read/write target for relay-plan resolution. */
|
|
1604
|
+
interface OutboxTarget {
|
|
1605
|
+
/** Authors to resolve relays for. */
|
|
1606
|
+
authors?: string[];
|
|
1607
|
+
/** Single pubkey to resolve relays for. */
|
|
1608
|
+
pubkey?: string;
|
|
1609
|
+
/** Whether the plan is for reading (their write relays) or writing (their read relays). */
|
|
1610
|
+
direction?: 'read' | 'write';
|
|
1611
|
+
/** Relay-selection strategy. */
|
|
1612
|
+
strategy?: OutboxStrategy;
|
|
1613
|
+
}
|
|
1614
|
+
/** The relay plan the shell would use for a target. */
|
|
1615
|
+
interface OutboxRelayPlan {
|
|
1616
|
+
/** Resolved relay URLs. */
|
|
1617
|
+
relays: string[];
|
|
1618
|
+
/** Where the plan came from. */
|
|
1619
|
+
source: 'nip65' | 'cache' | 'policy' | 'fallback';
|
|
1620
|
+
/** Authors for which no relay list could be resolved. */
|
|
1621
|
+
missingAuthors?: string[];
|
|
1622
|
+
}
|
|
1623
|
+
/** Outcome of an outbox query, as returned by the {@link OutboxRouter}. */
|
|
1624
|
+
interface OutboxResult {
|
|
1625
|
+
/** Deduplicated, signature-validated events. */
|
|
1626
|
+
events: NostrEvent[];
|
|
1627
|
+
/** Map of event id -> relay URLs where the shell observed the event. */
|
|
1628
|
+
relays: Record<string, string[]>;
|
|
1629
|
+
/** True when some relay lists or connections failed and results are partial. */
|
|
1630
|
+
incomplete?: boolean;
|
|
1631
|
+
/** Error reason when the query could not complete. */
|
|
1632
|
+
error?: string;
|
|
1633
|
+
}
|
|
1634
|
+
/** Outcome of an outbox publish, as returned by the {@link OutboxRouter}. */
|
|
1635
|
+
interface OutboxPublishResult {
|
|
1636
|
+
/** Whether the publish succeeded on at least the required relays. */
|
|
1637
|
+
ok: boolean;
|
|
1638
|
+
/** The signed event returned by the shell. */
|
|
1639
|
+
event?: NostrEvent;
|
|
1640
|
+
/** The published event id. */
|
|
1641
|
+
eventId?: string;
|
|
1642
|
+
/** Map of relay URL -> per-relay publish success. */
|
|
1643
|
+
relays?: Record<string, boolean>;
|
|
1644
|
+
/** Error reason when the publish failed. */
|
|
1645
|
+
error?: string;
|
|
1646
|
+
}
|
|
1647
|
+
/** Sink an {@link OutboxRouter} streams subscription lifecycle through. */
|
|
1648
|
+
interface OutboxSubscriptionSink {
|
|
1649
|
+
/** Deliver a matching event; `relay` is the relay it was observed on, when known. */
|
|
1650
|
+
event(event: NostrEvent, relay?: string): void;
|
|
1651
|
+
/** Signal end-of-stored-events. */
|
|
1652
|
+
eose(): void;
|
|
1653
|
+
/** Signal that the subscription was closed upstream; `reason` is optional. */
|
|
1654
|
+
closed(reason?: string): void;
|
|
1655
|
+
}
|
|
1656
|
+
/** Handle to a router-owned subscription. */
|
|
1657
|
+
interface OutboxRouterSubscription {
|
|
1658
|
+
/** Stop the subscription and release its relay connections. */
|
|
1659
|
+
close(): void;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Abstract outbox router. Implementors own relay discovery (NIP-65 / NIP-66),
|
|
1663
|
+
* routing, fallback, deduplication, signature validation, signing, and publish
|
|
1664
|
+
* fanout. The service translates wire envelopes into these calls and back.
|
|
1665
|
+
*/
|
|
1666
|
+
interface OutboxRouter {
|
|
1667
|
+
/** Resolve relays, query them, dedup by id, validate signatures, collect events. */
|
|
1668
|
+
query(filters: NostrFilter[], options?: OutboxQueryOptions): Promise<OutboxResult>;
|
|
1669
|
+
/** Open a live outbox-aware subscription, streaming through `sink`. */
|
|
1670
|
+
subscribe(filters: NostrFilter[], options: OutboxSubscribeOptions | undefined, sink: OutboxSubscriptionSink): OutboxRouterSubscription;
|
|
1671
|
+
/** Sign `template` and fan it out to the relevant write/inbox relays. */
|
|
1672
|
+
publish(template: EventTemplate, options?: OutboxPublishOptions): Promise<OutboxPublishResult>;
|
|
1673
|
+
/** Return the relay plan the shell would use for a read/write target. */
|
|
1674
|
+
resolveRelays(target: OutboxTarget): Promise<OutboxRelayPlan>;
|
|
1675
|
+
}
|
|
1676
|
+
/** Options for {@link createOutboxService}. */
|
|
1677
|
+
interface OutboxServiceOptions {
|
|
1678
|
+
/** The outbox router the shell uses to reach relays. Required. */
|
|
1679
|
+
router: OutboxRouter;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Create the NAP-OUTBOX service handler.
|
|
1683
|
+
*
|
|
1684
|
+
* @param options - Must provide an {@link OutboxRouter}.
|
|
1685
|
+
* @returns A `ServiceHandler` ready for `runtime.registerService('outbox', handler)`.
|
|
1686
|
+
* @throws If `options.router` is missing.
|
|
1687
|
+
*/
|
|
1688
|
+
declare function createOutboxService(options: OutboxServiceOptions): ServiceHandler;
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* relay-pool-outbox-router.ts — concrete {@link OutboxRouter} backed by a relay pool.
|
|
1692
|
+
*
|
|
1693
|
+
* Implements the outbox-model routing that NAP-OUTBOX centralizes so napplets
|
|
1694
|
+
* don't each reinvent it: derive authors, resolve their NIP-65 relays, fan a
|
|
1695
|
+
* per-relay subscription out across the plan, deduplicate by event id (while
|
|
1696
|
+
* recording every relay an event was observed on), validate signatures, and —
|
|
1697
|
+
* for publish — sign the template and fan it out to the relevant write/inbox
|
|
1698
|
+
* relays.
|
|
1699
|
+
*
|
|
1700
|
+
* NIP-65 relay-list *fetching* is the host's concern (it may come from a
|
|
1701
|
+
* kind-10002 cache, a NIP-66 indexer via `@kehto/nip/66`, or a live query), so
|
|
1702
|
+
* it is injected via {@link RelayPoolOutboxRouterOptions.loadRelayLists}. The
|
|
1703
|
+
* relay pool, signer, and signature verifier are injected too — keeping this
|
|
1704
|
+
* router browser-agnostic and unit-testable with mocks.
|
|
1705
|
+
*
|
|
1706
|
+
* Relay-selection model (per the outbox model):
|
|
1707
|
+
* - reading an author's events → their **write** relays (where they publish)
|
|
1708
|
+
* - writing to reach an author → their **read** relays (their inbox)
|
|
1709
|
+
*
|
|
1710
|
+
* `strategy` overrides the direction default: `outbox` forces write relays,
|
|
1711
|
+
* `inbox` forces read relays, `auto` (default) follows the read/write direction.
|
|
1712
|
+
*
|
|
1713
|
+
* @example
|
|
1714
|
+
* ```ts
|
|
1715
|
+
* import { createOutboxService, createRelayPoolOutboxRouter } from '@kehto/services';
|
|
1716
|
+
*
|
|
1717
|
+
* const router = createRelayPoolOutboxRouter({
|
|
1718
|
+
* relayPool: myOutboxPool,
|
|
1719
|
+
* loadRelayLists: (pubkeys) => relayListCache.getMany(pubkeys),
|
|
1720
|
+
* fallbackRelays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
1721
|
+
* signEvent: (tmpl) => signer.signEvent(tmpl),
|
|
1722
|
+
* verifyEvent: (ev) => verifyEvent(ev),
|
|
1723
|
+
* });
|
|
1724
|
+
* runtime.registerService('outbox', createOutboxService({ router }));
|
|
1725
|
+
* ```
|
|
1726
|
+
*
|
|
1727
|
+
* @packageDocumentation
|
|
1728
|
+
*/
|
|
1729
|
+
|
|
1730
|
+
/** A NIP-65 relay list for a single pubkey. */
|
|
1731
|
+
interface RelayListEntry {
|
|
1732
|
+
/** Relays the author reads from (their inbox). */
|
|
1733
|
+
read: string[];
|
|
1734
|
+
/** Relays the author writes to (where their events land). */
|
|
1735
|
+
write: string[];
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Relay pool contract the router drives. Implementors adapt their pool library
|
|
1739
|
+
* (nostr-tools SimplePool, applesauce-relay, etc.). Unlike the lower-level
|
|
1740
|
+
* relay NUB pool, both methods take an explicit relay-URL set so the router
|
|
1741
|
+
* controls outbox routing and can attribute events to the relay they arrived on.
|
|
1742
|
+
*/
|
|
1743
|
+
interface OutboxRelayPool {
|
|
1744
|
+
/**
|
|
1745
|
+
* Subscribe to `filters` on exactly `relayUrls`. The callback receives each
|
|
1746
|
+
* matching event or the literal `'EOSE'` once stored events are exhausted.
|
|
1747
|
+
* Returns a handle to cancel the subscription.
|
|
1748
|
+
*/
|
|
1749
|
+
subscribe(filters: NostrFilter[], relayUrls: string[], callback: (item: NostrEvent | 'EOSE') => void): {
|
|
1750
|
+
unsubscribe(): void;
|
|
1751
|
+
};
|
|
1752
|
+
/**
|
|
1753
|
+
* Publish `event` to `relayUrls`. May return a per-relay success map; a
|
|
1754
|
+
* `void`/missing return is treated as optimistic success on every target.
|
|
1755
|
+
*/
|
|
1756
|
+
publish(event: NostrEvent, relayUrls: string[]): Promise<Record<string, boolean>> | Record<string, boolean> | void;
|
|
1757
|
+
/** Whether the relay pool is connected and able to handle requests. */
|
|
1758
|
+
isAvailable(): boolean;
|
|
1759
|
+
}
|
|
1760
|
+
/** Options for {@link createRelayPoolOutboxRouter}. */
|
|
1761
|
+
interface RelayPoolOutboxRouterOptions {
|
|
1762
|
+
/** Relay pool the router subscribes/publishes through. Required. */
|
|
1763
|
+
relayPool: OutboxRelayPool;
|
|
1764
|
+
/**
|
|
1765
|
+
* Resolve NIP-65 relay lists for a set of pubkeys. Pubkeys with no known
|
|
1766
|
+
* list are simply omitted from the returned map (they become `missingAuthors`).
|
|
1767
|
+
*/
|
|
1768
|
+
loadRelayLists(pubkeys: string[]): Promise<Map<string, RelayListEntry>> | Map<string, RelayListEntry>;
|
|
1769
|
+
/** Relays to fall back to when NIP-65 data is absent, stale, or empty. Required. */
|
|
1770
|
+
fallbackRelays: string[];
|
|
1771
|
+
/**
|
|
1772
|
+
* Sign a template before publish (shell-mediated; napplets never sign). When
|
|
1773
|
+
* omitted, `publish` resolves with `{ ok: false, error: 'publish denied' }`.
|
|
1774
|
+
*/
|
|
1775
|
+
signEvent?(template: EventTemplate): Promise<NostrEvent>;
|
|
1776
|
+
/**
|
|
1777
|
+
* Validate an event signature before delivering it to a napplet. May be sync
|
|
1778
|
+
* or async. Defaults to accepting every event (host pools often pre-verify).
|
|
1779
|
+
*/
|
|
1780
|
+
verifyEvent?(event: NostrEvent): Promise<boolean> | boolean;
|
|
1781
|
+
/**
|
|
1782
|
+
* Gate relay URLs (e.g. block private-network hosts). Defaults to allowing
|
|
1783
|
+
* only `ws://` / `wss://` URLs — `options.relays` hints pass through this too.
|
|
1784
|
+
*/
|
|
1785
|
+
isRelayAllowed?(url: string): boolean;
|
|
1786
|
+
/** Default query timeout when `options.timeoutMs` is unset. Default 4000ms. */
|
|
1787
|
+
defaultTimeoutMs?: number;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Create a relay-pool-backed {@link OutboxRouter}.
|
|
1791
|
+
*
|
|
1792
|
+
* @param options - Relay pool, NIP-65 loader, fallback relays, and optional
|
|
1793
|
+
* signer / verifier / relay gate / timeout.
|
|
1794
|
+
* @returns An {@link OutboxRouter} for {@link createOutboxService}.
|
|
1795
|
+
* @throws If `relayPool`, `loadRelayLists`, or `fallbackRelays` are missing.
|
|
1796
|
+
*/
|
|
1797
|
+
declare function createRelayPoolOutboxRouter(options: RelayPoolOutboxRouterOptions): OutboxRouter;
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* upload-service.ts — NAP-UPLOAD (shell-mediated file/blob upload) reference service.
|
|
1801
|
+
*
|
|
1802
|
+
* Shell-side handler for the NAP-UPLOAD wire protocol. It is a pure envelope
|
|
1803
|
+
* router: it validates `upload.*` envelopes, delegates the actual byte transfer
|
|
1804
|
+
* (server selection, rail authorization signing, the HTTP upload) to an injected
|
|
1805
|
+
* {@link Uploader}, and posts the correlated result / status messages back to the
|
|
1806
|
+
* napplet.
|
|
1807
|
+
*
|
|
1808
|
+
* The uploader is injected (options-as-bridge) so this service has no transport
|
|
1809
|
+
* or Nostr dependency and is fully unit-testable. NAP-UPLOAD is deliberately
|
|
1810
|
+
* abstract over the backend — the runtime decides *how* it uploads (NIP-96,
|
|
1811
|
+
* Blossom, …). A concrete HTTP-backed uploader ships alongside as
|
|
1812
|
+
* {@link createHttpUploader}.
|
|
1813
|
+
*
|
|
1814
|
+
* ──────────────────────────── Responsibilities ────────────────────────────
|
|
1815
|
+
* Inbound: upload.upload, upload.status
|
|
1816
|
+
* Outbound: upload.upload.result, upload.status.result, upload.status.changed
|
|
1817
|
+
*
|
|
1818
|
+
* The service owns the `uploadId` (generated per request, scoped to the
|
|
1819
|
+
* requesting napplet), tracks the latest {@link UploadStatus} per upload for
|
|
1820
|
+
* `upload.status` queries, and cleans up on window teardown. The shell owns
|
|
1821
|
+
* consent, policy, server selection, signing, and the HTTP upload — all behind
|
|
1822
|
+
* the {@link Uploader}.
|
|
1823
|
+
*
|
|
1824
|
+
* @example
|
|
1825
|
+
* ```ts
|
|
1826
|
+
* import { createUploadService, createHttpUploader } from '@kehto/services';
|
|
1827
|
+
*
|
|
1828
|
+
* const uploader = createHttpUploader({ rails: { nip96: { servers } }, signEvent });
|
|
1829
|
+
* runtime.registerService('upload', createUploadService({ uploader }));
|
|
1830
|
+
* ```
|
|
1831
|
+
*
|
|
1832
|
+
* @packageDocumentation
|
|
1833
|
+
*/
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Storage rail. `nip96` (NIP-96 HTTP file storage) and `blossom` (Blossom blob
|
|
1837
|
+
* storage) are the first concrete backends; the open string keeps the API
|
|
1838
|
+
* stable as shells add rails (torrents, usenet, …).
|
|
1839
|
+
*/
|
|
1840
|
+
type UploadRail = 'nip96' | 'blossom' | (string & {});
|
|
1841
|
+
/** Lifecycle state of an upload. */
|
|
1842
|
+
type UploadState = 'pending' | 'uploading' | 'complete' | 'failed' | 'cancelled';
|
|
1843
|
+
/** Pixel dimensions of an uploaded image/video. */
|
|
1844
|
+
interface UploadDimensions {
|
|
1845
|
+
width: number;
|
|
1846
|
+
height: number;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* A napplet's upload request. `data` crosses the postMessage boundary by
|
|
1850
|
+
* structured clone — shells never require base64 encoding.
|
|
1851
|
+
*/
|
|
1852
|
+
interface UploadRequest {
|
|
1853
|
+
/** Storage rail; omit to let the shell pick a configured default. */
|
|
1854
|
+
rail?: UploadRail;
|
|
1855
|
+
/** The bytes to upload. */
|
|
1856
|
+
data: ArrayBuffer | Blob;
|
|
1857
|
+
/** MIME type; inferred from `data` when omitted. */
|
|
1858
|
+
mimeType?: string;
|
|
1859
|
+
/** Suggested filename. */
|
|
1860
|
+
filename?: string;
|
|
1861
|
+
/** Alt text / description for the file event. */
|
|
1862
|
+
caption?: string;
|
|
1863
|
+
/** Request the server not re-encode the file (NIP-96 `no_transform`). */
|
|
1864
|
+
noTransform?: boolean;
|
|
1865
|
+
/** Rail-specific or shell-specific extra metadata. */
|
|
1866
|
+
metadata?: Record<string, unknown>;
|
|
1867
|
+
}
|
|
1868
|
+
/** A single Nostr tag (NIP-94 / imeta entries are arrays of strings). */
|
|
1869
|
+
type NostrTag = string[];
|
|
1870
|
+
/** The result of an upload. */
|
|
1871
|
+
interface UploadResult {
|
|
1872
|
+
/** Whether the upload succeeded (or is progressing) vs failed/cancelled. */
|
|
1873
|
+
ok: boolean;
|
|
1874
|
+
/** Shell-generated id, scoped to the requesting napplet. */
|
|
1875
|
+
uploadId: string;
|
|
1876
|
+
/** Current lifecycle state. */
|
|
1877
|
+
status: UploadState;
|
|
1878
|
+
/** The rail the shell used. */
|
|
1879
|
+
rail: UploadRail;
|
|
1880
|
+
/** Primary download URL. */
|
|
1881
|
+
url?: string;
|
|
1882
|
+
/** Mirrors / alternative server URLs. */
|
|
1883
|
+
fallbackUrls?: string[];
|
|
1884
|
+
/** Hash of the stored blob (NIP-94 `x`). */
|
|
1885
|
+
sha256?: string;
|
|
1886
|
+
/** Hash before server transforms (NIP-94 `ox`). */
|
|
1887
|
+
originalSha256?: string;
|
|
1888
|
+
/** Size in bytes. */
|
|
1889
|
+
size?: number;
|
|
1890
|
+
/** Stored MIME type. */
|
|
1891
|
+
mimeType?: string;
|
|
1892
|
+
/** Image/video dimensions when known. */
|
|
1893
|
+
dimensions?: UploadDimensions;
|
|
1894
|
+
/** Blurhash placeholder when known. */
|
|
1895
|
+
blurhash?: string;
|
|
1896
|
+
/** Ready-to-attach NIP-94 / imeta tags. */
|
|
1897
|
+
nip94?: NostrTag[];
|
|
1898
|
+
/** Error reason when the upload failed or was cancelled. */
|
|
1899
|
+
error?: string;
|
|
1900
|
+
}
|
|
1901
|
+
/** A status snapshot for an upload, including progress counters. */
|
|
1902
|
+
interface UploadStatus extends UploadResult {
|
|
1903
|
+
/** Bytes sent so far (while uploading). */
|
|
1904
|
+
bytesSent?: number;
|
|
1905
|
+
/** Total bytes to send. */
|
|
1906
|
+
bytesTotal?: number;
|
|
1907
|
+
/** Unix ms timestamp of this status. */
|
|
1908
|
+
updatedAt: number;
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Context handed to an {@link Uploader} for a single upload. Carries the
|
|
1912
|
+
* service-owned `uploadId` and a sink for streaming progress / state changes.
|
|
1913
|
+
*/
|
|
1914
|
+
interface UploaderContext {
|
|
1915
|
+
/** The service-generated upload id (authoritative; scoped to the napplet). */
|
|
1916
|
+
uploadId: string;
|
|
1917
|
+
/** The napplet window that requested the upload. */
|
|
1918
|
+
windowId: string;
|
|
1919
|
+
/**
|
|
1920
|
+
* Push a status update (progress, or a transition to complete/failed). The
|
|
1921
|
+
* service stamps `uploadId` and `updatedAt` before forwarding to the napplet
|
|
1922
|
+
* as `upload.status.changed`, and records it as the latest tracked status.
|
|
1923
|
+
*/
|
|
1924
|
+
onStatus(status: UploadStatus): void;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Abstract upload backend. Implementors own server selection, rail
|
|
1928
|
+
* authorization signing (NIP-98 for NIP-96, kind 24242 for Blossom), the HTTP
|
|
1929
|
+
* upload, and integrity-hash reporting. The service translates wire envelopes
|
|
1930
|
+
* into these calls and back. A concrete reference implementation ships as
|
|
1931
|
+
* {@link createHttpUploader}.
|
|
1932
|
+
*/
|
|
1933
|
+
interface Uploader {
|
|
1934
|
+
/** Upload `request.data`, streaming progress through `ctx.onStatus`. */
|
|
1935
|
+
upload(request: UploadRequest, ctx: UploaderContext): Promise<UploadResult>;
|
|
1936
|
+
/** Optional: resolve the latest status for an upload the service is not tracking. */
|
|
1937
|
+
status?(uploadId: string): Promise<UploadStatus | undefined>;
|
|
1938
|
+
/** Optional: abort an in-flight upload (called on window teardown). */
|
|
1939
|
+
cancel?(uploadId: string): void;
|
|
1940
|
+
}
|
|
1941
|
+
/** Options for {@link createUploadService}. */
|
|
1942
|
+
interface UploadServiceOptions {
|
|
1943
|
+
/** The upload backend the shell uses. Required. */
|
|
1944
|
+
uploader: Uploader;
|
|
1945
|
+
/** Generate an upload id; defaults to `crypto.randomUUID()`. */
|
|
1946
|
+
generateId?: () => string;
|
|
1947
|
+
/** Current time in unix ms; defaults to `Date.now()`. */
|
|
1948
|
+
now?: () => number;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Create the NAP-UPLOAD service handler.
|
|
1952
|
+
*
|
|
1953
|
+
* @param options - Must provide an {@link Uploader}.
|
|
1954
|
+
* @returns A `ServiceHandler` ready for `runtime.registerService('upload', handler)`.
|
|
1955
|
+
* @throws If `options.uploader` is missing.
|
|
1956
|
+
*/
|
|
1957
|
+
declare function createUploadService(options: UploadServiceOptions): ServiceHandler;
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* http-uploader.ts — NAP-UPLOAD concrete HTTP-backed {@link Uploader}.
|
|
1961
|
+
*
|
|
1962
|
+
* The reference upload backend for {@link createUploadService}. Implements two
|
|
1963
|
+
* storage rails over HTTP:
|
|
1964
|
+
*
|
|
1965
|
+
* - **NIP-96** — signs a NIP-98 (kind 27235) HTTP-auth event, POSTs the file as
|
|
1966
|
+
* `multipart/form-data`, and maps the returned NIP-94 event tags into an
|
|
1967
|
+
* {@link UploadResult}.
|
|
1968
|
+
* - **Blossom** — signs a kind 24242 authorization event, PUTs the raw bytes to
|
|
1969
|
+
* `<server>/upload`, and maps the returned blob descriptor.
|
|
1970
|
+
*
|
|
1971
|
+
* Signing (`signEvent`) and transport (`fetch`) are injected so the uploader
|
|
1972
|
+
* carries no Nostr or network dependency and is fully unit-testable. The shell
|
|
1973
|
+
* holds the signing key and never exposes it to napplets — the uploader only
|
|
1974
|
+
* receives a signing callback. Server URLs are shell configuration, not napplet
|
|
1975
|
+
* input: a napplet may *hint* a rail, but never a server.
|
|
1976
|
+
*
|
|
1977
|
+
* The configured server URL is used directly as the upload endpoint (the
|
|
1978
|
+
* NIP-96 `api_url` / Blossom base). Hosts that need `.well-known` discovery can
|
|
1979
|
+
* resolve it before constructing the uploader.
|
|
1980
|
+
*
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```ts
|
|
1983
|
+
* const uploader = createHttpUploader({
|
|
1984
|
+
* rails: { nip96: { servers: ['https://nostr.build/api/v2/nip96/upload'] } },
|
|
1985
|
+
* signEvent: (tmpl) => signer.signEvent(tmpl),
|
|
1986
|
+
* });
|
|
1987
|
+
* runtime.registerService('upload', createUploadService({ uploader }));
|
|
1988
|
+
* ```
|
|
1989
|
+
*
|
|
1990
|
+
* @packageDocumentation
|
|
1991
|
+
*/
|
|
1992
|
+
|
|
1993
|
+
/** Per-rail server configuration. The first server is the primary endpoint. */
|
|
1994
|
+
interface RailServerConfig {
|
|
1995
|
+
/** Ordered server endpoint URLs; index 0 is primary. */
|
|
1996
|
+
servers: string[];
|
|
1997
|
+
}
|
|
1998
|
+
/** Storage rails this uploader can serve. */
|
|
1999
|
+
interface HttpUploaderRails {
|
|
2000
|
+
/** NIP-96 HTTP file storage. */
|
|
2001
|
+
nip96?: RailServerConfig;
|
|
2002
|
+
/** Blossom blob storage. */
|
|
2003
|
+
blossom?: RailServerConfig;
|
|
2004
|
+
}
|
|
2005
|
+
/** Signs an event template on the user's behalf (shell holds the key). */
|
|
2006
|
+
type SignEvent = (template: EventTemplate) => Promise<NostrEvent>;
|
|
2007
|
+
/** Options for {@link createHttpUploader}. */
|
|
2008
|
+
interface HttpUploaderOptions {
|
|
2009
|
+
/** Configured rails + their servers. */
|
|
2010
|
+
rails: HttpUploaderRails;
|
|
2011
|
+
/** Rail to use when a request omits one; defaults to the first configured rail. */
|
|
2012
|
+
defaultRail?: UploadRail;
|
|
2013
|
+
/** Signs NIP-98 / Blossom auth events. Required. */
|
|
2014
|
+
signEvent: SignEvent;
|
|
2015
|
+
/** Fetch implementation; defaults to the global `fetch`. */
|
|
2016
|
+
fetch?: typeof fetch;
|
|
2017
|
+
/** Hex SHA-256 of the payload bytes; defaults to Web Crypto. */
|
|
2018
|
+
digestSha256?: (bytes: Uint8Array) => Promise<string>;
|
|
2019
|
+
/** Unix *seconds* clock for event timestamps; defaults to `Date.now()/1000`. */
|
|
2020
|
+
now?: () => number;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Create the reference HTTP {@link Uploader} (NIP-96 + Blossom rails).
|
|
2024
|
+
*
|
|
2025
|
+
* @param options - Rails, server config, and the injected `signEvent`.
|
|
2026
|
+
* @returns An {@link Uploader} for `createUploadService({ uploader })`.
|
|
2027
|
+
* @throws If `options.signEvent` is missing.
|
|
2028
|
+
*/
|
|
2029
|
+
declare function createHttpUploader(options: HttpUploaderOptions): Uploader;
|
|
2030
|
+
|
|
2031
|
+
export { type AudioServiceOptions, type AudioSource, type CacheServiceOptions, type ConfigSchemaValidation, type ConfigService, type ConfigServiceOptions, type CoordinatedRelayOptions, type GiftWrapDecryptResult, type HostCacheBridge, type HostDecryptBridge, type HostKeyEvent, type HostKeysBridge, type HostMediaBridge, type HttpUploaderOptions, type HttpUploaderRails, type IdentityDecryptErrorCode, type IdentityDecryptErrorMessage, type IdentityDecryptMessage, type IdentityDecryptResultMessage, type IdentityServiceOptions, type KeysServiceOptions, type MediaMetadataLike, type MediaPlaybackOwner, type MediaServiceOptions, type MediaSessionCreateOptions, type MediaSessionTarget, type MediaSourceRef, type NostrTag, type Notification, type NotificationServiceOptions, type NotifyServiceOptions, type OutboxPublishOptions, type OutboxPublishResult, type OutboxQueryOptions, type OutboxRelayPlan, type OutboxRelayPool, type OutboxResult, type OutboxRouter, type OutboxRouterSubscription, type OutboxServiceOptions, type OutboxStrategy, type OutboxSubscribeOptions, type OutboxSubscriptionSink, type OutboxTarget, type RailServerConfig, type RelayListEntry, type RelayPoolOutboxRouterOptions, type RelayPoolServiceOptions, type ResourceService, type ResourceServiceOptions, type Rumor, type SignEvent, type ThemeService, type ThemeServiceOptions, type UploadDimensions, type UploadRail, type UploadRequest, type UploadResult, type UploadServiceOptions, type UploadState, type UploadStatus, type Uploader, type UploaderContext, type VerifyEvent, createAudioService, createBrowserMediaBridge, createCacheService, createConfigService, createCoordinatedRelay, createHttpUploader, createIdentityService, createKeysService, createMediaService, createNotificationService, createNotifyService, createOutboxService, createRelayPoolOutboxRouter, createRelayPoolService, createResourceService, createThemeService, createUploadService };
|