@kehto/services 0.2.0 → 0.6.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.
@@ -0,0 +1,286 @@
1
+ // src/cvm-nostr-transport.ts
2
+ import { SimplePool } from "nostr-tools/pool";
3
+ import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools/pure";
4
+ import * as nip44 from "nostr-tools/nip44";
5
+ var KIND_CVM = 25910;
6
+ var KIND_GIFT_WRAP_EPHEMERAL = 21059;
7
+ var KIND_GIFT_WRAP_REGULAR = 1059;
8
+ var KIND_ANNOUNCE_SERVER = 11316;
9
+ var KIND_ANNOUNCE_TOOLS = 11317;
10
+ var DEFAULT_TIMEOUT_MS = 3e4;
11
+ var DEFAULT_DISCOVER_TIMEOUT_MS = 6e3;
12
+ var MCP_PROTOCOL_VERSION = "2025-11-25";
13
+ var SEEN_WRAP_LIMIT = 512;
14
+ var correlationCounter = 0;
15
+ function nextCorrelationId() {
16
+ correlationCounter += 1;
17
+ return `cvm-${correlationCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;
18
+ }
19
+ function randomizedPastTimestamp() {
20
+ const jitter = Math.floor(Math.random() * 172800);
21
+ return Math.floor(Date.now() / 1e3) - jitter;
22
+ }
23
+ function tagValue(tags, name) {
24
+ return tags.find((tag) => tag[0] === name)?.[1];
25
+ }
26
+ function simplePoolAdapter(sp) {
27
+ return {
28
+ subscribe(relays, filter, params) {
29
+ return sp.subscribe(relays, filter, {
30
+ onevent: params.onevent,
31
+ oneose: params.oneose
32
+ });
33
+ },
34
+ publish(relays, event) {
35
+ return sp.publish(relays, event);
36
+ }
37
+ };
38
+ }
39
+ function createNostrCvmTransport(options = {}) {
40
+ const defaultRelays = options.defaultRelays ?? [];
41
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
42
+ const encrypt2 = options.encrypt ?? true;
43
+ const wrapKind = options.ephemeralWrap === false ? KIND_GIFT_WRAP_REGULAR : KIND_GIFT_WRAP_EPHEMERAL;
44
+ const pool = options.pool ?? simplePoolAdapter(new SimplePool());
45
+ const clientSecretKey = options.clientSecretKey ?? generateSecretKey();
46
+ const clientPubkey = getPublicKey(clientSecretKey);
47
+ const clientInfo = options.clientInfo ?? { name: "kehto-cvm", version: "1.0.0" };
48
+ const sessions = /* @__PURE__ */ new Map();
49
+ const pending = /* @__PURE__ */ new Map();
50
+ const eventHandlers = /* @__PURE__ */ new Set();
51
+ const relayRefcount = /* @__PURE__ */ new Map();
52
+ const seenWraps = /* @__PURE__ */ new Set();
53
+ let inbound = null;
54
+ let subscribedRelays = "";
55
+ function resolveRelays(server) {
56
+ const relays = server.relays && server.relays.length > 0 ? server.relays : defaultRelays;
57
+ if (relays.length === 0) throw new Error("server not found");
58
+ return [...new Set(relays)];
59
+ }
60
+ function holdRelays(relays) {
61
+ for (const url of relays) relayRefcount.set(url, (relayRefcount.get(url) ?? 0) + 1);
62
+ refreshSubscription();
63
+ }
64
+ function releaseRelays(relays) {
65
+ for (const url of relays) {
66
+ const count = (relayRefcount.get(url) ?? 0) - 1;
67
+ if (count <= 0) relayRefcount.delete(url);
68
+ else relayRefcount.set(url, count);
69
+ }
70
+ refreshSubscription();
71
+ }
72
+ function refreshSubscription() {
73
+ const relays = [...relayRefcount.keys()].sort();
74
+ const key = relays.join(",");
75
+ if (key === subscribedRelays) return;
76
+ inbound?.close();
77
+ subscribedRelays = key;
78
+ inbound = relays.length === 0 ? null : pool.subscribe(
79
+ relays,
80
+ { kinds: encrypt2 ? [KIND_GIFT_WRAP_REGULAR, KIND_GIFT_WRAP_EPHEMERAL] : [KIND_CVM], ["#p"]: [clientPubkey] },
81
+ { onevent: handleInbound }
82
+ );
83
+ }
84
+ function rememberWrap(id) {
85
+ if (seenWraps.has(id)) return false;
86
+ seenWraps.add(id);
87
+ if (seenWraps.size > SEEN_WRAP_LIMIT) {
88
+ const oldest = seenWraps.values().next().value;
89
+ if (oldest !== void 0) seenWraps.delete(oldest);
90
+ }
91
+ return true;
92
+ }
93
+ function handleInbound(event) {
94
+ if (!rememberWrap(event.id)) return;
95
+ let serverPubkey;
96
+ let mcp;
97
+ try {
98
+ if (encrypt2) {
99
+ const conversationKey = nip44.getConversationKey(clientSecretKey, event.pubkey);
100
+ const inner = JSON.parse(nip44.decrypt(event.content, conversationKey));
101
+ serverPubkey = inner.pubkey;
102
+ mcp = JSON.parse(inner.content);
103
+ } else {
104
+ serverPubkey = event.pubkey;
105
+ mcp = JSON.parse(event.content);
106
+ }
107
+ } catch {
108
+ return;
109
+ }
110
+ const id = mcp.id;
111
+ if (id != null && pending.has(String(id))) {
112
+ const entry = pending.get(String(id));
113
+ pending.delete(String(id));
114
+ clearTimeout(entry.timer);
115
+ entry.resolve({ ...mcp, id: entry.originalId });
116
+ return;
117
+ }
118
+ if (mcp.method !== void 0 && sessions.has(serverPubkey)) {
119
+ const server = { pubkey: serverPubkey };
120
+ for (const handler of eventHandlers) handler(server, mcp);
121
+ }
122
+ }
123
+ function publishMcp(server, relays, message) {
124
+ const inner = finalizeEvent(
125
+ { kind: KIND_CVM, created_at: Math.floor(Date.now() / 1e3), tags: [["p", server.pubkey]], content: JSON.stringify(message) },
126
+ clientSecretKey
127
+ );
128
+ if (!encrypt2) {
129
+ pool.publish(relays, inner);
130
+ return;
131
+ }
132
+ const wrapSecretKey = generateSecretKey();
133
+ const conversationKey = nip44.getConversationKey(wrapSecretKey, server.pubkey);
134
+ const createdAt = wrapKind === KIND_GIFT_WRAP_EPHEMERAL ? Math.floor(Date.now() / 1e3) : randomizedPastTimestamp();
135
+ const wrap = finalizeEvent(
136
+ {
137
+ kind: wrapKind,
138
+ created_at: createdAt,
139
+ tags: [["p", server.pubkey]],
140
+ content: nip44.encrypt(JSON.stringify(inner), conversationKey)
141
+ },
142
+ wrapSecretKey
143
+ );
144
+ pool.publish(relays, wrap);
145
+ }
146
+ function sendCorrelated(server, relays, message, timeout) {
147
+ const correlationId = nextCorrelationId();
148
+ const originalId = message.id;
149
+ const outgoing = { ...message, id: correlationId };
150
+ return new Promise((resolve, reject) => {
151
+ const timer = setTimeout(() => {
152
+ pending.delete(correlationId);
153
+ reject(new Error("relay timeout"));
154
+ }, timeout);
155
+ pending.set(correlationId, { resolve, reject, timer, originalId });
156
+ try {
157
+ publishMcp(server, relays, outgoing);
158
+ } catch (err) {
159
+ pending.delete(correlationId);
160
+ clearTimeout(timer);
161
+ reject(err instanceof Error ? err : new Error("publish failed"));
162
+ }
163
+ });
164
+ }
165
+ function getSession(server) {
166
+ let session = sessions.get(server.pubkey);
167
+ if (!session) {
168
+ const relays = resolveRelays(server);
169
+ session = { relays, initialized: false, initializing: null };
170
+ sessions.set(server.pubkey, session);
171
+ holdRelays(relays);
172
+ }
173
+ return session;
174
+ }
175
+ async function ensureInitialized(server, session, timeout) {
176
+ if (session.initialized) return;
177
+ if (session.initializing) return session.initializing;
178
+ session.initializing = (async () => {
179
+ try {
180
+ await sendCorrelated(
181
+ server,
182
+ session.relays,
183
+ {
184
+ jsonrpc: "2.0",
185
+ id: "init",
186
+ method: "initialize",
187
+ params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo }
188
+ },
189
+ timeout
190
+ );
191
+ publishMcp(server, session.relays, { jsonrpc: "2.0", method: "notifications/initialized" });
192
+ session.initialized = true;
193
+ } catch {
194
+ throw new Error("initialization failed");
195
+ } finally {
196
+ session.initializing = null;
197
+ }
198
+ })();
199
+ return session.initializing;
200
+ }
201
+ return {
202
+ async discover(query) {
203
+ const relays = query?.relays && query.relays.length > 0 ? query.relays : defaultRelays;
204
+ if (relays.length === 0) return [];
205
+ const announces = /* @__PURE__ */ new Map();
206
+ const toolLists = /* @__PURE__ */ new Map();
207
+ await new Promise((resolve) => {
208
+ const sub = pool.subscribe(
209
+ relays,
210
+ { kinds: [KIND_ANNOUNCE_SERVER, KIND_ANNOUNCE_TOOLS], limit: query?.limit ? query.limit * 4 : 100 },
211
+ {
212
+ onevent(event) {
213
+ if (event.kind === KIND_ANNOUNCE_SERVER) announces.set(event.pubkey, event);
214
+ else if (event.kind === KIND_ANNOUNCE_TOOLS) toolLists.set(event.pubkey, event);
215
+ },
216
+ oneose() {
217
+ resolve();
218
+ }
219
+ }
220
+ );
221
+ setTimeout(() => {
222
+ sub.close();
223
+ resolve();
224
+ }, DEFAULT_DISCOVER_TIMEOUT_MS);
225
+ });
226
+ const servers = [];
227
+ for (const [pubkey, event] of announces) {
228
+ const name = tagValue(event.tags, "name");
229
+ const description = tagValue(event.tags, "about");
230
+ const server = {
231
+ pubkey,
232
+ relays: [...relays],
233
+ ...name ? { name } : {},
234
+ ...description ? { description } : {},
235
+ paymentRequired: false
236
+ };
237
+ const tools = toolLists.get(pubkey);
238
+ if (tools) {
239
+ const names = tools.tags.filter((tag) => tag[0] === "i" && typeof tag[2] === "string").map((tag) => tag[2]);
240
+ if (names.length > 0) server.capabilities = names;
241
+ }
242
+ servers.push(server);
243
+ }
244
+ const search = query?.search?.toLowerCase();
245
+ const filtered = search ? servers.filter((s) => `${s.name ?? ""} ${s.description ?? ""}`.toLowerCase().includes(search)) : servers;
246
+ return query?.limit ? filtered.slice(0, query.limit) : filtered;
247
+ },
248
+ async request(server, message, requestOptions) {
249
+ const session = getSession(server);
250
+ const timeout = requestOptions?.timeoutMs ?? timeoutMs;
251
+ if (requestOptions?.initialize) await ensureInitialized(server, session, timeout);
252
+ return sendCorrelated(server, session.relays, message, timeout);
253
+ },
254
+ async close(server) {
255
+ const session = sessions.get(server.pubkey);
256
+ if (!session) return;
257
+ sessions.delete(server.pubkey);
258
+ releaseRelays(session.relays);
259
+ },
260
+ onEvent(handler) {
261
+ eventHandlers.add(handler);
262
+ return {
263
+ close() {
264
+ eventHandlers.delete(handler);
265
+ }
266
+ };
267
+ },
268
+ dispose() {
269
+ inbound?.close();
270
+ inbound = null;
271
+ for (const entry of pending.values()) {
272
+ clearTimeout(entry.timer);
273
+ entry.reject(new Error("transport disposed"));
274
+ }
275
+ pending.clear();
276
+ sessions.clear();
277
+ relayRefcount.clear();
278
+ eventHandlers.clear();
279
+ subscribedRelays = "";
280
+ }
281
+ };
282
+ }
283
+ export {
284
+ createNostrCvmTransport
285
+ };
286
+ //# sourceMappingURL=cvm-nostr-transport.js.map
@@ -0,0 +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. */\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":";AA6BA,SAAS,kBAAkB;AAC3B,SAAS,eAAe,mBAAmB,oBAAoB;AAC/D,YAAY,WAAW;AAavB,IAAM,WAAW;AAEjB,IAAM,2BAA2B;AACjC,IAAM,yBAAyB;AAE/B,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB;AAC3B,IAAM,8BAA8B;AACpC,IAAM,uBAAuB;AAC7B,IAAM,kBAAkB;AA0ExB,IAAI,qBAAqB;AACzB,SAAS,oBAA4B;AACnC,wBAAsB;AACtB,SAAO,OAAO,kBAAkB,IAAI,aAAa,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;AACnF;AAEA,SAAS,0BAAkC;AAEzC,QAAM,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,MAAO;AACjD,SAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AACzC;AAEA,SAAS,SAAS,MAAkB,MAAkC;AACpE,SAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;AAChD;AAGA,SAAS,kBAAkB,IAA8B;AACvD,SAAO;AAAA,IACL,UAAU,QAAQ,QAAQ,QAAQ;AAChC,aAAO,GAAG,UAAU,QAAQ,QAA4B;AAAA,QACtD,SAAS,OAAO;AAAA,QAChB,QAAQ,OAAO;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,IACA,QAAQ,QAAQ,OAAO;AACrB,aAAO,GAAG,QAAQ,QAAQ,KAAwB;AAAA,IACpD;AAAA,EACF;AACF;AASO,SAAS,wBACd,UAAoC,CAAC,GACD;AACpC,QAAM,gBAAgB,QAAQ,iBAAiB,CAAC;AAChD,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAMA,WAAU,QAAQ,WAAW;AACnC,QAAM,WAAW,QAAQ,kBAAkB,QAAQ,yBAAyB;AAC5E,QAAM,OAAqB,QAAQ,QAAQ,kBAAkB,IAAI,WAAW,CAAC;AAC7E,QAAM,kBAAkB,QAAQ,mBAAmB,kBAAkB;AACrE,QAAM,eAAe,aAAa,eAAe;AACjD,QAAM,aAAa,QAAQ,cAAc,EAAE,MAAM,aAAa,SAAS,QAAQ;AAE/E,QAAM,WAAW,oBAAI,IAA2B;AAChD,QAAM,UAAU,oBAAI,IAA4B;AAChD,QAAM,gBAAgB,oBAAI,IAAkB;AAC5C,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,QAAM,YAAY,oBAAI,IAAY;AAClC,MAAI,UAA+B;AACnC,MAAI,mBAAmB;AAEvB,WAAS,cAAc,QAAgC;AACrD,UAAM,SAAS,OAAO,UAAU,OAAO,OAAO,SAAS,IAAI,OAAO,SAAS;AAC3E,QAAI,OAAO,WAAW,EAAG,OAAM,IAAI,MAAM,kBAAkB;AAC3D,WAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAAA,EAC5B;AAEA,WAAS,WAAW,QAAwB;AAC1C,eAAW,OAAO,OAAQ,eAAc,IAAI,MAAM,cAAc,IAAI,GAAG,KAAK,KAAK,CAAC;AAClF,wBAAoB;AAAA,EACtB;AAEA,WAAS,cAAc,QAAwB;AAC7C,eAAW,OAAO,QAAQ;AACxB,YAAM,SAAS,cAAc,IAAI,GAAG,KAAK,KAAK;AAC9C,UAAI,SAAS,EAAG,eAAc,OAAO,GAAG;AAAA,UACnC,eAAc,IAAI,KAAK,KAAK;AAAA,IACnC;AACA,wBAAoB;AAAA,EACtB;AAEA,WAAS,sBAA4B;AACnC,UAAM,SAAS,CAAC,GAAG,cAAc,KAAK,CAAC,EAAE,KAAK;AAC9C,UAAM,MAAM,OAAO,KAAK,GAAG;AAC3B,QAAI,QAAQ,iBAAkB;AAC9B,aAAS,MAAM;AACf,uBAAmB;AACnB,cAAU,OAAO,WAAW,IACxB,OACA,KAAK;AAAA,MACH;AAAA,MACA,EAAE,OAAOA,WAAU,CAAC,wBAAwB,wBAAwB,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,GAAG,CAAC,YAAY,EAAE;AAAA,MAC3G,EAAE,SAAS,cAAc;AAAA,IAC3B;AAAA,EACN;AAEA,WAAS,aAAa,IAAqB;AACzC,QAAI,UAAU,IAAI,EAAE,EAAG,QAAO;AAC9B,cAAU,IAAI,EAAE;AAChB,QAAI,UAAU,OAAO,iBAAiB;AACpC,YAAM,SAAS,UAAU,OAAO,EAAE,KAAK,EAAE;AACzC,UAAI,WAAW,OAAW,WAAU,OAAO,MAAM;AAAA,IACnD;AACA,WAAO;AAAA,EACT;AAEA,WAAS,cAAc,OAA6B;AAClD,QAAI,CAAC,aAAa,MAAM,EAAE,EAAG;AAC7B,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,UAAIA,UAAS;AACX,cAAM,kBAAwB,yBAAmB,iBAAiB,MAAM,MAAM;AAC9E,cAAM,QAAQ,KAAK,MAAY,cAAQ,MAAM,SAAS,eAAe,CAAC;AACtE,uBAAe,MAAM;AACrB,cAAM,KAAK,MAAM,MAAM,OAAO;AAAA,MAChC,OAAO;AACL,uBAAe,MAAM;AACrB,cAAM,KAAK,MAAM,MAAM,OAAO;AAAA,MAChC;AAAA,IACF,QAAQ;AACN;AAAA,IACF;AAEA,UAAM,KAAK,IAAI;AACf,QAAI,MAAM,QAAQ,QAAQ,IAAI,OAAO,EAAE,CAAC,GAAG;AACzC,YAAM,QAAQ,QAAQ,IAAI,OAAO,EAAE,CAAC;AACpC,cAAQ,OAAO,OAAO,EAAE,CAAC;AACzB,mBAAa,MAAM,KAAK;AACxB,YAAM,QAAQ,EAAE,GAAG,KAAK,IAAI,MAAM,WAAW,CAAC;AAC9C;AAAA,IACF;AAEA,QAAI,IAAI,WAAW,UAAa,SAAS,IAAI,YAAY,GAAG;AAC1D,YAAM,SAAuB,EAAE,QAAQ,aAAa;AACpD,iBAAW,WAAW,cAAe,SAAQ,QAAQ,GAAG;AAAA,IAC1D;AAAA,EACF;AAEA,WAAS,WAAW,QAAsB,QAAkB,SAA2B;AACrF,UAAM,QAAQ;AAAA,MACZ,EAAE,MAAM,UAAU,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAAG,MAAM,CAAC,CAAC,KAAK,OAAO,MAAM,CAAC,GAAG,SAAS,KAAK,UAAU,OAAO,EAAE;AAAA,MAC5H;AAAA,IACF;AACA,QAAI,CAACA,UAAS;AACZ,WAAK,QAAQ,QAAQ,KAAK;AAC1B;AAAA,IACF;AACA,UAAM,gBAAgB,kBAAkB;AACxC,UAAM,kBAAwB,yBAAmB,eAAe,OAAO,MAAM;AAI7E,UAAM,YACJ,aAAa,2BAA2B,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,wBAAwB;AAClG,UAAM,OAAO;AAAA,MACX;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,MAAM,CAAC,CAAC,KAAK,OAAO,MAAM,CAAC;AAAA,QAC3B,SAAe,cAAQ,KAAK,UAAU,KAAK,GAAG,eAAe;AAAA,MAC/D;AAAA,MACA;AAAA,IACF;AACA,SAAK,QAAQ,QAAQ,IAAI;AAAA,EAC3B;AAEA,WAAS,eACP,QACA,QACA,SACA,SACqB;AACrB,UAAM,gBAAgB,kBAAkB;AACxC,UAAM,aAAa,QAAQ;AAC3B,UAAM,WAAuB,EAAE,GAAG,SAAS,IAAI,cAAc;AAC7D,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AAClD,YAAM,QAAQ,WAAW,MAAM;AAC7B,gBAAQ,OAAO,aAAa;AAC5B,eAAO,IAAI,MAAM,eAAe,CAAC;AAAA,MACnC,GAAG,OAAO;AACV,cAAQ,IAAI,eAAe,EAAE,SAAS,QAAQ,OAAO,WAAW,CAAC;AACjE,UAAI;AACF,mBAAW,QAAQ,QAAQ,QAAQ;AAAA,MACrC,SAAS,KAAK;AACZ,gBAAQ,OAAO,aAAa;AAC5B,qBAAa,KAAK;AAClB,eAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,gBAAgB,CAAC;AAAA,MACjE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,WAAS,WAAW,QAAqC;AACvD,QAAI,UAAU,SAAS,IAAI,OAAO,MAAM;AACxC,QAAI,CAAC,SAAS;AACZ,YAAM,SAAS,cAAc,MAAM;AACnC,gBAAU,EAAE,QAAQ,aAAa,OAAO,cAAc,KAAK;AAC3D,eAAS,IAAI,OAAO,QAAQ,OAAO;AACnC,iBAAW,MAAM;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAEA,iBAAe,kBAAkB,QAAsB,SAAwB,SAAgC;AAC7G,QAAI,QAAQ,YAAa;AACzB,QAAI,QAAQ,aAAc,QAAO,QAAQ;AACzC,YAAQ,gBAAgB,YAAY;AAClC,UAAI;AACF,cAAM;AAAA,UACJ;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,YACE,SAAS;AAAA,YACT,IAAI;AAAA,YACJ,QAAQ;AAAA,YACR,QAAQ,EAAE,iBAAiB,sBAAsB,cAAc,CAAC,GAAG,WAAW;AAAA,UAChF;AAAA,UACA;AAAA,QACF;AAEA,mBAAW,QAAQ,QAAQ,QAAQ,EAAE,SAAS,OAAO,QAAQ,4BAA4B,CAAC;AAC1F,gBAAQ,cAAc;AAAA,MACxB,QAAQ;AACN,cAAM,IAAI,MAAM,uBAAuB;AAAA,MACzC,UAAE;AACA,gBAAQ,eAAe;AAAA,MACzB;AAAA,IACF,GAAG;AACH,WAAO,QAAQ;AAAA,EACjB;AAEA,SAAO;AAAA,IACL,MAAM,SAAS,OAAgD;AAC7D,YAAM,SAAS,OAAO,UAAU,MAAM,OAAO,SAAS,IAAI,MAAM,SAAS;AACzE,UAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,YAAM,YAAY,oBAAI,IAA4B;AAClD,YAAM,YAAY,oBAAI,IAA4B;AAClD,YAAM,IAAI,QAAc,CAAC,YAAY;AACnC,cAAM,MAAM,KAAK;AAAA,UACf;AAAA,UACA,EAAE,OAAO,CAAC,sBAAsB,mBAAmB,GAAG,OAAO,OAAO,QAAQ,MAAM,QAAQ,IAAI,IAAI;AAAA,UAClG;AAAA,YACE,QAAQ,OAAO;AACb,kBAAI,MAAM,SAAS,qBAAsB,WAAU,IAAI,MAAM,QAAQ,KAAK;AAAA,uBACjE,MAAM,SAAS,oBAAqB,WAAU,IAAI,MAAM,QAAQ,KAAK;AAAA,YAChF;AAAA,YACA,SAAS;AAAE,sBAAQ;AAAA,YAAG;AAAA,UACxB;AAAA,QACF;AACA,mBAAW,MAAM;AAAE,cAAI,MAAM;AAAG,kBAAQ;AAAA,QAAG,GAAG,2BAA2B;AAAA,MAC3E,CAAC;AAED,YAAM,UAAuB,CAAC;AAC9B,iBAAW,CAAC,QAAQ,KAAK,KAAK,WAAW;AACvC,cAAM,OAAO,SAAS,MAAM,MAAM,MAAM;AACxC,cAAM,cAAc,SAAS,MAAM,MAAM,OAAO;AAChD,cAAM,SAAoB;AAAA,UACxB;AAAA,UACA,QAAQ,CAAC,GAAG,MAAM;AAAA,UAClB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,UACvB,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,UACrC,iBAAiB;AAAA,QACnB;AACA,cAAM,QAAQ,UAAU,IAAI,MAAM;AAClC,YAAI,OAAO;AACT,gBAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,QAAQ,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;AAC1G,cAAI,MAAM,SAAS,EAAG,QAAO,eAAe;AAAA,QAC9C;AACA,gBAAQ,KAAK,MAAM;AAAA,MACrB;AAEA,YAAM,SAAS,OAAO,QAAQ,YAAY;AAC1C,YAAM,WAAW,SACb,QAAQ,OAAO,CAAC,MAAM,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,YAAY,EAAE,SAAS,MAAM,CAAC,IAC7F;AACJ,aAAO,OAAO,QAAQ,SAAS,MAAM,GAAG,MAAM,KAAK,IAAI;AAAA,IACzD;AAAA,IAEA,MAAM,QAAQ,QAAsB,SAAqB,gBAAyD;AAChH,YAAM,UAAU,WAAW,MAAM;AACjC,YAAM,UAAU,gBAAgB,aAAa;AAC7C,UAAI,gBAAgB,WAAY,OAAM,kBAAkB,QAAQ,SAAS,OAAO;AAChF,aAAO,eAAe,QAAQ,QAAQ,QAAQ,SAAS,OAAO;AAAA,IAChE;AAAA,IAEA,MAAM,MAAM,QAAqC;AAC/C,YAAM,UAAU,SAAS,IAAI,OAAO,MAAM;AAC1C,UAAI,CAAC,QAAS;AACd,eAAS,OAAO,OAAO,MAAM;AAC7B,oBAAc,QAAQ,MAAM;AAAA,IAC9B;AAAA,IAEA,QAAQ,SAA0C;AAChD,oBAAc,IAAI,OAAO;AACzB,aAAO;AAAA,QACL,QAAQ;AACN,wBAAc,OAAO,OAAO;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAAA,IAEA,UAAgB;AACd,eAAS,MAAM;AACf,gBAAU;AACV,iBAAW,SAAS,QAAQ,OAAO,GAAG;AACpC,qBAAa,MAAM,KAAK;AACxB,cAAM,OAAO,IAAI,MAAM,oBAAoB,CAAC;AAAA,MAC9C;AACA,cAAQ,MAAM;AACd,eAAS,MAAM;AACf,oBAAc,MAAM;AACpB,oBAAc,MAAM;AACpB,yBAAmB;AAAA,IACrB;AAAA,EACF;AACF;","names":["encrypt"]}
@@ -0,0 +1,207 @@
1
+ import { ServiceHandler } from '@kehto/runtime';
2
+
3
+ /**
4
+ * cvm-types.ts — NAP-CVM (ContextVM bridge) wire + MCP value types.
5
+ *
6
+ * Kehto-internal model for the NAP-CVM wire contract (upstream draft:
7
+ * napplet/nubs NAP-CVM, namespace `window.napplet.cvm`). The literal `type`
8
+ * strings and field names below MUST match the upstream `@napplet/nap/cvm`
9
+ * envelopes byte-for-byte so the shim client interoperates; the types live
10
+ * here (not imported from `@napplet/core`) per the same convention as
11
+ * NUB-RESOURCE (PROJECT.md Decision #31) to avoid a peer-dependency version
12
+ * bump.
13
+ *
14
+ * ContextVM transports MCP JSON-RPC over Nostr (kind 25910), optionally
15
+ * gift-wrap encrypted (CEP-4). The shell owns all transport, signing,
16
+ * encryption, and relay routing; the napplet only supplies a server identity
17
+ * and the MCP operation it wants.
18
+ */
19
+ /** A single MCP JSON-RPC message (request, response, or notification). */
20
+ interface McpMessage {
21
+ jsonrpc: '2.0';
22
+ id?: string | number;
23
+ method?: string;
24
+ params?: unknown;
25
+ result?: unknown;
26
+ error?: unknown;
27
+ }
28
+ /** An MCP tool descriptor, as returned by `tools/list`. */
29
+ interface McpTool {
30
+ name: string;
31
+ description?: string;
32
+ inputSchema: {
33
+ type: 'object';
34
+ properties?: Record<string, unknown>;
35
+ required?: string[];
36
+ };
37
+ }
38
+ /** A content block inside an MCP tool result. */
39
+ interface McpContentBlock {
40
+ type: string;
41
+ text?: string;
42
+ [key: string]: unknown;
43
+ }
44
+ /** The result of an MCP `tools/call`. */
45
+ interface McpToolResult {
46
+ content: McpContentBlock[];
47
+ isError?: boolean;
48
+ [key: string]: unknown;
49
+ }
50
+ /** Reference to a ContextVM server by public key, with optional relay hints. */
51
+ interface CvmServerRef {
52
+ /** Hex Nostr public key of the ContextVM server. */
53
+ pubkey: string;
54
+ /** Optional relay hints; the shell MAY use, ignore, or augment these. */
55
+ relays?: string[];
56
+ }
57
+ /** Query for `cvm.discover`. */
58
+ interface CvmDiscoverQuery {
59
+ search?: string;
60
+ kinds?: number[];
61
+ relays?: string[];
62
+ limit?: number;
63
+ }
64
+ /** A discovered ContextVM server announcement. */
65
+ interface CvmServer extends CvmServerRef {
66
+ name?: string;
67
+ description?: string;
68
+ capabilities?: string[];
69
+ paymentRequired?: boolean;
70
+ }
71
+ /** Per-request options for `cvm.request`. */
72
+ interface CvmRequestOptions {
73
+ /** Abort the request after this many milliseconds. */
74
+ timeoutMs?: number;
75
+ /** Perform an MCP `initialize` handshake before the request. */
76
+ initialize?: boolean;
77
+ /** Payment policy when a server requires value exchange (NAP-VALUE). */
78
+ payment?: 'deny' | 'prompt' | 'allow';
79
+ }
80
+ /**
81
+ * Wire envelopes — NIP-5D format `{ type: "cvm.<action>", ...payload }`. The
82
+ * shell echoes the request `id` in every `*.result`; `cvm.event` has no id.
83
+ */
84
+ interface CvmDiscoverMessage {
85
+ type: 'cvm.discover';
86
+ id: string;
87
+ query?: CvmDiscoverQuery;
88
+ }
89
+ interface CvmDiscoverResultMessage {
90
+ type: 'cvm.discover.result';
91
+ id: string;
92
+ servers: CvmServer[];
93
+ error?: string;
94
+ }
95
+ interface CvmRequestMessage {
96
+ type: 'cvm.request';
97
+ id: string;
98
+ server: CvmServerRef;
99
+ message: McpMessage;
100
+ options?: CvmRequestOptions;
101
+ }
102
+ interface CvmRequestResultMessage {
103
+ type: 'cvm.request.result';
104
+ id: string;
105
+ /** The MCP response. MCP-level errors live in `message.error`. */
106
+ message?: McpMessage;
107
+ /** Transport/shell-policy error (distinct from MCP-level errors). */
108
+ error?: string;
109
+ }
110
+ interface CvmCloseMessage {
111
+ type: 'cvm.close';
112
+ id: string;
113
+ server: CvmServerRef;
114
+ }
115
+ interface CvmCloseResultMessage {
116
+ type: 'cvm.close.result';
117
+ id: string;
118
+ error?: string;
119
+ }
120
+ interface CvmEventMessage {
121
+ type: 'cvm.event';
122
+ server: CvmServerRef;
123
+ message: McpMessage;
124
+ }
125
+ /** Napplet → shell CVM envelopes. */
126
+ type CvmInboundMessage = CvmDiscoverMessage | CvmRequestMessage | CvmCloseMessage;
127
+ /** Shell → napplet CVM envelopes. */
128
+ type CvmOutboundMessage = CvmDiscoverResultMessage | CvmRequestResultMessage | CvmCloseResultMessage | CvmEventMessage;
129
+ /**
130
+ * Documented transport/shell-policy error strings for `*.result.error`.
131
+ * MCP-level errors are returned inside `message.error` instead.
132
+ */
133
+ type CvmTransportError = 'server not found' | 'relay timeout' | 'initialization failed' | 'payment required' | 'payment denied' | 'unsupported method' | 'policy denied';
134
+
135
+ /**
136
+ * cvm-service.ts — NAP-CVM (ContextVM bridge) reference service.
137
+ *
138
+ * Shell-side handler for the NAP-CVM wire protocol. It is a pure envelope
139
+ * router: it validates `cvm.*` envelopes, delegates the actual ContextVM /
140
+ * MCP-over-Nostr work to an injected {@link CvmTransport}, and posts the
141
+ * correlated `*.result` (echoing the request `id`) back to the napplet.
142
+ *
143
+ * The transport is injected (options-as-bridge) so this service has no Nostr
144
+ * dependency and is fully unit-testable. A concrete ContextVM transport ships
145
+ * separately at `@kehto/services/cvm-nostr-transport`.
146
+ *
147
+ * ──────────────────────────── Responsibilities ────────────────────────────
148
+ * Inbound: cvm.discover, cvm.request, cvm.close
149
+ * Outbound: cvm.discover.result, cvm.request.result, cvm.close.result,
150
+ * cvm.event (server-pushed MCP notifications)
151
+ *
152
+ * MCP-level errors are returned inside `request.result.message.error`;
153
+ * transport/shell-policy failures are returned in the envelope `error` field.
154
+ *
155
+ * `cvm.event` is fanned out to every window that holds an active session with
156
+ * the originating server (a window opens a session by issuing a `cvm.request`
157
+ * and closes it via `cvm.close` or window teardown).
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * import { createCvmService } from '@kehto/services';
162
+ * import { createNostrCvmTransport } from '@kehto/services/cvm-nostr-transport';
163
+ *
164
+ * const transport = createNostrCvmTransport({ defaultRelays: ['wss://relay.contextvm.org'] });
165
+ * runtime.registerService('cvm', createCvmService({ transport }));
166
+ * ```
167
+ */
168
+
169
+ /**
170
+ * Abstract ContextVM transport. Implementors own Nostr relay access, signing,
171
+ * encryption (CEP-4 gift wrap), JSON-RPC correlation, and MCP initialization.
172
+ */
173
+ interface CvmTransport {
174
+ /** Resolve public ContextVM server announcements matching the query. */
175
+ discover(query?: CvmDiscoverQuery): Promise<CvmServer[]>;
176
+ /** Send a raw MCP message to a server and resolve with the MCP response. */
177
+ request(server: CvmServerRef, message: McpMessage, options?: CvmRequestOptions): Promise<McpMessage>;
178
+ /** Release any session state held for a server (subscriptions, init cache). */
179
+ close(server: CvmServerRef): Promise<void>;
180
+ /**
181
+ * Subscribe to server-pushed MCP messages not correlated to a single
182
+ * request (e.g. notifications). Returns a handle whose `close()` detaches.
183
+ */
184
+ onEvent(handler: (server: CvmServerRef, message: McpMessage) => void): {
185
+ close(): void;
186
+ };
187
+ }
188
+ /** Options for {@link createCvmService}. */
189
+ interface CvmServiceOptions {
190
+ /** The ContextVM transport the shell uses to reach servers. Required. */
191
+ transport: CvmTransport;
192
+ }
193
+ /** The created CVM service, exposing the handler plus a disposal hook. */
194
+ interface CvmService extends ServiceHandler {
195
+ /** Detach the transport event subscription. Idempotent. */
196
+ dispose(): void;
197
+ }
198
+ /**
199
+ * Create the NAP-CVM service handler.
200
+ *
201
+ * @param options - Must provide a {@link CvmTransport}.
202
+ * @returns A {@link CvmService} (a `ServiceHandler` with a `dispose()` hook).
203
+ * @throws If `options.transport` is missing.
204
+ */
205
+ declare function createCvmService(options: CvmServiceOptions): CvmService;
206
+
207
+ export { type CvmTransport as C, type McpContentBlock as M, type CvmCloseMessage as a, type CvmCloseResultMessage as b, type CvmDiscoverMessage as c, type CvmDiscoverQuery as d, type CvmDiscoverResultMessage as e, type CvmEventMessage as f, type CvmInboundMessage as g, type CvmOutboundMessage as h, type CvmRequestMessage as i, type CvmRequestOptions as j, type CvmRequestResultMessage as k, type CvmServer as l, type CvmServerRef as m, type CvmService as n, type CvmServiceOptions as o, type CvmTransportError as p, type McpMessage as q, type McpTool as r, type McpToolResult as s, createCvmService as t };