@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
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audio-service.ts","../src/notification-service.ts","../src/identity-service.ts","../src/relay-pool-service.ts","../src/cache-service.ts","../src/coordinated-relay.ts","../src/keys-service.ts","../src/browser-media-bridge.ts","../src/media-service.ts","../src/notify-service.ts","../src/theme-service.ts","../src/config-service.ts","../src/resource-service.ts","../src/cvm-service.ts"],"sourcesContent":["/**\n * audio-service.ts — Audio source registry as a ServiceHandler.\n *\n * Tracks which napplet windows are producing audio. Shell hosts wire this\n * into the runtime via registerService('audio', createAudioService(opts)).\n * Browser-agnostic — no DOM, no window, no postMessage.\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type { AudioSource, AudioServiceOptions } from './types.js';\n\n/** Audio service version — follows semver. */\nconst AUDIO_SERVICE_VERSION = '1.0.0';\n\n/**\n * Create an audio service handler.\n *\n * The audio service is a state registry that tracks active audio sources\n * per napplet window. Napplets announce audio state via `audio:*` topic\n * events; the service tracks sources and can relay mute commands back.\n *\n * @param options - Optional configuration (onChange callback for UI updates)\n * @returns A ServiceHandler to register with the runtime\n *\n * @example\n * ```ts\n * import { createAudioService } from '@kehto/services';\n *\n * const audio = createAudioService({\n * onChange: (sources) => {\n * // Update UI with current audio sources\n * for (const [windowId, source] of sources) {\n * console.log(`${source.title} (${source.muted ? 'muted' : 'playing'})`);\n * }\n * },\n * });\n *\n * runtime.registerService('audio', audio);\n * ```\n */\nexport function createAudioService(options?: AudioServiceOptions): ServiceHandler {\n const sources = new Map<string, AudioSource>();\n const onChange = options?.onChange;\n\n function notify(): void {\n onChange?.(new Map(sources));\n }\n\n const descriptor: ServiceDescriptor = {\n name: 'audio',\n version: AUDIO_SERVICE_VERSION,\n description: 'Audio source registry — tracks active audio sources per napplet window',\n };\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n if (message.type !== 'ifc.emit') return;\n const ifcMessage = message as NappletMessage & { topic?: unknown; payload?: unknown };\n const topic = typeof ifcMessage.topic === 'string' ? ifcMessage.topic : undefined;\n if (!topic?.startsWith('audio:')) return;\n\n const action = topic.slice(6); // 'audio:'.length === 6\n const payload = ifcMessage.payload && typeof ifcMessage.payload === 'object'\n ? ifcMessage.payload as Record<string, unknown>\n : {};\n\n switch (action) {\n case 'register': {\n const nappletClass = typeof payload.nappletClass === 'string' ? payload.nappletClass : '';\n const title = typeof payload.title === 'string' ? payload.title : '';\n sources.set(windowId, { windowId, nappletClass, title, muted: false });\n notify();\n break;\n }\n\n case 'unregister': {\n if (sources.delete(windowId)) {\n notify();\n }\n break;\n }\n\n case 'state-changed': {\n const source = sources.get(windowId);\n if (!source) return;\n if (typeof payload.title === 'string') {\n source.title = payload.title;\n }\n notify();\n break;\n }\n\n case 'mute': {\n const targetWindowId = typeof payload.windowId === 'string'\n ? payload.windowId\n : windowId;\n const muted = payload.muted === true;\n\n const source = sources.get(targetWindowId);\n if (source) {\n source.muted = muted;\n notify();\n }\n\n send({ type: 'ifc.event', topic: 'napplet:audio-muted', payload: { muted } } as NappletMessage);\n break;\n }\n\n default:\n // Unknown audio action — ignore\n break;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n if (sources.delete(windowId)) {\n notify();\n }\n },\n };\n}\n","\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type { Notification, NotificationServiceOptions } from './types.js';\n\n/** Notification service version — follows semver. */\nconst NOTIFICATION_SERVICE_VERSION = '1.0.0';\n\n/** Default maximum notifications per window. */\nconst DEFAULT_MAX_PER_WINDOW = 100;\n\n/** Counter for generating unique notification IDs. */\nlet idCounter = 0;\n\n/**\n * Generate a unique notification ID.\n */\nfunction generateId(): string {\n idCounter++;\n return `notif-${Date.now()}-${idCounter}`;\n}\n\ninterface NotificationStore {\n notifications: Map<string, Notification[]>;\n onChange?: (notifications: Notification[]) => void;\n maxPerWindow: number;\n}\n\nfunction getAllNotifications(store: NotificationStore): Notification[] {\n const all: Notification[] = [];\n for (const windowNotifs of store.notifications.values()) {\n all.push(...windowNotifs);\n }\n return all;\n}\n\nfunction notify(store: NotificationStore): void {\n store.onChange?.(getAllNotifications(store));\n}\n\nfunction getWindowNotifications(store: NotificationStore, windowId: string): Notification[] {\n let list = store.notifications.get(windowId);\n if (!list) {\n list = [];\n store.notifications.set(windowId, list);\n }\n return list;\n}\n\nfunction enforceLimit(store: NotificationStore, list: Notification[]): void {\n while (list.length > store.maxPerWindow) {\n list.shift();\n }\n}\n\nfunction findById(store: NotificationStore, id: string): [string, Notification, number] | undefined {\n for (const [windowId, list] of store.notifications) {\n const index = list.findIndex((n) => n.id === id);\n if (index !== -1) {\n return [windowId, list[index], index];\n }\n }\n return undefined;\n}\n\nfunction createNotification(\n store: NotificationStore,\n windowId: string,\n title: string,\n body: string,\n): Notification {\n const notification: Notification = {\n id: generateId(),\n windowId,\n title,\n body,\n read: false,\n createdAt: Math.floor(Date.now() / 1000),\n };\n const list = getWindowNotifications(store, windowId);\n list.push(notification);\n enforceLimit(store, list);\n notify(store);\n return notification;\n}\n\nfunction dismissNotification(store: NotificationStore, id: string): void {\n const found = findById(store, id);\n if (!found) return;\n const [foundWindowId, , index] = found;\n const list = store.notifications.get(foundWindowId);\n if (!list) return;\n list.splice(index, 1);\n if (list.length === 0) store.notifications.delete(foundWindowId);\n notify(store);\n}\n\nfunction markNotificationRead(store: NotificationStore, id: string): void {\n const found = findById(store, id);\n if (!found) return;\n const [, notification] = found;\n if (!notification.read) {\n notification.read = true;\n notify(store);\n }\n}\n\nfunction handleNotifyEnvelope(\n store: NotificationStore,\n windowId: string,\n action: string,\n msg: NappletMessage & Record<string, unknown>,\n send: (msg: NappletMessage) => void,\n): void {\n switch (action) {\n case 'create': {\n const title = typeof msg.title === 'string' ? msg.title : '';\n const body = typeof msg.body === 'string' ? msg.body : '';\n const notification = createNotification(store, windowId, title, body);\n send({ type: 'notify.created', id: notification.id } as NappletMessage);\n break;\n }\n\n case 'dismiss': {\n const notifId = typeof msg.notificationId === 'string' ? msg.notificationId : '';\n if (notifId) dismissNotification(store, notifId);\n break;\n }\n\n case 'read': {\n const notifId = typeof msg.notificationId === 'string' ? msg.notificationId : '';\n if (notifId) markNotificationRead(store, notifId);\n break;\n }\n\n case 'list': {\n const windowNotifs = store.notifications.get(windowId) ?? [];\n send({ type: 'notify.listed', notifications: windowNotifs } as NappletMessage);\n break;\n }\n\n default:\n break;\n }\n}\n\nfunction handleIfcNotification(\n store: NotificationStore,\n windowId: string,\n action: string,\n payload: Record<string, unknown>,\n send: (msg: NappletMessage) => void,\n): void {\n switch (action) {\n case 'create': {\n const title = typeof payload.title === 'string' ? payload.title : '';\n const body = typeof payload.body === 'string' ? payload.body : '';\n const notification = createNotification(store, windowId, title, body);\n send({ type: 'ifc.event', topic: 'notifications:created', payload: { id: notification.id } } as NappletMessage);\n break;\n }\n\n case 'dismiss': {\n const id = typeof payload.id === 'string' ? payload.id : '';\n if (id) dismissNotification(store, id);\n break;\n }\n\n case 'read': {\n const id = typeof payload.id === 'string' ? payload.id : '';\n if (id) markNotificationRead(store, id);\n break;\n }\n\n case 'list': {\n const windowNotifs = store.notifications.get(windowId) ?? [];\n send({ type: 'ifc.event', topic: 'notifications:listed', payload: { notifications: windowNotifs } } as NappletMessage);\n break;\n }\n\n default:\n break;\n }\n}\n\n/**\n * Create a notification service handler.\n *\n * The notification service is a state registry that tracks notifications\n * per napplet window. Napplets create and manage notifications via\n * `notifications:*` topic events; the shell host controls presentation\n * via the onChange callback.\n *\n * @param options - Optional configuration (onChange callback, maxPerWindow limit)\n * @returns A ServiceHandler to register with the runtime\n *\n * @example\n * ```ts\n * import { createNotificationService } from '@kehto/services';\n *\n * const notifications = createNotificationService({\n * onChange: (list) => {\n * const unread = list.filter(n => !n.read);\n * updateBadge(unread.length);\n * },\n * maxPerWindow: 50,\n * });\n *\n * runtime.registerService('notifications', notifications);\n * ```\n */\nexport function createNotificationService(options?: NotificationServiceOptions): ServiceHandler {\n const store: NotificationStore = {\n notifications: new Map<string, Notification[]>(),\n onChange: options?.onChange,\n maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW,\n };\n\n const descriptor: ServiceDescriptor = {\n name: 'notifications',\n version: NOTIFICATION_SERVICE_VERSION,\n description: 'Notification state registry — tracks notifications per napplet window',\n };\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const msg = message as NappletMessage & Record<string, unknown>;\n\n if (message.type.startsWith('notify.')) {\n handleNotifyEnvelope(store, windowId, message.type.slice(7), msg, send);\n return;\n }\n\n if (message.type !== 'ifc.emit') return;\n const topic = msg.topic as string | undefined;\n if (!topic?.startsWith('notifications:')) return;\n\n const payload = ((msg.payload ?? {}) as Record<string, unknown>);\n handleIfcNotification(store, windowId, topic.slice(14), payload, send);\n },\n\n onWindowDestroyed(windowId: string): void {\n if (store.notifications.delete(windowId)) {\n notify(store);\n }\n },\n };\n}\n","/**\n * identity-service.ts — NIP-5D identity nub reference service.\n *\n * MIGRATION from signer-service (v1.1 -> v1.2):\n * - signer.getPublicKey -> identity.getPublicKey (same shell state)\n * - signer.getRelays -> identity.getRelays (same shell state)\n * - signer.signEvent -> DELETED (no napplet-visible path; shell signs\n * internally inside relay.publish)\n * - signer.nip04.encrypt/decrypt -> DELETED\n * - signer.nip44.encrypt/decrypt -> DELETED (shell encrypts internally\n * inside relay.publishEncrypted)\n * - identity.decrypt -> ADDED in v1.8 as a shell-mediated decrypt request\n * with class gate + typed error union.\n *\n * See REQUIREMENTS.md DEPS-03 (Phase 15 changelog).\n *\n * Handles 10 identity.* request types from @napplet/nub/identity. getPublicKey\n * and getRelays return real values sourced from hooks.auth.getSigner(); the\n * remaining 7 (getProfile/getFollows/getList/getZaps/getMutes/getBlocked/\n * getBadges) are stub-level; decrypt delegates to a host-supplied bridge.\n * Host apps plug real backends via runtime.registerService('identity', realHandler).\n */\n\nimport type { NappletMessage, NostrEvent } from '@napplet/core';\nimport type { ServiceHandler, Signer } from '@kehto/runtime';\nimport type {\n IdentityGetPublicKeyResultMessage,\n IdentityGetRelaysResultMessage,\n IdentityGetProfileResultMessage,\n IdentityGetFollowsResultMessage,\n IdentityGetListResultMessage,\n IdentityGetZapsResultMessage,\n IdentityGetMutesResultMessage,\n IdentityGetBlockedResultMessage,\n IdentityGetBadgesResultMessage,\n RelayPermission,\n} from '@napplet/nub/identity/types';\n\n/** Identity service version — follows semver. */\nconst IDENTITY_SERVICE_VERSION = '1.0.0';\n\ntype EncryptionMode = 'nip04' | 'nip44-direct' | 'nip17';\n\nexport type IdentityDecryptErrorCode =\n | 'class-forbidden'\n | 'signer-denied'\n | 'signer-unavailable'\n | 'decrypt-failed'\n | 'malformed-wrap'\n | 'impersonation'\n | 'unsupported-encryption'\n | 'policy-denied';\n\nexport interface Rumor {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n}\n\nexport interface IdentityDecryptMessage extends NappletMessage {\n type: 'identity.decrypt';\n id: string;\n event: NostrEvent;\n}\n\nexport interface IdentityDecryptResultMessage extends NappletMessage {\n type: 'identity.decrypt.result';\n id: string;\n rumor: Rumor;\n sender: string;\n}\n\nexport interface IdentityDecryptErrorMessage extends NappletMessage {\n type: 'identity.decrypt.error';\n id: string;\n error: IdentityDecryptErrorCode;\n}\n\nconst DECRYPT_ERROR_CODES: readonly IdentityDecryptErrorCode[] = [\n 'class-forbidden',\n 'signer-denied',\n 'signer-unavailable',\n 'decrypt-failed',\n 'malformed-wrap',\n 'impersonation',\n 'unsupported-encryption',\n 'policy-denied',\n];\n\nconst DECRYPT_ERROR_CODE_SET = new Set<string>(DECRYPT_ERROR_CODES);\n\nexport interface GiftWrapDecryptResult {\n seal: NostrEvent;\n rumor: Rumor;\n}\n\nexport interface HostDecryptBridge {\n nip04Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;\n nip44Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;\n unwrapGiftWrap(wrap: NostrEvent): Promise<GiftWrapDecryptResult>;\n}\n\nexport type VerifyEvent = (event: NostrEvent) => boolean | Promise<boolean>;\n\nfunction isDecryptErrorCode(value: unknown): value is IdentityDecryptErrorCode {\n return typeof value === 'string' && DECRYPT_ERROR_CODE_SET.has(value);\n}\n\nfunction normalizeDecryptError(error: unknown): IdentityDecryptErrorCode {\n if (isDecryptErrorCode(error)) return error;\n if (typeof error === 'object' && error !== null) {\n const candidate = error as { code?: unknown; error?: unknown; message?: unknown };\n if (isDecryptErrorCode(candidate.code)) return candidate.code;\n if (isDecryptErrorCode(candidate.error)) return candidate.error;\n if (isDecryptErrorCode(candidate.message)) return candidate.message;\n }\n return 'decrypt-failed';\n}\n\nfunction sendDecryptError(\n id: string,\n error: IdentityDecryptErrorCode,\n send: (msg: NappletMessage) => void,\n): void {\n const result: IdentityDecryptErrorMessage = {\n type: 'identity.decrypt.error',\n id,\n error,\n };\n send(result);\n}\n\nfunction isStringArrayArray(value: unknown): value is string[][] {\n return Array.isArray(value) && value.every(\n (tag) => Array.isArray(tag) && tag.every((part) => typeof part === 'string'),\n );\n}\n\nfunction isNostrEvent(value: unknown): value is NostrEvent {\n const event = value as Partial<NostrEvent> | null;\n return typeof event === 'object' &&\n event !== null &&\n typeof event.id === 'string' &&\n typeof event.pubkey === 'string' &&\n typeof event.created_at === 'number' &&\n typeof event.kind === 'number' &&\n isStringArrayArray(event.tags) &&\n typeof event.content === 'string' &&\n typeof event.sig === 'string';\n}\n\nfunction isRumor(value: unknown): value is Rumor {\n const rumor = value as Partial<Rumor> | null;\n return typeof rumor === 'object' &&\n rumor !== null &&\n typeof rumor.id === 'string' &&\n typeof rumor.pubkey === 'string' &&\n typeof rumor.created_at === 'number' &&\n typeof rumor.kind === 'number' &&\n isStringArrayArray(rumor.tags) &&\n typeof rumor.content === 'string';\n}\n\nfunction isGiftWrapDecryptResult(value: unknown): value is GiftWrapDecryptResult {\n const result = value as Partial<GiftWrapDecryptResult> | null;\n return typeof result === 'object' &&\n result !== null &&\n isNostrEvent(result.seal) &&\n isRumor(result.rumor);\n}\n\nfunction firstDecodedByte(content: string): number | null {\n const trimmed = content.trim();\n if (trimmed.length === 0) return null;\n const normalized = trimmed.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n try {\n const decoded = atob(padded);\n return decoded.length > 0 ? decoded.charCodeAt(0) : null;\n } catch {\n return null;\n }\n}\n\nfunction detectEncryptionMode(event: NostrEvent): EncryptionMode | null {\n if (event.kind === 4) return 'nip04';\n if (event.kind === 1059) return 'nip17';\n if (event.kind === 14 || firstDecodedByte(event.content) === 0x02) {\n return 'nip44-direct';\n }\n return null;\n}\n\nfunction rumorFromSignedEvent(event: NostrEvent, content: string): Rumor {\n return {\n id: event.id,\n pubkey: event.pubkey,\n kind: event.kind,\n tags: event.tags,\n created_at: event.created_at,\n content,\n };\n}\n\nasync function handleDecrypt(\n id: string,\n message: IdentityDecryptMessage,\n send: (msg: NappletMessage) => void,\n options: IdentityServiceOptions,\n): Promise<void> {\n const event = (message as IdentityDecryptMessage & { event?: unknown }).event;\n if (!isNostrEvent(event)) {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n\n const verifyEvent = options.verifyEvent ?? (() => true);\n let verified: boolean;\n try {\n verified = await Promise.resolve(verifyEvent(event));\n } catch {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n if (!verified) {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n\n const mode = detectEncryptionMode(event);\n if (!mode) {\n sendDecryptError(id, 'unsupported-encryption', send);\n return;\n }\n\n const decryptor = options.getDecryptor?.() ?? null;\n if (!decryptor) {\n sendDecryptError(id, 'signer-unavailable', send);\n return;\n }\n\n try {\n if (mode === 'nip04') {\n const plaintext = await decryptor.nip04Decrypt(event.pubkey, event.content);\n const result: IdentityDecryptResultMessage = {\n type: 'identity.decrypt.result',\n id,\n rumor: rumorFromSignedEvent(event, plaintext),\n sender: event.pubkey,\n };\n send(result);\n return;\n }\n\n if (mode === 'nip44-direct') {\n const plaintext = await decryptor.nip44Decrypt(event.pubkey, event.content);\n const result: IdentityDecryptResultMessage = {\n type: 'identity.decrypt.result',\n id,\n rumor: rumorFromSignedEvent(event, plaintext),\n sender: event.pubkey,\n };\n send(result);\n return;\n }\n\n const unwrapped = await decryptor.unwrapGiftWrap(event);\n if (!isGiftWrapDecryptResult(unwrapped)) {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n if (unwrapped.seal.pubkey !== unwrapped.rumor.pubkey) {\n sendDecryptError(id, 'impersonation', send);\n return;\n }\n const result: IdentityDecryptResultMessage = {\n type: 'identity.decrypt.result',\n id,\n rumor: unwrapped.rumor,\n sender: unwrapped.seal.pubkey,\n };\n send(result);\n } catch (error) {\n sendDecryptError(id, normalizeDecryptError(error), send);\n }\n}\n\n/**\n * Options for creating the identity service.\n *\n * @example\n * ```ts\n * const identityService = createIdentityService({\n * getSigner: () => window.nostr ?? null,\n * });\n * runtime.registerService('identity', identityService);\n * ```\n */\nexport interface IdentityServiceOptions {\n /**\n * Return the NIP-07-compatible signer (or null) used to resolve\n * identity.getPublicKey / identity.getRelays. Called on every request —\n * availability can change dynamically.\n */\n getSigner: () => Signer | null;\n\n /**\n * Return the host decrypt bridge. Called only after outer event signature\n * verification and encryption-mode detection succeed. Null means decrypt is\n * unavailable while the rest of the identity service remains usable.\n */\n getDecryptor?: () => HostDecryptBridge | null;\n\n /**\n * Verify a received event before any decrypt attempt. Host shells should\n * wire this to their canonical Nostr event verifier; tests and old hosts\n * default to true for backward compatibility with the 9 read-only actions.\n */\n verifyEvent?: VerifyEvent;\n}\n\n/**\n * Create an identity service that handles NIP-5D identity.* envelope messages.\n *\n * Supports all 10 identity.* request types from @napplet/nub/identity. The two\n * read-only nostr-info queries (getPublicKey, getRelays) resolve through the\n * caller-supplied signer; the remaining 7 return default/empty payloads with\n * spec-correct envelope shapes so napplets always receive a result envelope.\n * identity.decrypt delegates to the host decrypt bridge.\n *\n * @param options - Identity service configuration (getSigner)\n * @returns A ServiceHandler ready for runtime.registerService('identity', handler)\n *\n * @example\n * ```ts\n * import { createIdentityService } from '@kehto/services';\n *\n * const identity = createIdentityService({\n * getSigner: () => mySignerAdapter,\n * });\n * runtime.registerService('identity', identity);\n * ```\n */\nexport function createIdentityService(options: IdentityServiceOptions): ServiceHandler {\n return {\n descriptor: {\n name: 'identity',\n version: IDENTITY_SERVICE_VERSION,\n description: 'NIP-5D identity NUB reference handler (9 read-only identity queries)',\n },\n\n handleMessage(\n _windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n\n function sendError(typeBase: string, error: string): void {\n send({ type: `${typeBase}.error`, id, error } as NappletMessage);\n }\n\n function sendSignerError(typeBase: string, fallback: string, err: unknown): void {\n sendError(typeBase, (err as Error)?.message ?? fallback);\n }\n\n const signer = options.getSigner();\n\n switch (message.type) {\n case 'identity.getPublicKey': {\n // Per NIP-5D spec comment \"Always succeeds\" — return empty pubkey when no signer is\n // configured rather than sending an error. The nub-identity shim's getPublicKey()\n // only handles 'identity.getPublicKey.result'; an error response hangs the Promise\n // indefinitely. Empty pubkey is the correct sentinel for \"no signer connected\".\n if (!signer) {\n const result: IdentityGetPublicKeyResultMessage = {\n type: 'identity.getPublicKey.result',\n id,\n pubkey: '',\n };\n send(result);\n return;\n }\n Promise.resolve(signer.getPublicKey?.())\n .then((pubkey) => {\n const result: IdentityGetPublicKeyResultMessage = {\n type: 'identity.getPublicKey.result',\n id,\n pubkey: (pubkey as string) ?? '',\n };\n send(result);\n })\n .catch((err: unknown) => sendSignerError('identity.getPublicKey', 'getPublicKey failed', err));\n return;\n }\n\n case 'identity.getRelays': {\n if (!signer) {\n sendError('identity.getRelays', 'no signer configured');\n return;\n }\n Promise.resolve(signer.getRelays?.() ?? {})\n .then((relays) => {\n const result: IdentityGetRelaysResultMessage = {\n type: 'identity.getRelays.result',\n id,\n relays: relays as Record<string, RelayPermission>,\n };\n send(result);\n })\n .catch((err: unknown) => sendSignerError('identity.getRelays', 'getRelays failed', err));\n return;\n }\n\n case 'identity.getProfile': {\n const result: IdentityGetProfileResultMessage = {\n type: 'identity.getProfile.result',\n id,\n profile: null,\n };\n send(result);\n return;\n }\n\n case 'identity.getFollows': {\n const result: IdentityGetFollowsResultMessage = {\n type: 'identity.getFollows.result',\n id,\n pubkeys: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getList': {\n const result: IdentityGetListResultMessage = {\n type: 'identity.getList.result',\n id,\n entries: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getZaps': {\n const result: IdentityGetZapsResultMessage = {\n type: 'identity.getZaps.result',\n id,\n zaps: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getMutes': {\n const result: IdentityGetMutesResultMessage = {\n type: 'identity.getMutes.result',\n id,\n pubkeys: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getBlocked': {\n const result: IdentityGetBlockedResultMessage = {\n type: 'identity.getBlocked.result',\n id,\n pubkeys: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getBadges': {\n const result: IdentityGetBadgesResultMessage = {\n type: 'identity.getBadges.result',\n id,\n badges: [],\n };\n send(result);\n return;\n }\n\n case 'identity.decrypt': {\n void handleDecrypt(id, message as IdentityDecryptMessage, send, options);\n return;\n }\n\n default:\n sendError(message.type, `Unknown identity method: ${message.type}`);\n }\n },\n\n // Identity service has no per-window state to clean up.\n onWindowDestroyed(_windowId: string): void {\n /* no-op */\n },\n };\n}\n","/**\n * relay-pool-service.ts — Relay pool as a ServiceHandler.\n *\n * Wraps an existing relay pool implementation (subscribe, publish,\n * selectRelayTier, isAvailable) as a ServiceHandler that receives\n * relay NUB envelope messages and manages subscription lifecycle.\n *\n * Handles: relay.subscribe, relay.close, relay.publish, relay.publishEncrypted.\n *\n * Note on `relay.publishEncrypted`: the canonical napplet→shell path routes\n * through @kehto/runtime handleRelayMessage, which performs shell-internal\n * NIP-44/NIP-04 encryption via the signer, then synthesizes a relay.publish\n * envelope handed to this service. The publishEncrypted branch here is a\n * fallback for alternate wirings; by the time the service sees the\n * envelope, content MUST already be ciphertext (the service never encrypts\n * or decrypts).\n */\n\nimport type { NostrEvent, NostrFilter, NappletMessage } from '@napplet/core';\nimport type { ServiceHandler } from '@kehto/runtime';\n\n// Timer globals available in all JS runtimes\ndeclare function setTimeout(callback: () => void, ms: number): unknown;\ndeclare function clearTimeout(id: unknown): void;\n\n/** EOSE fallback timeout in milliseconds. */\nconst EOSE_FALLBACK_MS = 15_000;\n\n/**\n * Options for creating a relay pool service.\n *\n * @example\n * ```ts\n * const relayPoolService = createRelayPoolService({\n * subscribe: (filters, cb, urls) => myPool.subscribe(filters, cb, urls),\n * publish: (event) => myPool.publish(event),\n * selectRelayTier: (filters) => myPool.selectRelays(filters),\n * isAvailable: () => myPool.connected,\n * });\n * ```\n */\nexport interface RelayPoolServiceOptions {\n /**\n * Subscribe to events matching filters. Returns handle with unsubscribe().\n *\n * @param filters - NIP-01 filter objects\n * @param callback - Receives matching events or 'EOSE'\n * @param relayUrls - Optional relay URL hints\n * @returns Handle to cancel the subscription\n */\n subscribe(\n filters: NostrFilter[],\n callback: (item: NostrEvent | 'EOSE') => void,\n relayUrls?: string[],\n ): { unsubscribe(): void };\n\n /**\n * Publish an event to relays.\n *\n * @param event - The event to publish\n */\n publish(event: NostrEvent): void;\n\n /**\n * Select relay URLs appropriate for the given filters.\n *\n * @param filters - NIP-01 filter objects\n * @returns Array of relay URLs\n */\n selectRelayTier(filters: NostrFilter[]): string[];\n\n /**\n * Whether the relay pool is available and connected.\n *\n * @returns true if the relay pool can handle requests\n */\n isAvailable(): boolean;\n}\n\n/** Internal subscription tracking entry. */\ninterface TrackedSubscription {\n handle: { unsubscribe(): void };\n eoseTimer: unknown;\n}\n\ntype RelayServiceMessage = NappletMessage & {\n subId?: unknown;\n filters?: unknown;\n event?: unknown;\n};\n\n/**\n * Create a relay pool service that wraps an existing relay pool\n * implementation as a ServiceHandler.\n *\n * Handles relay.subscribe, relay.close, and relay.publish envelopes.\n * Tracks subscriptions per windowId:subId for lifecycle management.\n * Sets a 15-second EOSE fallback timer on each subscription.\n *\n * @param options - Relay pool implementation to wrap\n * @returns A ServiceHandler ready for runtime.registerService('relay', handler)\n *\n * @example\n * ```ts\n * import { createRelayPoolService } from '@kehto/services';\n *\n * const pool = createRelayPoolService({\n * subscribe: (f, cb, urls) => applesauce.subscribe(f, cb, urls),\n * publish: (e) => applesauce.publish(e),\n * selectRelayTier: (f) => applesauce.getRelays(f),\n * isAvailable: () => applesauce.connected,\n * });\n * runtime.registerService('relay', pool);\n * ```\n */\nexport function createRelayPoolService(options: RelayPoolServiceOptions): ServiceHandler {\n const tracked = new Map<string, TrackedSubscription>();\n\n return {\n descriptor: {\n name: 'relay-pool',\n version: '1.0.0',\n description: 'Relay pool subscription and publishing',\n },\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const relayMessage = message as RelayServiceMessage;\n if (message.type === 'relay.subscribe') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const filters = Array.isArray(relayMessage.filters)\n ? relayMessage.filters as NostrFilter[]\n : [];\n const subKey = `${windowId}:${subId}`;\n\n // Cancel existing subscription for this key if any\n const existing = tracked.get(subKey);\n if (existing) {\n existing.handle.unsubscribe();\n clearTimeout(existing.eoseTimer);\n tracked.delete(subKey);\n }\n\n if (!options.isAvailable()) {\n send({ type: 'relay.eose', subId } as NappletMessage);\n return;\n }\n\n const relayUrls = options.selectRelayTier(filters);\n let eoseSent = false;\n\n const eoseTimer = setTimeout(() => {\n if (!eoseSent) {\n eoseSent = true;\n send({ type: 'relay.eose', subId } as NappletMessage);\n }\n }, EOSE_FALLBACK_MS);\n\n const handle = options.subscribe(filters, (item) => {\n if (item === 'EOSE') {\n clearTimeout(eoseTimer);\n if (!eoseSent) {\n eoseSent = true;\n send({ type: 'relay.eose', subId } as NappletMessage);\n }\n return;\n }\n send({ type: 'relay.event', subId, event: item } as NappletMessage);\n }, relayUrls);\n\n tracked.set(subKey, { handle, eoseTimer });\n return;\n }\n\n if (message.type === 'relay.close') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const subKey = `${windowId}:${subId}`;\n const entry = tracked.get(subKey);\n if (entry) {\n entry.handle.unsubscribe();\n clearTimeout(entry.eoseTimer);\n tracked.delete(subKey);\n }\n return;\n }\n\n if (message.type === 'relay.publish') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (event && typeof event === 'object' && options.isAvailable()) {\n options.publish(event);\n }\n return;\n }\n\n if (message.type === 'relay.publishEncrypted') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (event && typeof event === 'object' && options.isAvailable()) {\n options.publish(event);\n }\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const prefix = `${windowId}:`;\n for (const [key, entry] of tracked) {\n if (key.startsWith(prefix)) {\n entry.handle.unsubscribe();\n clearTimeout(entry.eoseTimer);\n tracked.delete(key);\n }\n }\n },\n };\n}\n","\nimport type { NostrEvent, NostrFilter, NappletMessage } from '@napplet/core';\nimport type { ServiceHandler } from '@kehto/runtime';\n\ntype RelayServiceMessage = NappletMessage & {\n subId?: unknown;\n filters?: unknown;\n event?: unknown;\n};\n\n/**\n * Options for creating a cache service.\n *\n * @example\n * ```ts\n * const cacheService = createCacheService({\n * query: (filters) => myIndexedDB.query(filters),\n * store: (event) => myIndexedDB.store(event),\n * isAvailable: () => true,\n * });\n * ```\n */\nexport interface CacheServiceOptions {\n /**\n * Query cached events matching the given filters.\n *\n * @param filters - NIP-01 filter objects\n * @returns Promise resolving to matching cached events\n */\n query(filters: NostrFilter[]): Promise<NostrEvent[]>;\n\n /**\n * Store an event in cache. Best-effort, may silently fail.\n *\n * @param event - The event to store\n */\n store(event: NostrEvent): void;\n\n /**\n * Whether the cache is available.\n *\n * @returns true if the cache can handle requests\n */\n isAvailable(): boolean;\n}\n\n/**\n * @kehto/services cross-package naming-parity alias for {@link CacheServiceOptions}.\n *\n * `HostCacheBridge` matches the v1.4 `HostKeysBridge` / `HostMediaBridge`\n * convention — it is a pure type alias for `CacheServiceOptions`, NOT a new\n * type. Existing consumers of `CacheServiceOptions` continue to work\n * unchanged; new consumers may prefer `HostCacheBridge` for consistency\n * with the other Host*Bridge names in `@kehto/services`.\n *\n * Anti-feature note (PITFALLS.md M-02): `CacheServiceOptions` MUST remain\n * the primary export. This alias is additive; do not rename or delete\n * `CacheServiceOptions` when other Host*Bridge names eventually\n * stabilize.\n *\n * @example\n * ```ts\n * import type { HostCacheBridge } from '@kehto/services';\n * const cache: HostCacheBridge = {\n * query: (filters) => myIndexedDB.query(filters),\n * store: (event) => myIndexedDB.store(event),\n * isAvailable: () => true,\n * };\n * ```\n */\nexport type HostCacheBridge = CacheServiceOptions;\n\n/**\n * Create a cache service that wraps an existing cache implementation\n * as a ServiceHandler.\n *\n * Cache relay.subscribe subscriptions are one-shot — they query, deliver\n * results, send EOSE, and are done. No long-lived subscription tracking needed.\n * Cache query failures are best-effort: EOSE is sent even on failure.\n *\n * @param options - Cache implementation to wrap\n * @returns A ServiceHandler ready for runtime.registerService('cache', handler)\n *\n * @example\n * ```ts\n * import { createCacheService } from '@kehto/services';\n *\n * const cache = createCacheService({\n * query: (f) => workerRelay.query(f),\n * store: (e) => workerRelay.store(e),\n * isAvailable: () => workerRelay.ready,\n * });\n * runtime.registerService('cache', cache);\n * ```\n */\nexport function createCacheService(options: CacheServiceOptions): ServiceHandler {\n return {\n descriptor: {\n name: 'cache',\n version: '1.0.0',\n description: 'Local event cache (IndexedDB, worker relay, etc.)',\n },\n\n handleMessage(_windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const relayMessage = message as RelayServiceMessage;\n if (message.type === 'relay.subscribe') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const filters = Array.isArray(relayMessage.filters)\n ? relayMessage.filters as NostrFilter[]\n : [];\n\n if (!options.isAvailable()) {\n send({ type: 'relay.eose', subId } as NappletMessage);\n return;\n }\n\n options\n .query(filters)\n .then((events) => {\n for (const event of events) {\n send({ type: 'relay.event', subId, event } as NappletMessage);\n }\n send({ type: 'relay.eose', subId } as NappletMessage);\n })\n .catch(() => {\n // Cache query is best-effort — send EOSE even on failure\n send({ type: 'relay.eose', subId } as NappletMessage);\n });\n return;\n }\n\n if (message.type === 'relay.publish') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (event && typeof event === 'object' && options.isAvailable()) {\n try {\n options.store(event);\n } catch {\n /* Cache write is best-effort */\n }\n }\n return;\n }\n },\n\n // Cache has no per-window state to clean up\n onWindowDestroyed(_windowId: string): void {\n /* no-op */\n },\n };\n}\n","\nimport type { NostrEvent, NostrFilter, NappletMessage } from '@napplet/core';\nimport type { ServiceHandler } from '@kehto/runtime';\nimport type { RelayPoolServiceOptions } from './relay-pool-service.js';\nimport type { CacheServiceOptions } from './cache-service.js';\n\n// Timer globals available in all JS runtimes\ndeclare function setTimeout(callback: () => void, ms: number): unknown;\ndeclare function clearTimeout(id: unknown): void;\n\n/** Default EOSE fallback timeout. */\nconst DEFAULT_EOSE_TIMEOUT_MS = 15_000;\n\n/**\n * Options for creating a coordinated relay service.\n *\n * @example\n * ```ts\n * const relay = createCoordinatedRelay({\n * relayPool: {\n * subscribe: (f, cb, urls) => pool.subscribe(f, cb, urls),\n * publish: (e) => pool.publish(e),\n * selectRelayTier: (f) => pool.selectRelays(f),\n * isAvailable: () => pool.connected,\n * },\n * cache: {\n * query: (f) => db.query(f),\n * store: (e) => db.store(e),\n * isAvailable: () => db.ready,\n * },\n * });\n * runtime.registerService('relay', relay);\n * ```\n */\nexport interface CoordinatedRelayOptions {\n /**\n * Relay pool implementation.\n * Uses the same interface as RelayPoolServiceOptions.\n */\n relayPool: RelayPoolServiceOptions;\n\n /**\n * Local cache implementation.\n * Uses the same interface as CacheServiceOptions.\n */\n cache: CacheServiceOptions;\n\n /**\n * EOSE fallback timeout in milliseconds.\n * Sent if relay pool doesn't respond within this time.\n * Default: 15000 (15 seconds).\n */\n eoseTimeoutMs?: number;\n}\n\n/** Internal state for a tracked subscription. */\ninterface TrackedSub {\n seenIds: Set<string>;\n cacheEose: boolean;\n relayEose: boolean;\n eoseSent: boolean;\n eoseTimer: unknown;\n relayHandle: { unsubscribe(): void } | null;\n}\n\ntype RelayServiceMessage = NappletMessage & {\n subId?: unknown;\n filters?: unknown;\n event?: unknown;\n};\n\n/**\n * Create a coordinated relay service that combines relay pool and cache\n * into a single ServiceHandler with dedup and unified EOSE.\n *\n * On relay.subscribe: queries cache first, then subscribes to relay pool.\n * Events are deduplicated by ID. EOSE is sent after both sources complete.\n * On relay.publish: publishes to relay pool and stores in cache.\n * On relay.close: cancels relay pool subscription.\n *\n * @param options - Relay pool and cache implementations to coordinate\n * @returns A ServiceHandler ready for runtime.registerService('relay', handler)\n *\n * @example\n * ```ts\n * import { createCoordinatedRelay } from '@kehto/services';\n *\n * const relay = createCoordinatedRelay({ relayPool: myPool, cache: myCache });\n * runtime.registerService('relay', relay);\n * ```\n */\nexport function createCoordinatedRelay(options: CoordinatedRelayOptions): ServiceHandler {\n const timeoutMs = options.eoseTimeoutMs ?? DEFAULT_EOSE_TIMEOUT_MS;\n const subs = new Map<string, TrackedSub>();\n\n function maybeSendEose(subKey: string, subId: string, send: (msg: NappletMessage) => void): void {\n const sub = subs.get(subKey);\n if (!sub || sub.eoseSent) return;\n if (sub.cacheEose && sub.relayEose) {\n sub.eoseSent = true;\n clearTimeout(sub.eoseTimer);\n send({ type: 'relay.eose', subId } as NappletMessage);\n }\n }\n\n return {\n descriptor: {\n name: 'relay',\n version: '1.0.0',\n description: 'Coordinated relay pool + cache with dedup and unified EOSE',\n },\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const relayMessage = message as RelayServiceMessage;\n if (message.type === 'relay.subscribe') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const filters = Array.isArray(relayMessage.filters)\n ? relayMessage.filters as NostrFilter[]\n : [];\n const subKey = `${windowId}:${subId}`;\n\n // Cancel existing subscription for this key\n const existing = subs.get(subKey);\n if (existing) {\n existing.relayHandle?.unsubscribe();\n clearTimeout(existing.eoseTimer);\n subs.delete(subKey);\n }\n\n const cacheAvailable = options.cache.isAvailable();\n const relayAvailable = options.relayPool.isAvailable();\n\n // Neither source available — send EOSE immediately\n if (!cacheAvailable && !relayAvailable) {\n send({ type: 'relay.eose', subId } as NappletMessage);\n return;\n }\n\n const tracked: TrackedSub = {\n seenIds: new Set(),\n cacheEose: !cacheAvailable, // mark done if cache not available\n relayEose: !relayAvailable, // mark done if relay not available\n eoseSent: false,\n eoseTimer: null as unknown,\n relayHandle: null,\n };\n subs.set(subKey, tracked);\n\n function deliver(event: NostrEvent): void {\n if (tracked.seenIds.has(event.id)) return;\n tracked.seenIds.add(event.id);\n if (subs.has(subKey)) send({ type: 'relay.event', subId, event } as NappletMessage);\n }\n\n // Query cache (async)\n if (cacheAvailable) {\n options.cache\n .query(filters)\n .then((events) => {\n for (const event of events) deliver(event);\n tracked.cacheEose = true;\n maybeSendEose(subKey, subId, send);\n })\n .catch(() => {\n // Cache query is best-effort\n tracked.cacheEose = true;\n maybeSendEose(subKey, subId, send);\n });\n }\n\n // Subscribe to relay pool\n if (relayAvailable) {\n tracked.eoseTimer = setTimeout(() => {\n if (!tracked.eoseSent) {\n tracked.relayEose = true;\n maybeSendEose(subKey, subId, send);\n }\n }, timeoutMs);\n\n const relayUrls = options.relayPool.selectRelayTier(filters);\n tracked.relayHandle = options.relayPool.subscribe(filters, (item) => {\n if (item === 'EOSE') {\n clearTimeout(tracked.eoseTimer);\n tracked.relayEose = true;\n maybeSendEose(subKey, subId, send);\n return;\n }\n deliver(item);\n // Store relay events in cache\n if (cacheAvailable) {\n try { options.cache.store(item); } catch { /* best-effort */ }\n }\n }, relayUrls);\n }\n return;\n }\n\n if (message.type === 'relay.close') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const subKey = `${windowId}:${subId}`;\n const entry = subs.get(subKey);\n if (entry) {\n entry.relayHandle?.unsubscribe();\n clearTimeout(entry.eoseTimer);\n subs.delete(subKey);\n }\n return;\n }\n\n if (message.type === 'relay.publish') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (!event || typeof event !== 'object') return;\n // Publish to relay pool\n if (options.relayPool.isAvailable()) {\n options.relayPool.publish(event);\n }\n if (options.cache.isAvailable()) {\n try { options.cache.store(event); } catch { /* best-effort */ }\n }\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const prefix = `${windowId}:`;\n for (const [key, entry] of subs) {\n if (key.startsWith(prefix)) {\n entry.relayHandle?.unsubscribe();\n clearTimeout(entry.eoseTimer);\n subs.delete(key);\n }\n }\n },\n };\n}\n","/**\n * keys-service.ts — NIP-5D keys NUB reference document-level listener implementation.\n *\n * Handles the 3 napplet -> shell request types from @napplet/nub/keys:\n * - keys.forward -> invokes options.onForward (hotkey passthrough, fire-and-forget)\n * - keys.registerAction -> parses action.defaultKey into a chord spec, stores the\n * subscription in an in-memory registry keyed by actionId,\n * tracks windowId ownership so onWindowDestroyed can auto-\n * unsubscribe, and echoes { actionId, binding } as .result\n * - keys.unregisterAction -> removes the subscription; fire-and-forget (no envelope)\n *\n * Real listener: on service construction the handler attaches a single\n * `keydown` listener to `options.listenerTarget` (default: `document`). Each\n * keydown is matched against the chord-subscription registry; matches invoke\n * `options.onForward` with the DOM-shape payload AND push a canonical\n * `keys.action` envelope back to the owning napplet via the per-window `send`\n * callback captured at `keys.registerAction` time. Subscriptions persist\n * across messages; `onWindowDestroyed(windowId)` drops all subscriptions owned\n * by the destroyed window as well as its cached `send` handle.\n *\n * On each document keydown matching a registered action, the service\n * additionally emits a `keys.action` envelope to the action's owning napplet\n * via the per-window `send` callback — this is the canonical @napplet/nub/keys\n * surface the SDK's `keys.onAction(...)` helper consumes. The shape is a\n * superset of `KeysActionMessage`: `{ type, actionId, chord }` where `chord`\n * is the parsed `{ ctrl, alt, shift, meta, key }` struct (extension field;\n * base shape unchanged, downstream SDKs that only read `{ type, actionId }`\n * ignore `chord` silently).\n *\n * Field-name translation: @napplet/nub/keys uses the compact\n * { ctrl, alt, shift, meta } form on the wire; the shell's HotkeyHooks\n * (packages/shell/src/types.ts) expects the DOM-compatible\n * { ctrlKey, altKey, shiftKey, metaKey } form. This service performs the\n * translation so callers of `onForward` see the DOM shape.\n *\n * Shell -> napplet push envelopes `keys.bindings` remain the shell-side keys\n * forwarder's responsibility (DRIFT-SHELL-06, tracked under Plan 12-11 /\n * future phase); `keys.action` is emitted here per Plan 26-01.\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n KeysForwardMessage,\n KeysRegisterActionMessage,\n KeysRegisterActionResultMessage,\n KeysActionMessage,\n} from '@napplet/nub/keys/types';\n\n/**\n * Minimal structural subset of the DOM `KeyboardEvent` exposed to\n * `HostKeysBridge` subscribe callbacks. DOM `KeyboardEvent` satisfies this\n * structurally with no adapter needed. OS-bridge impls (Electron, Tauri —\n * out of v1.4 scope) synthesize this from native key events.\n */\nexport interface HostKeyEvent {\n key: string;\n code: string;\n ctrlKey: boolean;\n altKey: boolean;\n shiftKey: boolean;\n metaKey: boolean;\n /** True for OS autorepeat; the service filters these by default. */\n repeat?: boolean;\n}\n\n/**\n * Host-bridge contract for pluggable keyboard backends.\n *\n * The browser reference implementation (the default {@link createKeysService}\n * behaviour when `hostBridge` is omitted) registers a `document`-level keydown\n * listener and satisfies this interface structurally — it exposes\n * `subscribe(chord, callback) => unsubscribe` semantics but omits the two\n * OS-level optional fields (browsers cannot register global hotkeys without\n * privileged APIs).\n *\n * Host apps (Electron, Tauri) implement this interface in their own code and\n * pass it via `createKeysService({ hostBridge: myBridge })` — the service\n * then delegates subscription lifecycle to the bridge and remains browser-free.\n *\n * Reference implementations for Electron / Tauri are explicitly out of v1.4\n * scope and live in host-app examples / follow-up milestones (see\n * REQUIREMENTS.md \"Future Requirements\").\n *\n * @example\n * ```ts\n * // Host-app pseudocode (Electron main-process relay):\n * const electronBridge: HostKeysBridge = {\n * subscribe(chord, cb) {\n * const handle = globalShortcut.register(chord, () => cb({ key: '', code: '', ctrlKey: false, altKey: false, shiftKey: false, metaKey: false }));\n * return () => globalShortcut.unregister(chord);\n * },\n * registerGlobalHotkey: (chord) => globalShortcut.register(chord, () => {}),\n * onGlobalHotkey: (cb) => globalHotkeyBridge.on('global-hotkey', (_, chord) => cb(chord)),\n * };\n *\n * const keys = createKeysService({ hostBridge: electronBridge });\n * runtime.registerService('keys', keys);\n * ```\n */\nexport interface HostKeysBridge {\n /**\n * Subscribe a callback to a chord. Returns an unsubscribe handle.\n *\n * Implementations MUST:\n * - invoke `callback` exactly once per matching chord event (implementations\n * are responsible for any OS-autorepeat filtering)\n * - invoke `callback` synchronously during the event delivery\n * - accept the string chord format documented by @napplet/nub/keys\n * (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)\n */\n subscribe(chord: string, callback: (event: KeyboardEvent | HostKeyEvent) => void): () => void;\n\n /**\n * Optional: register an OS-level global hotkey (works even when the host\n * window is not focused). Returns true on success, false if the chord\n * cannot be registered (e.g. already claimed by another app).\n *\n * Omitted by the browser reference implementation — browsers cannot\n * register OS-level global hotkeys without privileged APIs. Electron\n * (`globalShortcut`) and Tauri (`GlobalShortcut`) provide this.\n */\n registerGlobalHotkey?(chord: string): boolean;\n\n /**\n * Optional: subscribe to OS-level global hotkey events (regardless of\n * focus). Returns an unsubscribe handle.\n *\n * Omitted by the browser reference implementation. See\n * {@link HostKeysBridge.registerGlobalHotkey}.\n */\n onGlobalHotkey?(callback: (chord: string) => void): () => void;\n}\n\n/** Keys service version — follows semver. */\nconst KEYS_SERVICE_VERSION = '1.2.0';\n\n/**\n * Parsed chord struct — internal, never on the wire. The napplet-facing API\n * (and the `action.defaultKey` field) is a string like `\"Ctrl+Shift+K\"`;\n * `parseChord` lowers those strings into this struct for efficient matching\n * against `KeyboardEvent` modifier flags.\n */\ninterface ChordSpec {\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n meta: boolean;\n /** Normalized uppercase single character or DOM key name (e.g. 'K', 'Enter'). */\n key: string;\n}\n\n/** Registry entry — maps a registered actionId back to its owning window + chord. */\ninterface ActionEntry {\n chord: ChordSpec;\n /** Original chord string, preserved for the .result `binding` field. */\n chordString: string;\n windowId: string;\n}\n\n/**\n * Options for creating a keys service via createKeysService().\n *\n * @example\n * ```ts\n * const keys = createKeysService({\n * onForward: (event) => {\n * // event has DOM-compatible field names: ctrlKey, altKey, etc.\n * hotkeyDispatcher.dispatch(event);\n * },\n * });\n * ```\n */\nexport interface KeysServiceOptions {\n /**\n * Called on `keys.forward` (napplet-forwarded chord) AND on document keydown\n * matching a registered action. Receives the DOM-style field names\n * (ctrlKey/altKey/shiftKey/metaKey) to match the shell's HotkeyHooks\n * contract. The service translates from the wire shape\n * ({ ctrl, alt, shift, meta }) before invoking this callback.\n */\n onForward?: (event: {\n key: string;\n code: string;\n ctrlKey: boolean;\n altKey: boolean;\n shiftKey: boolean;\n metaKey: boolean;\n }) => void;\n /**\n * EventTarget to attach the default keydown listener to. Defaults to the\n * global `document` when running in a DOM environment, else an isolated\n * `new EventTarget()` (SSR / Node-test safe). Passing a fresh\n * `new EventTarget()` is useful for unit tests. Mirrors the pattern used\n * by `@kehto/shell`'s `createKeysForwarder`.\n *\n * Ignored when `hostBridge` is provided — the bridge owns subscription\n * lifecycle and no document listener is attached.\n */\n listenerTarget?: EventTarget;\n /**\n * Optional pluggable backend for chord subscription. When provided, the\n * service delegates `keys.registerAction` → `bridge.subscribe(chord, cb)`\n * and stores the returned unsubscribe handle keyed on `actionId`. The\n * default document-listener path is NOT attached when `hostBridge` is\n * provided — the bridge is authoritative. See {@link HostKeysBridge}.\n */\n hostBridge?: HostKeysBridge;\n /**\n * Optional set of shell-reserved chords. Strings in the `@napplet/nub/keys`\n * wire format (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`). When a napplet sends\n * `keys.forward` with a chord matching this set — or when a document\n * keydown matches a reserved chord — the service invokes `onForward`\n * (or the `hostBridge`-registered handler) but suppresses the\n * `keys.action` push to any napplet that registered the same chord via\n * `keys.registerAction`. Precedence: reserved > registered. The shell\n * WANTS the forward — that is why it reserved the chord.\n *\n * Normalized once at service construction via the same chord parser used\n * for `action.defaultKey` — `'Ctrl+K'` / `'Control+k'` / `'ctrl+K'` all\n * match. Static; no runtime mutation. For dynamic reservation see the\n * deferred `HostKeysBridge.reserveAbsolute(chords)` extension.\n *\n * @example\n * ```ts\n * const keys = createKeysService({\n * reservedChords: ['Ctrl+Alt+T', 'Super+Space'],\n * onForward: (event) => wm.dispatch(event),\n * });\n * ```\n */\n reservedChords?: ReadonlyArray<string>;\n}\n\n/** Modifier-token aliases accepted by `parseChord` (case-insensitive). */\nconst MODIFIER_ALIASES: Record<string, keyof Pick<ChordSpec, 'ctrl' | 'alt' | 'shift' | 'meta'>> = {\n ctrl: 'ctrl',\n control: 'ctrl',\n alt: 'alt',\n option: 'alt',\n shift: 'shift',\n meta: 'meta',\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n super: 'meta',\n};\n\n/**\n * Parse a chord string into a `ChordSpec`. Modifier tokens are case-insensitive\n * and recognize common aliases (Cmd/Command/Win/Super → meta, Control → ctrl,\n * Option → alt). Single-character keys are normalized to uppercase so chord\n * matching is case-insensitive; multi-character DOM key names (`Enter`,\n * `ArrowUp`, `F4`) preserve their original casing.\n *\n * Examples:\n * parseChord('Ctrl+Shift+K') → { ctrl: true, alt: false, shift: true, meta: false, key: 'K' }\n * parseChord('ctrl+s') → { ctrl: true, alt: false, shift: false, meta: false, key: 'S' }\n * parseChord('Cmd+P') → { ctrl: false, alt: false, shift: false, meta: true, key: 'P' }\n * parseChord('K') → { ctrl: false, alt: false, shift: false, meta: false, key: 'K' }\n * parseChord('Ctrl++') → { ctrl: true, alt: false, shift: false, meta: false, key: '+' }\n *\n * @throws Error('empty chord') when the input is the empty string.\n * @throws Error(`unknown modifier: ${tok}`) when a non-final token isn't a recognized modifier.\n * @throws Error(`empty key in chord: ${chord}`) when the final token is empty/whitespace.\n */\nfunction parseChord(chord: string): ChordSpec {\n if (chord.length === 0) throw new Error('empty chord');\n const parts = chord.split('+');\n const out: ChordSpec = { ctrl: false, alt: false, shift: false, meta: false, key: '' };\n // The final token is always the key — even if it is literally '+' (chord like 'Ctrl++').\n // All preceding tokens must be modifiers.\n for (let i = 0; i < parts.length - 1; i++) {\n const tok = parts[i].trim().toLowerCase();\n if (tok.length === 0) continue; // tolerate stray whitespace\n const slot = MODIFIER_ALIASES[tok];\n if (!slot) throw new Error(`unknown modifier: ${parts[i]}`);\n out[slot] = true;\n }\n const keyTok = parts[parts.length - 1].trim();\n if (keyTok.length === 0) throw new Error(`empty key in chord: ${chord}`);\n // Single characters normalize to uppercase for case-insensitive comparison;\n // multi-character DOM key names (Enter, ArrowUp, F4) preserve their original casing.\n out.key = keyTok.length === 1 ? keyTok.toUpperCase() : keyTok;\n return out;\n}\n\n/**\n * Create a keys service handler.\n *\n * Attaches a single `keydown` listener to `options.listenerTarget`\n * (default `document`). Matching chord subscriptions invoke `options.onForward`\n * with a DOM-shape payload AND push a `keys.action` envelope back to the\n * owning napplet via the per-window `send` callback captured at\n * `keys.registerAction` time. Returns a `ServiceHandler` augmented with a\n * `destroy()` method that detaches the listener and clears all registries.\n *\n * @param options - Optional configuration (onForward callback, listenerTarget)\n * @returns A ServiceHandler (with `destroy()`) to register with the runtime\n *\n * @example\n * ```ts\n * import { createKeysService } from '@kehto/services';\n *\n * const keys = createKeysService({\n * onForward: (event) => shellHotkeyDispatcher.execute(event),\n * });\n *\n * runtime.registerService('keys', keys);\n * // Later, on shell teardown:\n * keys.destroy();\n * ```\n */\nexport function createKeysService(\n options: KeysServiceOptions = {},\n): ServiceHandler & { destroy(): void } {\n const descriptor: ServiceDescriptor = {\n name: 'keys',\n version: KEYS_SERVICE_VERSION,\n description: options.hostBridge\n ? 'NIP-5D keys NUB reference handler (host-bridge delegated)'\n : 'NIP-5D keys NUB reference handler (document-level chord listener)',\n };\n\n function chordSpecKey(spec: {\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n meta: boolean;\n key: string;\n }): string {\n return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;\n }\n const reservedChordKeys: Set<string> = new Set();\n if (options.reservedChords) {\n for (const chordStr of options.reservedChords) {\n // parseChord throws on malformed input — let it bubble up at construction\n // so misconfigured shells fail loudly at boot, not silently at runtime.\n reservedChordKeys.add(chordSpecKey(parseChord(chordStr)));\n }\n }\n // Canonicalize a wire-shape keys.forward payload into the same key.\n function forwardKey(m: {\n key: string;\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n meta: boolean;\n }): string {\n const k = m.key.length === 1 ? m.key.toUpperCase() : m.key;\n return `${m.ctrl}|${m.alt}|${m.shift}|${m.meta}|${k}`;\n }\n function forwardPayload(m: KeysForwardMessage): Omit<HostKeyEvent, 'repeat'> {\n return {\n key: m.key,\n code: m.code,\n ctrlKey: m.ctrl,\n altKey: m.alt,\n shiftKey: m.shift,\n metaKey: m.meta,\n };\n }\n // Canonicalize a DOM KeyboardEvent into the same key (for Branch B keydown listener).\n function eventKey(ev: KeyboardEvent): string {\n const k = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;\n return `${ev.ctrlKey}|${ev.altKey}|${ev.shiftKey}|${ev.metaKey}|${k}`;\n }\n\n if (options.hostBridge) {\n const bridge = options.hostBridge;\n // windowId → Set<actionId> — parallels Branch B for scoped cleanup.\n const bridgeWindowActions = new Map<string, Set<string>>();\n // actionId → unsubscribe handle returned from bridge.subscribe.\n const unsubscribeHandles = new Map<string, () => void>();\n\n return {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'keys.forward': {\n // Legacy napplet-forwarded path still works identically — preserves wire contract.\n // Phase 33 / KEYS-04-05: reserved chords take precedence. The Branch-A\n // handler never dispatches keys.action to napplets on forward (bridge\n // owns chord → napplet routing via its own subscribe callback), so\n // reservation here is observationally identical to the base case —\n // but we compute and check the reservation explicitly to document\n // the contract for future edits and keep both branches uniform.\n const m = message as KeysForwardMessage;\n const reserved = reservedChordKeys.has(forwardKey(m));\n options.onForward?.(forwardPayload(m));\n if (reserved) {\n return;\n }\n return;\n }\n\n case 'keys.registerAction': {\n const m = message as KeysRegisterActionMessage;\n if (m.action.defaultKey) {\n try {\n const unsubscribe = bridge.subscribe(m.action.defaultKey, (ev) => {\n // Normalize either KeyboardEvent or HostKeyEvent to the DOM-shape onForward payload.\n const e = ev as KeyboardEvent | HostKeyEvent;\n // Bridges may not filter autorepeat — we do, matching Branch B semantics.\n if ('repeat' in e && e.repeat) return;\n options.onForward?.({\n key: e.key,\n code: e.code,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n shiftKey: e.shiftKey,\n metaKey: e.metaKey,\n });\n // Canonical shell→napplet push: emit keys.action to the owning\n // napplet. Shape matches Branch B (superset of KeysActionMessage);\n // the `chord` extension is omitted here because bridges deliver\n // pre-parsed chord events without the internal ChordSpec struct.\n const payload: KeysActionMessage = {\n type: 'keys.action',\n actionId: m.action.id,\n };\n send(payload as NappletMessage);\n });\n unsubscribeHandles.set(m.action.id, unsubscribe);\n if (!bridgeWindowActions.has(windowId)) bridgeWindowActions.set(windowId, new Set());\n bridgeWindowActions.get(windowId)!.add(m.action.id);\n } catch (err) {\n const id = m.id ?? '';\n send({\n type: 'keys.registerAction.error',\n id,\n error: `bridge subscribe failed: ${(err as Error).message}`,\n } as NappletMessage);\n return;\n }\n }\n const result: KeysRegisterActionResultMessage = {\n type: 'keys.registerAction.result',\n id: m.id,\n actionId: m.action.id,\n ...(m.action.defaultKey ? { binding: m.action.defaultKey } : {}),\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'keys.unregisterAction': {\n const m = message as NappletMessage & { actionId?: string };\n if (m.actionId) {\n const unsubscribe = unsubscribeHandles.get(m.actionId);\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch {\n /* best-effort */\n }\n unsubscribeHandles.delete(m.actionId);\n // Prune the bridgeWindowActions entry that owns this actionId.\n for (const [wid, set] of bridgeWindowActions.entries()) {\n if (set.delete(m.actionId) && set.size === 0) bridgeWindowActions.delete(wid);\n }\n }\n }\n return;\n }\n\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown keys method: ${message.type}`,\n } as NappletMessage);\n return;\n }\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const actions = bridgeWindowActions.get(windowId);\n if (!actions) return;\n for (const actionId of actions) {\n const unsubscribe = unsubscribeHandles.get(actionId);\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch {\n /* best-effort */\n }\n }\n unsubscribeHandles.delete(actionId);\n }\n bridgeWindowActions.delete(windowId);\n },\n\n destroy(): void {\n for (const unsubscribe of unsubscribeHandles.values()) {\n try {\n unsubscribe();\n } catch {\n /* best-effort */\n }\n }\n unsubscribeHandles.clear();\n bridgeWindowActions.clear();\n },\n };\n }\n\n const actionRegistry = new Map<string, ActionEntry>(); // actionId → {chord, chordString, windowId}\n const windowActions = new Map<string, Set<string>>(); // windowId → Set<actionId>\n // Per-window `send` callback captured at registerAction time. Used to push\n // keys.action envelopes back to the owning napplet on chord match — this is\n // the canonical @napplet/nub/keys surface the SDK's `keys.onAction(...)`\n // helper consumes.\n const sendHandles = new Map<string, (msg: NappletMessage) => void>(); // windowId → send\n\n // ─── Listener target (SSR / test-safe fallback, mirrors keys-forwarder.ts) ─\n const target: EventTarget =\n options.listenerTarget ??\n (typeof document !== 'undefined' ? document : new EventTarget());\n\n function chordMatches(spec: ChordSpec, ev: KeyboardEvent): boolean {\n if (spec.ctrl !== ev.ctrlKey) return false;\n if (spec.alt !== ev.altKey) return false;\n if (spec.shift !== ev.shiftKey) return false;\n if (spec.meta !== ev.metaKey) return false;\n const evKey = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;\n return spec.key === evKey;\n }\n\n const listener = (rawEv: Event): void => {\n const ev = rawEv as KeyboardEvent;\n if (ev.repeat) return; // ignore OS autorepeat — matches \"I pressed it once\" intent (CONTEXT Area 1)\n\n // Phase 33 / KEYS-04-05: reserved chords fire onForward but suppress\n // keys.action fan-out to napplets. Check ONCE up front.\n const isReserved = reservedChordKeys.has(eventKey(ev));\n\n // Determine if any registered action would match — needed to decide\n // whether to fire onForward on a non-reserved keydown (legacy parity).\n // A reserved chord fires onForward regardless of napplet registration\n // (WM-launcher case: shell declares chord, no napplet registers it).\n let anyMatch = false;\n for (const entry of actionRegistry.values()) {\n if (chordMatches(entry.chord, ev)) {\n anyMatch = true;\n break;\n }\n }\n\n if (isReserved || anyMatch) {\n options.onForward?.({\n key: ev.key,\n code: ev.code,\n ctrlKey: ev.ctrlKey,\n altKey: ev.altKey,\n shiftKey: ev.shiftKey,\n metaKey: ev.metaKey,\n });\n }\n\n if (isReserved) return; // reserved → no napplet fan-out\n\n // (2) Canonical shell→napplet push: emit keys.action to the owning\n // napplet via its captured send callback. The SDK's keys.onAction\n // helper subscribes to this envelope. We attach a `chord` extension\n // field so the demo napplet can display the fired chord without\n // reconstructing it from the original registration.\n for (const [actionId, entry] of actionRegistry.entries()) {\n if (chordMatches(entry.chord, ev)) {\n const send = sendHandles.get(entry.windowId);\n if (send) {\n const payload: KeysActionMessage & { chord: ChordSpec } = {\n type: 'keys.action',\n actionId,\n chord: entry.chord,\n };\n send(payload as NappletMessage);\n }\n // Intentionally no `break` — two actions subscribing to the same chord\n // both receive the event. Conflict resolution is an explicit v1.5+\n // concern per CONTEXT.md Deferred Ideas.\n }\n }\n };\n\n target.addEventListener('keydown', listener);\n\n return {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'keys.forward': {\n // Legacy passthrough: napplet-forwarded keydown translation.\n // Preserved bit-for-bit from the stub — existing tests + the\n // keys-forwarder.ts -> service.handleMessage path depend on this shape.\n //\n // Phase 33 / KEYS-04-05: Branch B's keys.forward handler does NOT\n // emit keys.action (fan-out happens in the document keydown listener),\n // so reservation is observationally identical to the base case.\n // The explicit guard below pins the contract for future edits.\n const m = message as KeysForwardMessage;\n const reserved = reservedChordKeys.has(forwardKey(m));\n options.onForward?.(forwardPayload(m));\n if (reserved) return;\n return;\n }\n\n case 'keys.registerAction': {\n const m = message as KeysRegisterActionMessage;\n // Capture (or refresh) the per-window send callback. The runtime's\n // service-handler contract guarantees `send` remains valid for this\n // windowId until onWindowDestroyed(windowId) fires — we cache the\n // most recent invocation so the keydown listener can push\n // keys.action envelopes back to the owning napplet.\n sendHandles.set(windowId, send);\n\n if (m.action.defaultKey) {\n try {\n const chord = parseChord(m.action.defaultKey);\n actionRegistry.set(m.action.id, {\n chord,\n chordString: m.action.defaultKey,\n windowId,\n });\n if (!windowActions.has(windowId)) windowActions.set(windowId, new Set());\n windowActions.get(windowId)!.add(m.action.id);\n } catch (err) {\n // Parse failure: respond with .error envelope (unknown-method pattern).\n const id = m.id ?? '';\n send({\n type: 'keys.registerAction.error',\n id,\n error: `invalid chord: ${(err as Error).message}`,\n } as NappletMessage);\n return;\n }\n }\n const result: KeysRegisterActionResultMessage = {\n type: 'keys.registerAction.result',\n id: m.id,\n actionId: m.action.id,\n ...(m.action.defaultKey ? { binding: m.action.defaultKey } : {}),\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'keys.unregisterAction': {\n // Fire-and-forget per @napplet/nub/keys spec. Remove subscription if present.\n const m = message as NappletMessage & { actionId?: string };\n if (m.actionId && actionRegistry.has(m.actionId)) {\n const entry = actionRegistry.get(m.actionId)!;\n actionRegistry.delete(m.actionId);\n const set = windowActions.get(entry.windowId);\n if (set) {\n set.delete(m.actionId);\n // If the window has no remaining actions, drop its cached send\n // that no longer subscribes to anything.\n if (set.size === 0) {\n windowActions.delete(entry.windowId);\n sendHandles.delete(entry.windowId);\n }\n }\n }\n return;\n }\n\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown keys method: ${message.type}`,\n } as NappletMessage);\n return;\n }\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const actions = windowActions.get(windowId);\n if (actions) {\n for (const actionId of actions) actionRegistry.delete(actionId);\n windowActions.delete(windowId);\n }\n sendHandles.delete(windowId);\n },\n\n destroy(): void {\n target.removeEventListener('keydown', listener);\n actionRegistry.clear();\n windowActions.clear();\n sendHandles.clear();\n },\n };\n}\n","import type { MediaAction, MediaMetadata } from '@napplet/nub/media/types';\nimport type { HostMediaBridge } from './media-service.js';\n\n/** Silent-audio prime data URL (4 kHz silent WAV, 44 bytes, zero network dependency).\n * Browsers refuse to render OS media controls without a playing audio element —\n * this silent loop primes the MediaSession API. Per CONTEXT.md Area 1. */\nconst SILENT_AUDIO_DATA_URL =\n 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=';\n\n/** Default action set — all 5 nub-media transport actions. */\nexport const DEFAULT_MEDIA_ACTIONS: readonly MediaAction[] = ['play', 'pause', 'next', 'prev', 'seek'];\n\n/** Minimal subset of navigator.mediaSession the browser bridge depends on. Makes the bridge\n * Node/test-safe: unit tests pass a MockMediaSession via mediaSessionTarget.\n * The handler parameter uses `details?` (optional) so both the real DOM impl\n * (which always passes an object) and test mocks that omit details both satisfy\n * this type structurally. */\nexport type MediaSessionTarget = {\n metadata: MediaMetadataLike | null;\n playbackState: 'none' | 'playing' | 'paused';\n setActionHandler(action: string, handler: ((details?: { action?: string; seekTime?: number }) => void) | null): void;\n};\n\n/** Structural subset of the DOM MediaMetadata class — assignable from a plain object\n * with title/artist/album/artwork fields. The browser impl uses `new MediaMetadata({...})`;\n * tests can pass a plain object. */\nexport type MediaMetadataLike = { title?: string; artist?: string; album?: string; artwork?: unknown };\n\n/** Mapping from DOM MediaSession action names to nub-media MediaAction literals. */\nconst ACTION_MATRIX: ReadonlyArray<[string, MediaAction]> = [\n ['play', 'play'],\n ['pause', 'pause'],\n ['nexttrack', 'next'],\n ['previoustrack', 'prev'],\n ['seekto', 'seek'],\n];\n\n/**\n * Reference browser implementation of {@link HostMediaBridge}.\n *\n * Mirrors metadata to `navigator.mediaSession.metadata` (via the DOM\n * `MediaMetadata` constructor when available; plain-object fallback in test\n * envs). Mirrors playback state to `navigator.mediaSession.playbackState`\n * with the canonical mapping: 'playing' maps to 'playing', 'paused' maps to\n * 'paused', 'buffering' maps to 'paused', 'stopped' maps to 'none'. Installs\n * `setActionHandler` callbacks for play/pause/nexttrack/previoustrack/seekto\n * that fan into the onAction subscriber with the mapped `MediaAction` literal\n * (and `value` from `details.seekTime` for seekto). When `setActiveSession` is\n * called with a non-null `actions` parameter, only the declared actions get\n * active handlers — the remaining are cleared (matching the capabilities\n * narrowing behavior of Plan 27-01's inline implementation).\n *\n * Installs a silent-audio prime (4 kHz silent WAV data URL) when the first\n * session becomes active (setActiveSession called with a non-null sessionId) —\n * browsers refuse to render OS media controls without a playing audio element.\n * Removes the element when destroySession brings the active session count to\n * zero.\n *\n * @param opts.mediaSessionTarget - Override navigator.mediaSession (tests).\n * @param opts.documentTarget - Override document (tests; pass null to disable silent-audio prime).\n */\nexport function createBrowserMediaBridge(opts: {\n mediaSessionTarget?: MediaSessionTarget;\n documentTarget?: Document | null;\n} = {}): HostMediaBridge {\n const ms: MediaSessionTarget | null =\n opts.mediaSessionTarget\n ?? (typeof navigator !== 'undefined' && 'mediaSession' in navigator\n ? (navigator.mediaSession as MediaSessionTarget)\n : null);\n const doc: Document | null =\n opts.documentTarget !== undefined\n ? opts.documentTarget\n : (typeof document !== 'undefined' ? document : null);\n\n let silentAudioEl: HTMLAudioElement | null = null;\n let activeSessionId: string | null = null;\n let sessionsActive = 0;\n const actionCallbacks = new Set<(sessionId: string, action: MediaAction, value?: number) => void>();\n\n function primeSilentAudio(): void {\n if (silentAudioEl || !doc) return;\n const el = doc.createElement('audio') as HTMLAudioElement;\n el.src = SILENT_AUDIO_DATA_URL;\n el.loop = true;\n el.style.display = 'none';\n (el as HTMLAudioElement).setAttribute('data-kehto-silent-audio-prime', 'true');\n doc.body.appendChild(el);\n void el.play().catch(() => { /* autoplay refused — metadata mirror still works */ });\n silentAudioEl = el;\n }\n\n function teardownSilentAudio(): void {\n if (!silentAudioEl) return;\n try { silentAudioEl.pause(); } catch { /* best-effort */ }\n try { silentAudioEl.remove(); } catch { /* best-effort */ }\n silentAudioEl = null;\n }\n\n /**\n * Install action handlers for the given set of nub-media actions. Actions not\n * in the set get their handler cleared. Matches Plan 27-01's installActionHandlersFor\n * behavior exactly so capabilities-narrowing tests continue to pass.\n */\n function applyActionHandlers(actions: readonly MediaAction[] = DEFAULT_MEDIA_ACTIONS): void {\n if (!ms) return;\n for (const [domAction, nubAction] of ACTION_MATRIX) {\n if (!actions.includes(nubAction)) {\n try { ms.setActionHandler(domAction, null); } catch { /* best-effort */ }\n continue;\n }\n ms.setActionHandler(domAction, (details) => {\n if (!activeSessionId) return;\n const value = nubAction === 'seek' && typeof details?.seekTime === 'number' ? details.seekTime : undefined;\n for (const cb of actionCallbacks) {\n cb(activeSessionId, nubAction, value);\n }\n });\n }\n }\n\n function writeMetadata(metadata: MediaMetadata | undefined): void {\n if (!ms) return;\n if (!metadata) { ms.metadata = null; return; }\n const artwork = metadata.artwork?.url ? [{ src: metadata.artwork.url }] : undefined;\n const init: MediaMetadataLike & { artwork?: unknown } = {\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n ...(artwork ? { artwork } : {}),\n };\n try {\n const ctor = (globalThis as typeof globalThis & { MediaMetadata?: new (init: MediaMetadataLike) => MediaMetadataLike }).MediaMetadata;\n ms.metadata = ctor ? new ctor(init) : (init as MediaMetadataLike);\n } catch {\n ms.metadata = init as MediaMetadataLike;\n }\n }\n\n return {\n setMetadata(sessionId, metadata) {\n if (sessionId === activeSessionId) writeMetadata(metadata);\n },\n setPlaybackState(sessionId, state) {\n if (!ms || sessionId !== activeSessionId) return;\n ms.playbackState =\n state === 'playing' ? 'playing'\n : state === 'paused' || state === 'buffering' ? 'paused'\n : 'none';\n },\n onAction(callback) {\n actionCallbacks.add(callback);\n return () => { actionCallbacks.delete(callback); };\n },\n setActiveSession(sessionId, actions) {\n activeSessionId = sessionId;\n if (!sessionId) {\n if (ms) {\n ms.metadata = null;\n ms.playbackState = 'none';\n for (const [domAction] of ACTION_MATRIX) {\n try { ms.setActionHandler(domAction, null); } catch { /* best-effort */ }\n }\n }\n return;\n }\n // Prime silent-audio on first session becoming active.\n if (sessionsActive === 0) { primeSilentAudio(); sessionsActive = 1; }\n applyActionHandlers(actions ?? DEFAULT_MEDIA_ACTIONS);\n },\n destroySession(_sessionId) {\n sessionsActive = Math.max(0, sessionsActive - 1);\n if (sessionsActive === 0) teardownSilentAudio();\n },\n };\n}\n","/**\n * media-service.ts — NIP-5D media NUB reference service (navigator.mediaSession\n * reference implementation).\n *\n * Handles the napplet-owned subset of @napplet/nub/media:\n * media.session.create (result), media.session.update, media.session.destroy,\n * media.state, media.capabilities.\n *\n * HostMediaBridge contract: {@link HostMediaBridge} defines the pluggable backend\n * contract for metadata/state mirroring + action routing. The browser reference\n * implementation is {@link createBrowserMediaBridge} (mirrors to navigator.mediaSession\n * with setActionHandler matrix). When no hostBridge option is passed, createMediaService\n * internally uses createBrowserMediaBridge as the default — behavior is identical\n * to the Plan 27-01 single-path implementation.\n *\n * navigator.mediaSession mirroring: on session.create the browser bridge mirrors the\n * napplet-supplied metadata to navigator.mediaSession.metadata via new MediaMetadata()\n * and installs setActionHandler callbacks for the 5 OS transport actions\n * (play / pause / nexttrack / previoustrack / seekto). Each callback emits a canonical\n * media.command envelope to the owning napplet — that is the @napplet/nub/media\n * MediaCommandMessage shape consumed by the SDK's mediaOnCommand() helper.\n *\n * NAP-MEDIA now distinguishes napplet-owned playback from shell-owned\n * playback. This reference backend supports `owner: \"napplet\"` because it\n * mirrors a napplet's own media element to navigator.mediaSession. It rejects\n * `owner: \"shell\"` until a host bridge provides policy-checked source fetching\n * and playback.\n *\n * media.command push: when the OS user clicks a media control (hardware key, lock-screen\n * transport, OS media overlay), the bridge's onAction callback fires. The service looks\n * up the active session's owning napplet, invokes the per-window send callback captured\n * at session.create time, and delivers:\n * { type: 'media.command', sessionId, action, value? }\n * where action is the nub-media MediaAction literal (play|pause|next|prev|seek) and\n * value is the seekTime (seconds) for seek.\n *\n * Silent-audio prime: the browser bridge creates a hidden <audio> element with a\n * 4 kHz silent WAV data URL and plays it when the first session becomes active\n * (setActiveSession with a non-null sessionId). Without a playing audio element in\n * the host page, most browsers refuse to render OS media controls. The element is\n * cleaned up when the last session is destroyed (via bridge.destroySession).\n *\n * Multi-session registry with last-active-wins semantics: every session.create is\n * tracked in a Map<sessionId, SessionEntry>; any media.state report promotes that\n * session to active. The active session's metadata and playback state are reflected\n * via bridge.setMetadata / bridge.setPlaybackState. When the active session is\n * destroyed, the next-most-recently-touched session is promoted automatically.\n *\n * Note: this is SEPARATE from packages/services/src/audio-service.ts, which\n * is the legacy ifc-topic-based audio source registry (audio:* topic events\n * over ifc.emit). media-service is the canonical @napplet/nub/media NIP-5D\n * path and they coexist — audio-service continues to track audio sources for\n * shell UI, while media-service handles the NUB protocol envelope surface.\n *\n * Shell -> Napplet push types (media.command) are emitted here when\n * bridge.onAction callbacks fire — this is the canonical Phase 27 real backend.\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n MediaAction,\n MediaCapabilitiesMessage,\n MediaCommandMessage,\n MediaMetadata,\n MediaSessionDestroyMessage,\n MediaSessionUpdateMessage,\n MediaStateMessage,\n} from '@napplet/nub/media/types';\nimport {\n createBrowserMediaBridge,\n DEFAULT_MEDIA_ACTIONS,\n type MediaSessionTarget,\n} from './browser-media-bridge.js';\n\nexport { createBrowserMediaBridge } from './browser-media-bridge.js';\nexport type {\n MediaMetadataLike,\n MediaSessionTarget,\n} from './browser-media-bridge.js';\n\n/** Registry entry — maps a sessionId back to its owning window, metadata snapshot,\n * state snapshot, and declared capabilities. Internal; never on the wire. */\ninterface SessionEntry {\n sessionId: string;\n windowId: string;\n owner: MediaPlaybackOwner;\n source: MediaSourceRef | undefined;\n metadata: MediaMetadata | undefined;\n state: { status: 'playing' | 'paused' | 'stopped' | 'buffering'; position?: number; duration?: number; volume?: number } | undefined;\n actions: readonly MediaAction[]; // from media.capabilities; defaults to ['play','pause','next','prev','seek']\n /** Monotonic tick updated on every session.create / session.update / state — used for last-active-wins resolution when the current active session is destroyed. */\n lastTouched: number;\n}\n\n/** Media service version — follows semver. */\nconst MEDIA_SERVICE_VERSION = '1.1.0';\n\nexport type MediaPlaybackOwner = 'shell' | 'napplet';\n\nexport interface MediaSourceRef {\n url?: string;\n blossomHash?: string;\n nostr?: {\n eventId?: string;\n address?: string;\n relays?: string[];\n };\n mimeType?: string;\n}\n\nexport interface MediaSessionCreateOptions {\n owner: MediaPlaybackOwner;\n sessionId?: string;\n source?: MediaSourceRef;\n metadata?: MediaMetadata;\n capabilities?: MediaAction[];\n autoplay?: boolean;\n live?: boolean;\n}\n\ntype MediaSessionCreateEnvelope = NappletMessage & Partial<MediaSessionCreateOptions> & {\n type: 'media.session.create';\n id?: string;\n};\n\ntype MediaSessionCreateResultEnvelope = NappletMessage & {\n type: 'media.session.create.result';\n id: string;\n sessionId?: string;\n owner?: MediaPlaybackOwner;\n error?: string;\n};\n\n/**\n * Host-bridge contract for pluggable media backends.\n *\n * The browser reference implementation ({@link createBrowserMediaBridge}) mirrors\n * napplet-reported metadata/state to `navigator.mediaSession` and installs\n * `setActionHandler` callbacks that fan into the bridge's onAction subscribers.\n * It satisfies this interface with all 5 fields implemented (setActiveSession\n * switches the active session and optionally re-applies action-handler narrowing;\n * destroySession tears down the silent-audio prime on last-session teardown).\n *\n * Host apps (Electron, Tauri) implement this interface in their own code and\n * pass it via `createMediaService({ hostBridge: myBridge })` — the service\n * then delegates metadata/state mirroring + action routing to the bridge and\n * remains browser-free. Session-ownership bookkeeping (which windowId owns\n * which sessionId, which send callback routes media.command back to which\n * napplet) stays in the service layer — that's wire-protocol concern, not a\n * bridge concern.\n *\n * Reference implementations for Electron / Tauri are explicitly out of v1.4\n * scope and live in host-app examples / follow-up milestones (see\n * REQUIREMENTS.md \"Future Requirements\").\n *\n * @example\n * ```ts\n * // Host-app pseudocode (Electron main-process relay):\n * const electronBridge: HostMediaBridge = {\n * setMetadata(sessionId, md) { mediaBridge.sendMetadata({ sessionId, md }); },\n * setPlaybackState(sessionId, state) { mediaBridge.sendPlaybackState({ sessionId, state }); },\n * onAction(cb) {\n * const handler = (_: unknown, msg: { sessionId: string; action: MediaAction; value?: number }) =>\n * cb(msg.sessionId, msg.action, msg.value);\n * mediaBridge.onAction(handler);\n * return () => mediaBridge.offAction(handler);\n * },\n * };\n * const media = createMediaService({ hostBridge: electronBridge });\n * runtime.registerService('media', media);\n * ```\n */\nexport interface HostMediaBridge {\n /**\n * Set the metadata displayed on the OS transport surface for a session.\n * Called on session.create (with initial metadata) and on session.update\n * (with merged metadata) whenever the session is the active session.\n * Implementations MUST be idempotent.\n */\n setMetadata(sessionId: string, metadata: MediaMetadata): void;\n\n /**\n * Set the playback state for a session. Called on media.state reports\n * whenever the session is the active session. State strings match\n * nub-media MediaState.status exactly. Implementations MUST be idempotent.\n */\n setPlaybackState(sessionId: string, state: 'playing' | 'paused' | 'stopped' | 'buffering'): void;\n\n /**\n * Subscribe to OS-level action events (user clicks play/pause/seek/next/prev\n * on the transport surface). Returns an unsubscribe handle.\n *\n * The callback receives `(sessionId, action, value?)`. `sessionId` is the\n * bridge's currently-active session (the browser impl tracks this internally\n * via setActionHandler-at-fire-time; native impls track via setActiveSession).\n * `value` is populated for `action === 'seek'` (seek target in seconds) and\n * for `action === 'volume'` (0.0-1.0). The service dispatches the resulting\n * `media.command` envelope to the owning napplet of that session.\n */\n onAction(callback: (sessionId: string, action: MediaAction, value?: number) => void): () => void;\n\n /**\n * Optional: notify the bridge that the active session has changed. The\n * browser reference impl uses this to switch which session's metadata/state\n * is mirrored to the singleton navigator.mediaSession and to install (or\n * clear) action handlers for the session's declared capabilities.\n *\n * The optional `actions` parameter carries the session's declared capability\n * set so the bridge can narrow which OS transport buttons are active. When\n * omitted, the bridge applies its default set. Native OS bridges that track\n * active-session state internally may omit this field entirely.\n */\n setActiveSession?(sessionId: string | null, actions?: readonly MediaAction[]): void;\n\n /**\n * Optional: tear down per-session resources. The browser reference impl\n * uses this to remove the silent-audio prime element when the last session\n * is destroyed. Bridges that need no per-session teardown may omit this field.\n */\n destroySession?(sessionId: string): void;\n}\n\n/**\n * Optional host callbacks for the media service.\n *\n * Host shells can hook session creation and state updates to drive\n * their own UI (e.g., now-playing banner) without replacing the\n * ServiceHandler wholesale.\n *\n * @example\n * ```ts\n * const media = createMediaService({\n * onSessionCreate: (windowId, sessionId, metadata) => {\n * console.log(`[${windowId}] created session ${sessionId}`, metadata);\n * },\n * onState: (windowId, sessionId, state) => {\n * nowPlaying.update(windowId, state);\n * },\n * });\n *\n * runtime.registerService('media', media);\n * ```\n */\nexport interface MediaServiceOptions {\n /** Called when a napplet creates a session. May inspect/record the session. */\n onSessionCreate?: (windowId: string, sessionId: string, metadata?: unknown) => void;\n /** Called on media.state updates — high-frequency; keep handler work minimal. */\n onState?: (windowId: string, sessionId: string, state: unknown) => void;\n /** Called when a napplet destroys a session. */\n onSessionDestroy?: (windowId: string, sessionId: string) => void;\n /** Called when a napplet updates session metadata. */\n onSessionUpdate?: (windowId: string, sessionId: string, metadata: unknown) => void;\n /** Called when a napplet declares capabilities for a session. */\n onCapabilities?: (windowId: string, sessionId: string, actions: unknown) => void;\n\n /**\n * MediaSession target override (used by default browser bridge only).\n * Defaults to `navigator.mediaSession` when running in a browser. Pass a\n * MockMediaSession in unit tests. Ignored when `hostBridge` is provided.\n */\n mediaSessionTarget?: MediaSessionTarget;\n\n /**\n * DOM document override (used by default browser bridge only).\n * Defaults to `document` when available. Set to null to disable the silent-audio\n * prime entirely — useful in unit tests. Ignored when `hostBridge` is provided.\n */\n documentTarget?: Document | null;\n\n /**\n * Optional pluggable backend for metadata/state mirroring + OS action handling.\n * When provided, the service delegates setMetadata / setPlaybackState / onAction\n * to the bridge and skips navigator.mediaSession entirely. When omitted, the\n * service internally uses {@link createBrowserMediaBridge} as the default.\n * See {@link HostMediaBridge}.\n */\n hostBridge?: HostMediaBridge;\n}\n\ninterface MediaServiceState {\n bridge: HostMediaBridge;\n options: MediaServiceOptions;\n sessionRegistry: Map<string, SessionEntry>;\n windowSessions: Map<string, Set<string>>;\n sendHandles: Map<string, (msg: NappletMessage) => void>;\n activeSessionId: string | null;\n touchCounter: number;\n sessionCounter: number;\n}\n\nfunction createMediaServiceState(options: MediaServiceOptions, bridge: HostMediaBridge): MediaServiceState {\n return {\n bridge,\n options,\n sessionRegistry: new Map<string, SessionEntry>(),\n windowSessions: new Map<string, Set<string>>(),\n sendHandles: new Map<string, (msg: NappletMessage) => void>(),\n activeSessionId: null,\n touchCounter: 0,\n sessionCounter: 0,\n };\n}\n\nfunction setActive(state: MediaServiceState, sessionId: string | null, actions?: readonly MediaAction[]): void {\n state.activeSessionId = sessionId;\n state.bridge.setActiveSession?.(sessionId, actions);\n if (!sessionId) return;\n const entry = state.sessionRegistry.get(sessionId);\n if (!entry) return;\n if (entry.metadata) state.bridge.setMetadata(sessionId, entry.metadata);\n if (entry.state) state.bridge.setPlaybackState(sessionId, entry.state.status);\n}\n\nfunction promoteNextActiveOrClear(state: MediaServiceState): void {\n if (state.sessionRegistry.size === 0) {\n setActive(state, null);\n return;\n }\n let latest: SessionEntry | null = null;\n for (const entry of state.sessionRegistry.values()) {\n if (!latest || entry.lastTouched > latest.lastTouched) latest = entry;\n }\n setActive(state, latest ? latest.sessionId : null, latest?.actions);\n}\n\nfunction sendMediaCommand(state: MediaServiceState, sessionId: string, action: MediaAction, value?: number): void {\n const entry = state.sessionRegistry.get(sessionId);\n if (!entry) return;\n const send = state.sendHandles.get(entry.windowId);\n if (!send) return;\n const payload: MediaCommandMessage = {\n type: 'media.command',\n sessionId,\n action,\n ...(typeof value === 'number' ? { value } : {}),\n };\n send(payload as NappletMessage);\n}\n\nfunction registerWindowSession(state: MediaServiceState, windowId: string, sessionId: string): void {\n if (!state.windowSessions.has(windowId)) state.windowSessions.set(windowId, new Set());\n state.windowSessions.get(windowId)!.add(sessionId);\n}\n\nfunction sendSessionCreateResult(\n send: (msg: NappletMessage) => void,\n id: string | undefined,\n fields: Omit<MediaSessionCreateResultEnvelope, 'type' | 'id'>,\n): void {\n send({\n type: 'media.session.create.result',\n id: id ?? '',\n ...fields,\n } as NappletMessage);\n}\n\nfunction isMediaPlaybackOwner(value: unknown): value is MediaPlaybackOwner {\n return value === 'shell' || value === 'napplet';\n}\n\nfunction hasSourceRef(source: MediaSourceRef | undefined): boolean {\n if (!source) return false;\n if (typeof source.url === 'string' && source.url.length > 0) return true;\n if (typeof source.blossomHash === 'string' && source.blossomHash.length > 0) return true;\n if (source.nostr) {\n return Boolean(source.nostr.eventId || source.nostr.address);\n }\n return false;\n}\n\nfunction canonicalizeSessionId(\n state: MediaServiceState,\n windowId: string,\n preferredSessionId: string | undefined,\n): string {\n const trimmed = typeof preferredSessionId === 'string' ? preferredSessionId.trim() : '';\n const hint = trimmed || `session-${++state.sessionCounter}`;\n if (!state.sessionRegistry.has(hint)) return hint;\n\n let next: string;\n do {\n next = `${windowId}:${hint}:${++state.sessionCounter}`;\n } while (state.sessionRegistry.has(next));\n return next;\n}\n\nfunction handleSessionCreate(\n state: MediaServiceState,\n windowId: string,\n message: MediaSessionCreateEnvelope,\n send: (msg: NappletMessage) => void,\n): void {\n if (!isMediaPlaybackOwner(message.owner)) {\n sendSessionCreateResult(send, message.id, { error: 'missing owner' });\n return;\n }\n\n if (message.owner === 'shell') {\n if (!hasSourceRef(message.source)) {\n sendSessionCreateResult(send, message.id, { owner: 'shell', error: 'missing source' });\n return;\n }\n sendSessionCreateResult(send, message.id, { owner: 'shell', error: 'unsupported owner mode' });\n return;\n }\n\n state.sendHandles.set(windowId, send);\n const sessionId = canonicalizeSessionId(state, windowId, message.sessionId);\n const entry: SessionEntry = {\n sessionId,\n windowId,\n owner: message.owner,\n source: message.source,\n metadata: message.metadata,\n state: undefined,\n actions: message.capabilities ?? DEFAULT_MEDIA_ACTIONS,\n lastTouched: ++state.touchCounter,\n };\n state.sessionRegistry.set(sessionId, entry);\n registerWindowSession(state, windowId, sessionId);\n setActive(state, sessionId, entry.actions);\n state.options.onSessionCreate?.(windowId, sessionId, message.metadata);\n sendSessionCreateResult(send, message.id, { sessionId, owner: message.owner });\n}\n\nfunction handleSessionUpdate(state: MediaServiceState, windowId: string, message: MediaSessionUpdateMessage): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry) {\n entry.metadata = { ...entry.metadata, ...message.metadata };\n entry.lastTouched = ++state.touchCounter;\n if (entry.owner === 'napplet' && message.sessionId === state.activeSessionId && entry.metadata) {\n state.bridge.setMetadata(message.sessionId, entry.metadata);\n }\n }\n state.options.onSessionUpdate?.(windowId, message.sessionId, message.metadata);\n}\n\nfunction handleSessionDestroy(state: MediaServiceState, windowId: string, message: MediaSessionDestroyMessage): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry) {\n state.sessionRegistry.delete(message.sessionId);\n const set = state.windowSessions.get(entry.windowId);\n if (set) {\n set.delete(message.sessionId);\n if (set.size === 0) state.windowSessions.delete(entry.windowId);\n }\n state.bridge.destroySession?.(message.sessionId);\n if (message.sessionId === state.activeSessionId) promoteNextActiveOrClear(state);\n }\n state.options.onSessionDestroy?.(windowId, message.sessionId);\n}\n\nfunction handleMediaState(state: MediaServiceState, windowId: string, message: MediaStateMessage): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry?.owner === 'napplet') {\n entry.state = {\n status: message.status,\n position: message.position,\n duration: message.duration,\n volume: message.volume,\n };\n entry.lastTouched = ++state.touchCounter;\n if (state.activeSessionId !== message.sessionId) setActive(state, message.sessionId, entry.actions);\n else state.bridge.setPlaybackState(message.sessionId, message.status);\n }\n state.options.onState?.(windowId, message.sessionId, message);\n}\n\nfunction handleMediaCapabilities(\n state: MediaServiceState,\n windowId: string,\n message: MediaCapabilitiesMessage,\n): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry?.owner === 'napplet') {\n entry.actions = message.actions;\n entry.lastTouched = ++state.touchCounter;\n if (message.sessionId === state.activeSessionId) {\n state.bridge.setActiveSession?.(message.sessionId, entry.actions);\n }\n }\n state.options.onCapabilities?.(windowId, message.sessionId, message.actions);\n}\n\nfunction handleMediaMessage(\n state: MediaServiceState,\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n): void {\n switch (message.type) {\n case 'media.session.create':\n handleSessionCreate(state, windowId, message as MediaSessionCreateEnvelope, send);\n return;\n case 'media.session.update':\n handleSessionUpdate(state, windowId, message as MediaSessionUpdateMessage);\n return;\n case 'media.session.destroy':\n handleSessionDestroy(state, windowId, message as MediaSessionDestroyMessage);\n return;\n case 'media.state':\n handleMediaState(state, windowId, message as MediaStateMessage);\n return;\n case 'media.capabilities':\n handleMediaCapabilities(state, windowId, message as MediaCapabilitiesMessage);\n return;\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown media method: ${message.type}`,\n } as NappletMessage);\n }\n }\n}\n\nfunction destroyWindowSessions(state: MediaServiceState, windowId: string): void {\n const sessions = state.windowSessions.get(windowId);\n if (sessions) {\n const ownedActive = state.activeSessionId !== null && sessions.has(state.activeSessionId);\n for (const sessionId of sessions) {\n state.sessionRegistry.delete(sessionId);\n state.bridge.destroySession?.(sessionId);\n }\n state.windowSessions.delete(windowId);\n if (ownedActive) promoteNextActiveOrClear(state);\n }\n state.sendHandles.delete(windowId);\n}\n\nfunction destroyMediaState(state: MediaServiceState, unsubscribeAction: () => void): void {\n unsubscribeAction();\n for (const sessionId of state.sessionRegistry.keys()) state.bridge.destroySession?.(sessionId);\n state.bridge.setActiveSession?.(null);\n state.sessionRegistry.clear();\n state.windowSessions.clear();\n state.sendHandles.clear();\n state.activeSessionId = null;\n state.touchCounter = 0;\n state.sessionCounter = 0;\n}\n\n/**\n * Create a media NUB service handler with navigator.mediaSession integration.\n *\n * Implements the 5 napplet->shell media.* request types defined in\n * `@napplet/nub/media`. Only `media.session.create` produces a reply\n * envelope (`media.session.create.result`) — the remaining four\n * (`session.update`, `session.destroy`, `state`, `capabilities`) are\n * fire-and-forget per the NUB spec.\n *\n * Unknown `media.*` actions produce a `<type>.error` envelope so\n * napplets are never left hanging on a malformed request.\n *\n * @param options - Optional host callbacks for session lifecycle + state, plus\n * mediaSessionTarget and documentTarget for test injection, and an optional\n * hostBridge for native-OS media backend delegation.\n * @returns A ServiceHandler (with `destroy()`) to register with the runtime\n *\n * @example\n * ```ts\n * import { createMediaService } from '@kehto/services';\n *\n * const media = createMediaService();\n * runtime.registerService('media', media);\n * // Later, on shell teardown:\n * media.destroy();\n * ```\n */\nexport function createMediaService(options: MediaServiceOptions = {}): ServiceHandler & { destroy(): void } {\n const descriptor: ServiceDescriptor = {\n name: 'media',\n version: MEDIA_SERVICE_VERSION,\n description: options.hostBridge\n ? 'NIP-5D media NUB reference handler (host-bridge delegated)'\n : 'NIP-5D media NUB reference handler (navigator.mediaSession mirror)',\n };\n\n const bridge: HostMediaBridge = options.hostBridge\n ?? createBrowserMediaBridge({\n mediaSessionTarget: options.mediaSessionTarget,\n documentTarget: options.documentTarget,\n });\n const state = createMediaServiceState(options, bridge);\n\n const unsubscribeAction = bridge.onAction((sessionId, action, value) => {\n sendMediaCommand(state, sessionId, action, value);\n });\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n handleMediaMessage(state, windowId, message, send);\n },\n\n onWindowDestroyed(windowId: string): void {\n destroyWindowSessions(state, windowId);\n },\n\n destroy(): void {\n destroyMediaState(state, unsubscribeAction);\n },\n };\n}\n","/**\n * notify-service.ts — NIP-5D notify NUB reference service (stub-level).\n *\n * Handles the 5 napplet -> shell request types from `@napplet/nub/notify`:\n * - `notify.send` -> `notify.send.result` (shell-assigned id)\n * - `notify.dismiss` -> fire-and-forget\n * - `notify.badge` -> fire-and-forget\n * - `notify.channel.register` -> fire-and-forget\n * - `notify.permission.request` -> `notify.permission.result { granted }`\n *\n * Stub-level: no real Notification API calls, no real channel registry.\n * Host apps wire a real backend via\n * `runtime.registerService('notify', realHandler)`.\n *\n * Coexistence note: this is SEPARATE from\n * `packages/services/src/notification-service.ts`, which is the legacy\n * ifc-topic-based notification registry (operates on `ifc.emit` topics\n * under `notifications:*`). `notify-service.ts` is the canonical\n * `@napplet/nub/notify` NIP-5D path and lives alongside the legacy module.\n * If the host app registers this service via\n * `runtime.registerService('notify', ...)`, @napplet/nub/notify messages\n * land here; the legacy ifc-emit topic path remains untouched.\n *\n * Shell -> napplet push messages (`notify.action`, `notify.clicked`,\n * `notify.dismissed`, `notify.controls`) are not emitted by this stub —\n * they are the host app's responsibility and are deferred to a future plan.\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n NotifySendMessage,\n NotifySendResultMessage,\n NotifyPermissionRequestMessage,\n NotifyPermissionResultMessage,\n} from '@napplet/nub/notify/types';\n\n/** Notify service version — follows semver. */\nconst NOTIFY_SERVICE_VERSION = '1.0.0';\n\n/**\n * Optional configuration for `createNotifyService`.\n *\n * @example\n * ```ts\n * const notify = createNotifyService({\n * generateId: () => crypto.randomUUID(),\n * defaultGrant: false,\n * onSend: (windowId, msg) => console.log(`napplet ${windowId} sent ${msg.title}`),\n * });\n * runtime.registerService('notify', notify);\n * ```\n */\nexport interface NotifyServiceOptions {\n /**\n * Generate a shell-assigned notification ID for `notify.send.result`.\n * Default: a monotonically increasing `shell-<n>` counter.\n */\n generateId?: () => string;\n /**\n * Default permission grant for `notify.permission.request`.\n * Host apps that want real permission prompts should replace this\n * service with a real backend via `runtime.registerService`.\n * Default: `true`.\n */\n defaultGrant?: boolean;\n /**\n * Called synchronously when a napplet dispatches `notify.send`.\n * Intended for host-app plumbing (UI toast, logging, etc.) without\n * requiring a full backend replacement.\n */\n onSend?: (windowId: string, payload: NotifySendMessage) => void;\n}\n\n/**\n * Create a stub-level notify service handler.\n *\n * Answers the 5 napplet->shell request types from `@napplet/nub/notify`.\n * Does NOT implement a real backend (no DOM Notification API, no channel\n * registry, no permission prompt). Host apps replace this via\n * `runtime.registerService('notify', realHandler)` when a real backend is\n * needed.\n *\n * @param options - Optional service configuration (see NotifyServiceOptions)\n * @returns A ServiceHandler to register with the runtime under domain `notify`\n *\n * @example\n * ```ts\n * import { createNotifyService } from '@kehto/services';\n *\n * const notify = createNotifyService();\n * runtime.registerService('notify', notify);\n * ```\n */\nexport function createNotifyService(options: NotifyServiceOptions = {}): ServiceHandler {\n let counter = 0;\n const gen = options.generateId ?? ((): string => {\n counter += 1;\n return `shell-${counter}`;\n });\n const defaultGrant = options.defaultGrant ?? true;\n\n const descriptor: ServiceDescriptor = {\n name: 'notify',\n version: NOTIFY_SERVICE_VERSION,\n description: 'NIP-5D notify NUB reference handler (stub)',\n };\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n switch (message.type) {\n case 'notify.send': {\n const m = message as NotifySendMessage;\n options.onSend?.(windowId, m);\n const result: NotifySendResultMessage = {\n type: 'notify.send.result',\n id: m.id,\n notificationId: gen(),\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'notify.dismiss':\n case 'notify.badge':\n case 'notify.channel.register':\n // state to mutate; host apps with real backends override.\n return;\n\n case 'notify.permission.request': {\n const m = message as NotifyPermissionRequestMessage;\n const result: NotifyPermissionResultMessage = {\n type: 'notify.permission.result',\n id: m.id,\n granted: defaultGrant,\n };\n send(result as NappletMessage);\n return;\n }\n\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown notify method: ${message.type}`,\n } as NappletMessage);\n }\n }\n },\n\n onWindowDestroyed(_windowId: string): void {\n // No per-window state in the stub. Host apps with real backends\n // override to clean up pending notifications / channel registrations.\n },\n };\n}\n","/**\n * theme-service.ts — NIP-5D theme NUB reference service.\n *\n * Handles the single napplet->shell request type from @napplet/nub/theme:\n * - `theme.get` -> `theme.get.result { theme }` (current theme)\n *\n * Exposes a host-facing `publishTheme(theme)` handle that:\n * 1. Replaces the service's internal current theme.\n * 2. Invokes `options.onBroadcast(envelope)` synchronously with a\n * `theme.changed` envelope so the shell adapter (Plan 13-02) can\n * fan-out the push to every registered napplet.\n * 3. Returns the envelope so callers can use it directly if they prefer.\n *\n * The default theme values are centralized here and mirrored in the runtime's\n * fallback path (`packages/runtime/src/runtime.ts`) — runtime does NOT import\n * from @kehto/services because services depends on runtime (one-way only).\n *\n * Host apps replace this via `runtime.registerService('theme', realHandler)`\n * when real CSS injection / storage / dark-mode logic is needed.\n *\n * @example\n * ```ts\n * import { createThemeService } from '@kehto/services';\n *\n * const theme = createThemeService({\n * onBroadcast: (envelope) => broadcastToAllNapplets(envelope),\n * });\n * runtime.registerService('theme', theme.handler);\n *\n * // Later, when the user flips dark/light mode:\n * theme.publishTheme(newTheme);\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from\n// @napplet/core v0.2.0+ (napplet phase-81). Re-exported from\n// @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n Theme,\n ThemeChangedMessage,\n ThemeGetResultMessage,\n} from '@napplet/nub/theme/types';\n\n/** Theme service version — follows semver. */\nconst THEME_SERVICE_VERSION = '1.0.0';\n\n/**\n * Canonical default theme values.\n *\n * Must stay synchronized with the runtime fallback constant at\n * `packages/runtime/src/runtime.ts` (THEME_FALLBACK_DEFAULT). The runtime\n * keeps a local copy instead of importing from @kehto/services to avoid a\n * runtime->services dependency (services depends on runtime; one-way).\n */\nconst DEFAULT_THEME: Theme = {\n colors: {\n background: '#0a0a0a',\n text: '#e0e0e0',\n primary: '#7aa2f7',\n },\n // fonts, background, title intentionally undefined — all optional per\n // @napplet/nub/theme Theme interface.\n};\n\n/**\n * Configuration for `createThemeService`.\n *\n * @example\n * ```ts\n * const theme = createThemeService({\n * initialTheme: { colors: { background: '#fff', text: '#000', primary: '#00f' } },\n * onBroadcast: (envelope) => shellBridge.broadcastToAll(envelope),\n * });\n * ```\n */\nexport interface ThemeServiceOptions {\n /**\n * Override the default theme payload. If omitted, the service starts with\n * the canonical defaults (`#0a0a0a / #e0e0e0 / #7aa2f7`).\n */\n initialTheme?: Theme;\n\n /**\n * Called synchronously from `publishTheme(theme)` with a `theme.changed`\n * envelope. Intended for the shell adapter (Plan 13-02) to fan-out the\n * push to every registered napplet via the runtime's sendToNapplet\n * primitive.\n *\n * Keep this callback shape framework-agnostic — the service does NOT\n * import any shell / browser APIs.\n */\n onBroadcast?: (envelope: ThemeChangedMessage) => void;\n}\n\n/**\n * A theme service bundle — the ServiceHandler that handles `theme.*`\n * envelopes, the host-facing `publishTheme(theme)` handle for theme-change\n * broadcasts, and a `getCurrentTheme()` accessor for host-side reads.\n */\nexport interface ThemeService {\n /** Register this with the runtime via `runtime.registerService('theme', handler)`. */\n handler: ServiceHandler;\n\n /**\n * Publish a theme-change to the shell adapter. Updates the service's\n * internal current theme, invokes `options.onBroadcast` with a\n * `theme.changed` envelope, and returns the envelope.\n *\n * @param theme - The new theme payload\n * @returns A `theme.changed` envelope (same one passed to onBroadcast)\n */\n publishTheme: (theme: Theme) => ThemeChangedMessage;\n\n /** Return the current theme. Equivalent to the payload a napplet's `theme.get` would receive. */\n getCurrentTheme: () => Theme;\n}\n\n/**\n * Create a theme service that handles the NIP-5D `theme.*` NUB.\n *\n * Answers `theme.get` with the current theme (default or\n * `options.initialTheme`). Exposes `publishTheme(theme)` for the host app to\n * broadcast theme changes to every registered napplet — the shell adapter\n * (Plan 13-02) wires `onBroadcast` to `runtime.sendToNapplet` fan-out.\n *\n * @param options - Optional service configuration (see ThemeServiceOptions)\n * @returns A ThemeService bundle ready for `runtime.registerService('theme', service.handler)`\n *\n * @example\n * ```ts\n * import { createThemeService } from '@kehto/services';\n *\n * const theme = createThemeService();\n * runtime.registerService('theme', theme.handler);\n * theme.publishTheme({ colors: { background: '#fff', text: '#000', primary: '#00f' } });\n * ```\n */\nexport function createThemeService(options: ThemeServiceOptions = {}): ThemeService {\n let currentTheme: Theme = options.initialTheme ?? DEFAULT_THEME;\n\n const descriptor: ServiceDescriptor = {\n name: 'theme',\n version: THEME_SERVICE_VERSION,\n description: 'NIP-5D theme NUB reference handler',\n };\n\n const handler: ServiceHandler = {\n descriptor,\n\n handleMessage(\n _windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n\n if (message.type === 'theme.get') {\n const result: ThemeGetResultMessage = {\n type: 'theme.get.result',\n id,\n theme: currentTheme,\n };\n send(result as NappletMessage);\n return;\n }\n\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown theme method: ${message.type}`,\n } as NappletMessage);\n },\n\n // Theme service has no per-window state to clean up.\n onWindowDestroyed(_windowId: string): void {\n /* no-op */\n },\n };\n\n function publishTheme(theme: Theme): ThemeChangedMessage {\n currentTheme = theme;\n const envelope: ThemeChangedMessage = { type: 'theme.changed', theme };\n options.onBroadcast?.(envelope);\n return envelope;\n }\n\n function getCurrentTheme(): Theme {\n return currentTheme;\n }\n\n return { handler, publishTheme, getCurrentTheme };\n}\n","/**\n * config-service.ts — NUB-CONFIG reference service (9th NUB domain, v1.7 Phase 39).\n *\n * Shell-side reference implementation for the canonical NUB-CONFIG wire\n * protocol (`@napplet/nub/config`, published at `^0.3.0`). Handles the full\n * 8-message discriminated union: 5 napplet→shell request types + 3\n * shell→napplet result/push types.\n *\n * ──────────────────────────── SCOPE BOUNDARY (CONFIG-04) ─────────────────────────\n * NUB-CONFIG is **shell-managed per-napplet configuration**. Napplets observe\n * values via `config.get` (one-shot) or `config.subscribe` (snapshot + live\n * push). The shell is the **sole writer** — there is intentionally **NO**\n * `config.set` wire message. Napplets cannot mutate configuration values;\n * the shell owns persistence and the update flow.\n *\n * Do NOT use this service as a general key-value store. NUB-STORAGE\n * (`state:read` / `state:write`) remains the general KV surface. Using\n * NUB-CONFIG to store e.g. `{ lastScrollPosition: 420 }` is an anti-pattern\n * (H-07 in PITFALLS.md) — such state belongs in NUB-STORAGE.\n * ──────────────────────────────────────────────────────────────────────────────────\n *\n * Host integration: provide `getValues()` returning the current\n * `ConfigValues` snapshot. Call the returned `publishValues(newValues)`\n * whenever the configuration changes — the service fans the new snapshot\n * out to every napplet that has an active `config.subscribe`.\n *\n * Optional: provide `registerSchema` to accept napplet-declared schemas at\n * runtime (the ref impl does a minimal shape check using the Core Subset\n * validator; use `ajv` in host impls that need strict draft-07 conformance).\n * Provide `openSettings` to open a shell-side UI for the napplet (no\n * response envelope — fire-and-forget UI hook).\n *\n * @example\n * ```ts\n * import { createConfigService } from '@kehto/services';\n *\n * const configFixtures = { theme: 'dark', density: 'compact', recentSearches: [] };\n * const config = createConfigService({\n * getValues: () => ({ ...configFixtures }),\n * });\n * runtime.registerService('config', config.handler);\n *\n * // Later, when shell-side values change:\n * configFixtures.theme = 'light';\n * config.publishValues({ ...configFixtures });\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n ConfigGetMessage,\n ConfigRegisterSchemaMessage,\n ConfigOpenSettingsMessage,\n ConfigValues,\n ConfigValuesMessage,\n ConfigRegisterSchemaResultMessage,\n ConfigSchemaErrorCode,\n NappletConfigSchema,\n} from '@napplet/nub/config/types';\n\n/** Config service version — follows semver. */\nconst CONFIG_SERVICE_VERSION = '1.0.0';\n\n/**\n * Shape returned by a successful `registerSchema` result (ok=true) or a\n * rejection (ok=false + code + error). Mirrors the wire envelope fields.\n */\nexport type ConfigSchemaValidation =\n | { ok: true }\n | { ok: false; code: ConfigSchemaErrorCode; error: string };\n\n/**\n * Configuration options for `createConfigService` (options-as-bridge\n * per v1.6 Decision 18).\n *\n * @example\n * ```ts\n * const config = createConfigService({\n * getValues: () => ({ theme: 'dark', density: 'compact' }),\n * openSettings: (windowId, section) => showSettingsPanel(windowId, section),\n * });\n * ```\n */\nexport interface ConfigServiceOptions {\n /**\n * Returns the current configuration values snapshot.\n * Called on every `config.get` and at every `config.subscribe` initial push.\n * Implementations should return a fresh object (not a mutable reference).\n */\n getValues(): ConfigValues;\n\n /**\n * Optional: receive notification when a napplet subscribes to config updates.\n * Fire-and-forget — the service tracks the subscription internally regardless.\n */\n onSubscribe?: (windowId: string) => void;\n\n /**\n * Optional: receive notification when a napplet unsubscribes.\n */\n onUnsubscribe?: (windowId: string) => void;\n\n /**\n * Optional: validate and store a napplet-provided schema.\n *\n * If omitted, the ref impl runs its own Core Subset check (hand-coded\n * validator; 30-50 lines) and returns ok/reject. Hosts that need strict\n * draft-07 conformance should provide an ajv-backed implementation.\n *\n * Return shape mirrors `config.registerSchema.result` wire envelope\n * (minus the `id` — the dispatch layer correlates).\n */\n registerSchema?: (\n windowId: string,\n schema: NappletConfigSchema,\n version: number | undefined,\n ) => ConfigSchemaValidation;\n\n /**\n * Optional: open the shell-side settings UI for this napplet.\n * Fire-and-forget — no response envelope per the wire spec.\n * If omitted, `config.openSettings` is silently dropped (D10 allows\n * the config-demo napplet to function without a settings UI).\n */\n openSettings?: (windowId: string, section: string | undefined) => void;\n}\n\n/**\n * NUB-CONFIG reference service bundle — `handler` to register with the\n * runtime, `publishValues` for the host app to push updates live to all\n * subscribed napplets.\n */\nexport interface ConfigService {\n /** Register this with the runtime via `runtime.registerService('config', handler)`. */\n handler: ServiceHandler;\n\n /**\n * Broadcast a new values snapshot to every napplet with an active\n * `config.subscribe`. Each subscriber receives a `config.values` envelope\n * with no `id` (push form per wire spec — absence of `id` distinguishes\n * push from correlated `config.get` response).\n *\n * @param values - The new configuration snapshot (full object, not a diff)\n */\n publishValues(values: ConfigValues): void;\n}\n\n/**\n * Minimal JSON Schema validator covering the NUB-CONFIG Core Subset:\n * type: object / string / number / boolean / array, required[], default, properties.\n *\n * Explicitly rejects: $ref, pattern, oneOf/anyOf/allOf/not, if/then/else.\n * Returns { ok: true } on shape sanity, otherwise an error code per the\n * canonical ConfigSchemaErrorCode union.\n *\n * Host apps that need strict draft-07 conformance should supply a custom\n * `registerSchema` callback backed by ajv@8.\n */\nfunction validateCoreSubset(schema: unknown): ConfigSchemaValidation {\n if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {\n return { ok: false, code: 'invalid-schema', error: 'schema root must be an object' };\n }\n const s = schema as Record<string, unknown>;\n\n // Reject forbidden keywords (NUB-CONFIG Core Subset limits per spec).\n if ('$ref' in s) {\n return { ok: false, code: 'ref-not-allowed', error: '$ref is not permitted in the Core Subset' };\n }\n if ('pattern' in s) {\n return {\n ok: false,\n code: 'pattern-not-allowed',\n error: 'pattern is not permitted in the Core Subset',\n };\n }\n if ('oneOf' in s || 'anyOf' in s || 'allOf' in s || 'not' in s) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: 'oneOf/anyOf/allOf/not are not permitted in the Core Subset',\n };\n }\n if ('if' in s || 'then' in s || 'else' in s) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: 'if/then/else are not permitted in the Core Subset',\n };\n }\n if (s.type !== 'object') {\n return { ok: false, code: 'invalid-schema', error: 'schema root must have type: \"object\"' };\n }\n\n // Shallow properties check: each declared property must use a supported type.\n const props = s.properties;\n if (props !== undefined && (typeof props !== 'object' || props === null)) {\n return { ok: false, code: 'invalid-schema', error: 'properties must be an object' };\n }\n if (props) {\n for (const [key, val] of Object.entries(props as Record<string, unknown>)) {\n if (typeof val !== 'object' || val === null) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: `property \"${key}\" must be an object schema`,\n };\n }\n const pv = val as Record<string, unknown>;\n const ALLOWED_TYPES = new Set(['string', 'number', 'boolean', 'array', 'object']);\n if (pv.type !== undefined && !ALLOWED_TYPES.has(pv.type as string)) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: `property \"${key}\" must have type: string|number|boolean|array|object`,\n };\n }\n }\n }\n\n return { ok: true };\n}\n\n/**\n * Create a NUB-CONFIG reference service.\n *\n * Shell-writes, napplet-reads. Handles the full `@napplet/nub/config` wire\n * protocol: `config.get` (correlated snapshot), `config.subscribe` /\n * `config.unsubscribe` (live push stream), `config.registerSchema` (optional\n * schema registration + Core Subset validation), `config.openSettings`\n * (optional UI deep-link, fire-and-forget).\n *\n * Returns a `ConfigService` bundle: `{ handler, publishValues }`.\n * Register `handler` with the runtime; call `publishValues(newValues)` from\n * the shell whenever config state changes.\n *\n * @param options - Host-supplied implementation hooks (options-as-bridge,\n * v1.6 Decision 18). `getValues` is required; all other fields are optional.\n * @returns A ConfigService bundle.\n *\n * @see ConfigServiceOptions for the options shape.\n * @see packages/services/src/theme-service.ts for the sibling pattern.\n * @see SCOPE BOUNDARY comment at the top of this file re: NUB-STORAGE separation.\n *\n * @example\n * ```ts\n * import { createConfigService } from '@kehto/services';\n *\n * const config = createConfigService({\n * getValues: () => ({ theme: 'dark', density: 'compact' }),\n * openSettings: (windowId, section) => openSettingsUI(section),\n * });\n * runtime.registerService('config', config.handler);\n *\n * // Push a live update to all subscribers:\n * config.publishValues({ theme: 'light', density: 'compact' });\n * ```\n */\nexport function createConfigService(options: ConfigServiceOptions): ConfigService {\n /**\n * Per-window subscriber set. Maps windowId → the send callback captured at\n * `config.subscribe` time. `publishValues` fans out to every entry.\n */\n const subscribers = new Map<string, (msg: NappletMessage) => void>();\n\n const descriptor: ServiceDescriptor = {\n name: 'config',\n version: CONFIG_SERVICE_VERSION,\n description: 'NUB-CONFIG reference service — shell-writes, napplet-reads configuration',\n };\n\n const handler: ServiceHandler = {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'config.get': {\n const m = message as ConfigGetMessage;\n const reply: ConfigValuesMessage = {\n type: 'config.values',\n id: m.id,\n values: options.getValues(),\n };\n send(reply as NappletMessage);\n return;\n }\n\n case 'config.subscribe': {\n // Capture the send callback so publishValues can fan pushes out.\n subscribers.set(windowId, send);\n // Immediate initial snapshot push — no `id` (push form per wire spec).\n const push: ConfigValuesMessage = {\n type: 'config.values',\n values: options.getValues(),\n };\n send(push as NappletMessage);\n options.onSubscribe?.(windowId);\n return;\n }\n\n case 'config.unsubscribe': {\n subscribers.delete(windowId);\n options.onUnsubscribe?.(windowId);\n return;\n }\n\n case 'config.registerSchema': {\n const m = message as ConfigRegisterSchemaMessage;\n // Delegate to host-supplied validator if present; otherwise use\n // the built-in Core Subset hand-coded validator (D12).\n const validation: ConfigSchemaValidation = options.registerSchema\n ? options.registerSchema(windowId, m.schema, m.version)\n : validateCoreSubset(m.schema);\n\n const result: ConfigRegisterSchemaResultMessage = validation.ok\n ? { type: 'config.registerSchema.result', id: m.id, ok: true }\n : {\n type: 'config.registerSchema.result',\n id: m.id,\n ok: false,\n code: validation.code,\n error: validation.error,\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'config.openSettings': {\n const m = message as ConfigOpenSettingsMessage;\n // Silently dropped if openSettings hook not provided (D10).\n options.openSettings?.(windowId, m.section);\n return;\n }\n\n default:\n // Unknown config.* message — silently ignored per NIP-5D.\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n // A napplet iframe was destroyed — drop any active subscription so we\n // don't retain the stale send callback in the subscribers map.\n subscribers.delete(windowId);\n },\n };\n\n /**\n * Broadcast a new config values snapshot to every subscribed napplet.\n * Each subscriber receives a `config.values` push envelope (no `id` —\n * absence of `id` distinguishes a push from a correlated `config.get`\n * response per the NUB-CONFIG wire spec).\n */\n function publishValues(values: ConfigValues): void {\n const envelope: ConfigValuesMessage = {\n type: 'config.values',\n values,\n };\n for (const send of subscribers.values()) {\n try {\n send(envelope as NappletMessage);\n } catch {\n // Subscriber's send callback threw (e.g., iframe gone without\n // onWindowDestroyed firing yet). Best-effort — drop silently.\n }\n }\n }\n\n return { handler, publishValues };\n}\n","/**\n * resource-service.ts — NUB-RESOURCE reference service (10th NUB domain, v1.7 Phase 40).\n *\n * Shell-side reference implementation for the canonical NUB-RESOURCE wire\n * protocol (`internal-resource.ts` in @kehto/shell/src/types; kehto-internal\n * model per PROJECT.md Decision #31 — diverges from upstream `@napplet/nub/\n * resource` in field names + error vocabulary). Handles the canonical\n * 4-message protocol:\n * Inbound: resource.bytes, resource.cancel\n * Outbound: resource.bytes.result, resource.bytes.error\n *\n * ──────────────────────── SCOPE BOUNDARY (RESOURCE-01) ────────────────────────\n * NUB-RESOURCE is an **authenticated fetch proxy** — read-only, atomic.\n *\n * This service is NOT responsible for:\n * - Streaming / chunked responses (host-app concern)\n * - Response caching / conditional requests (host-app concern)\n * - Upload / POST body construction (NUB-RESOURCE v1.7 is read-only)\n * - Redirect limits, MIME sniffing, SVG rasterization (host-fetch concern)\n * - Private-IP blocking, SSRF mitigation (host-provided-fetch responsibility)\n *\n * These belong to the host-app's `fetch` implementation per D7 and\n * SHELL-RESOURCE-POLICY.md (Phase 40 Plan 40-03). Kehto ships a reference\n * service; production hardening is the host app's concern.\n * ──────────────────────────────────────────────────────────────────────────────\n *\n * Host integration: provide `fetch`, `isOriginGranted`, `getConnectGrants`,\n * and `resolveIdentity`. ALL FOUR are required from day one (H-03 prevention).\n *\n * @example\n * ```ts\n * import { createResourceService } from '@kehto/services';\n *\n * const resourceSvc = createResourceService({\n * fetch: (url, init) => globalThis.fetch(url, init),\n * isOriginGranted: (origin, grants) => grants.includes(origin),\n * getConnectGrants: (dTag, hash) => connectStore.getOrigins(dTag, hash),\n * resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,\n * });\n * runtime.registerService('resource', resourceSvc);\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\n\n/** Resource service version — follows semver. */\nconst RESOURCE_SERVICE_VERSION = '1.0.0';\n\n/**\n * Options for `createResourceService` (options-as-bridge per v1.6 Decision 18).\n *\n * ALL FOUR fields are required. The factory throws at construction if any is\n * missing — H-03 prevention: the grants source (`getConnectGrants`) MUST be\n * wired from day one so there is no window where resource requests bypass the\n * grant check.\n *\n * @see PITFALLS.md:228 (H-03) — grants-source coupling must be present at construction\n */\nexport interface ResourceServiceOptions {\n /**\n * Host-supplied fetch implementation. Receives the URL, a partial init\n * (method, headers, signal), and must return a `Response`-compatible promise.\n *\n * The host's `fetch` is the ONLY place to implement redirect limits, MIME\n * sniffing, SVG rasterization, private-IP / SSRF blocking, etc.\n * This service does NOT filter: it proxies transparently.\n *\n * @param url - The URL from the resource.bytes request\n * @param init - Method, headers (from napplet), and an AbortSignal\n */\n fetch(\n url: string,\n init: { method?: string; headers?: Record<string, string>; signal: AbortSignal }\n ): Promise<Response>;\n\n /**\n * Returns true if `origin` is present in `grants` (the list returned by\n * `getConnectGrants` for the napplet's dTag + aggregateHash).\n *\n * The reference implementation is simply `grants.includes(origin)`. Host apps\n * may provide normalized-origin comparison if needed.\n *\n * @param origin - Parsed origin of the requested URL (scheme + host + port)\n * @param grants - Readonly list from getConnectGrants for this napplet identity\n */\n isOriginGranted(origin: string, grants: readonly string[]): boolean;\n\n /**\n * Returns the list of allowed fetch origins for the given napplet identity.\n * Called on every `resource.bytes` request — must be synchronous and fast.\n *\n * Typically wraps `connectStore.getOrigins(dTag, aggregateHash)` from\n * @kehto/shell.\n *\n * H-03 prevention: REQUIRED from day one — factory throws on construction\n * if omitted.\n *\n * @param dTag - The napplet's d-tag (from session registry)\n * @param aggregateHash - The napplet's aggregate hash (from session registry)\n */\n getConnectGrants(dTag: string, aggregateHash: string): readonly string[];\n\n /**\n * Resolve a windowId to the napplet's identity (dTag + aggregateHash).\n * Returns null if the window is not in the session registry.\n *\n * Typically wraps `sessionRegistry.getEntryByWindowId(windowId)`.\n *\n * @param windowId - The iframe window identifier\n */\n resolveIdentity(windowId: string): { dTag: string; aggregateHash: string } | null;\n}\n\n/**\n * Type alias for the ServiceHandler returned by `createResourceService`.\n * Exported for host apps that need to type-annotate the handler reference.\n */\nexport type ResourceService = ServiceHandler;\n\n/**\n * Convert an ArrayBuffer to base64 string, safe for both browser and Node.\n * Chunked in 0x8000-byte slices to avoid `String.fromCharCode(...largeArray)`\n * stack overflow on large responses.\n */\nfunction arrayBufferToBase64(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf);\n const CHUNK = 0x8000; // 32 KB\n let binary = '';\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n}\n\ninterface ResourceRequestState {\n inFlight: Map<string, { controller: AbortController; windowId: string }>;\n perWindow: Map<string, Set<string>>;\n}\n\nfunction assertResourceOptions(options: ResourceServiceOptions): void {\n if (\n typeof options?.fetch !== 'function' ||\n typeof options?.isOriginGranted !== 'function' ||\n typeof options?.getConnectGrants !== 'function' ||\n typeof options?.resolveIdentity !== 'function'\n ) {\n throw new Error(\n '[RESOURCE-01 / H-03] createResourceService requires {fetch, isOriginGranted, getConnectGrants, resolveIdentity} ' +\n '— all four options are required from day one. ' +\n 'The grants source (getConnectGrants) MUST be wired at construction time to prevent unguarded fetch proxying.',\n );\n }\n}\n\nfunction trackRequest(\n state: ResourceRequestState,\n requestId: string,\n windowId: string,\n controller: AbortController,\n): void {\n state.inFlight.set(requestId, { controller, windowId });\n if (!state.perWindow.has(windowId)) {\n state.perWindow.set(windowId, new Set());\n }\n state.perWindow.get(windowId)!.add(requestId);\n}\n\nfunction untrackRequest(state: ResourceRequestState, requestId: string): void {\n const entry = state.inFlight.get(requestId);\n if (entry) {\n state.inFlight.delete(requestId);\n state.perWindow.get(entry.windowId)?.delete(requestId);\n }\n}\n\nfunction sendResourceError(\n send: (m: NappletMessage) => void,\n requestId: string,\n code: 'denied' | 'invalid-url' | 'canceled' | 'network-error',\n message: string,\n): void {\n send({\n type: 'resource.bytes.error',\n requestId,\n code,\n message,\n } as NappletMessage);\n}\n\nfunction parseResourceUrl(send: (m: NappletMessage) => void, requestId: string, url: string): URL | null {\n try {\n return new URL(url);\n } catch {\n sendResourceError(send, requestId, 'invalid-url', `invalid URL: ${url}`);\n return null;\n }\n}\n\nfunction collectResponseHeaders(response: Response): Record<string, string> {\n const headers: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n headers[key] = value;\n });\n return headers;\n}\n\nasync function handleBytes(\n options: ResourceServiceOptions,\n state: ResourceRequestState,\n windowId: string,\n msg: { requestId: string; url: string; init?: { method?: string; headers?: Readonly<Record<string, string>> } },\n send: (m: NappletMessage) => void,\n): Promise<void> {\n const { requestId, url, init } = msg;\n const identity = options.resolveIdentity(windowId);\n if (!identity) {\n sendResourceError(send, requestId, 'denied', 'napplet identity not resolvable');\n return;\n }\n\n const parsedUrl = parseResourceUrl(send, requestId, url);\n if (!parsedUrl) return;\n\n const origin = parsedUrl.origin;\n const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);\n if (!options.isOriginGranted(origin, grants)) {\n sendResourceError(send, requestId, 'denied', `origin ${origin} not granted`);\n return;\n }\n\n const controller = new AbortController();\n trackRequest(state, requestId, windowId, controller);\n\n try {\n const response = await options.fetch(url, {\n method: init?.method,\n headers: init?.headers ? { ...init.headers } : undefined,\n signal: controller.signal,\n });\n const buffer = await response.arrayBuffer();\n send({\n type: 'resource.bytes.result',\n requestId,\n status: response.status,\n headers: collectResponseHeaders(response),\n bodyBase64: arrayBufferToBase64(buffer),\n } as NappletMessage);\n } catch (err: unknown) {\n const isAbort =\n controller.signal.aborted ||\n (err instanceof Error && (err.name === 'AbortError' || err.name === 'DOMException'));\n sendResourceError(\n send,\n requestId,\n isAbort ? 'canceled' : 'network-error',\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n untrackRequest(state, requestId);\n }\n}\n\nfunction handleCancel(state: ResourceRequestState, requestId: string): void {\n const entry = state.inFlight.get(requestId);\n if (entry) {\n entry.controller.abort();\n }\n}\n\nfunction destroyWindowRequests(state: ResourceRequestState, windowId: string): void {\n const requestIds = state.perWindow.get(windowId);\n if (!requestIds) return;\n for (const requestId of requestIds) {\n const entry = state.inFlight.get(requestId);\n if (entry) {\n entry.controller.abort();\n state.inFlight.delete(requestId);\n }\n }\n state.perWindow.delete(windowId);\n}\n\n/**\n * Create a NUB-RESOURCE reference service.\n *\n * Implements canonical 4-message protocol: `resource.bytes` (napplet → shell\n * fetch request), `resource.cancel` (napplet → shell in-flight cancellation),\n * `resource.bytes.result` (shell → napplet success), `resource.bytes.error`\n * (shell → napplet failure/denial/cancel).\n *\n * On-construction guard (H-03 prevention): all four options are validated at\n * factory call time. If any is missing, the factory throws immediately with a\n * message containing `[RESOURCE-01 / H-03]` so misconfigured shell apps fail\n * loudly at startup rather than silently at first dispatch.\n *\n * Returns a `ServiceHandler` (no `publishValues`-style surface — resource has\n * no shell-initiated push beyond the response/error path).\n *\n * @param options - REQUIRED: fetch, isOriginGranted, getConnectGrants, resolveIdentity\n * @returns ServiceHandler to register via `runtime.registerService('resource', handler)`\n *\n * @example\n * ```ts\n * import { createResourceService } from '@kehto/services';\n *\n * const svc = createResourceService({\n * fetch: (url, init) => globalThis.fetch(url, init),\n * isOriginGranted: (origin, grants) => grants.includes(origin),\n * getConnectGrants: (dTag, hash) => connectStore.getOrigins(dTag, hash),\n * resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,\n * });\n * runtime.registerService('resource', svc);\n * ```\n */\nexport function createResourceService(options: ResourceServiceOptions): ResourceService {\n assertResourceOptions(options);\n const state: ResourceRequestState = {\n inFlight: new Map<string, { controller: AbortController; windowId: string }>(),\n perWindow: new Map<string, Set<string>>(),\n };\n\n const descriptor: ServiceDescriptor = {\n name: 'resource',\n version: RESOURCE_SERVICE_VERSION,\n description:\n 'NUB-RESOURCE reference service — shell-proxied authenticated fetch (RESOURCE-01..06)',\n };\n\n const handler: ServiceHandler = {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'resource.bytes': {\n const m = message as NappletMessage & {\n requestId: string;\n url: string;\n init?: { method?: string; headers?: Readonly<Record<string, string>> };\n };\n handleBytes(options, state, windowId, m, send).catch(() => { /* errors surface via send() */ });\n return;\n }\n\n case 'resource.cancel': {\n const m = message as NappletMessage & { requestId: string };\n handleCancel(state, m.requestId);\n return;\n }\n\n default:\n // Unknown resource.* message — silently ignored per NIP-5D.\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n destroyWindowRequests(state, windowId);\n },\n };\n\n return handler;\n}\n","/**\n * cvm-service.ts — NAP-CVM (ContextVM bridge) reference service.\n *\n * Shell-side handler for the NAP-CVM wire protocol. It is a pure envelope\n * router: it validates `cvm.*` envelopes, delegates the actual ContextVM /\n * MCP-over-Nostr work to an injected {@link CvmTransport}, and posts the\n * correlated `*.result` (echoing the request `id`) back to the napplet.\n *\n * The transport is injected (options-as-bridge) so this service has no Nostr\n * dependency and is fully unit-testable. A concrete ContextVM transport ships\n * separately at `@kehto/services/cvm-nostr-transport`.\n *\n * ──────────────────────────── Responsibilities ────────────────────────────\n * Inbound: cvm.discover, cvm.request, cvm.close\n * Outbound: cvm.discover.result, cvm.request.result, cvm.close.result,\n * cvm.event (server-pushed MCP notifications)\n *\n * MCP-level errors are returned inside `request.result.message.error`;\n * transport/shell-policy failures are returned in the envelope `error` field.\n *\n * `cvm.event` is fanned out to every window that holds an active session with\n * the originating server (a window opens a session by issuing a `cvm.request`\n * and closes it via `cvm.close` or window teardown).\n *\n * @example\n * ```ts\n * import { createCvmService } from '@kehto/services';\n * import { createNostrCvmTransport } from '@kehto/services/cvm-nostr-transport';\n *\n * const transport = createNostrCvmTransport({ defaultRelays: ['wss://relay.contextvm.org'] });\n * runtime.registerService('cvm', createCvmService({ transport }));\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n CvmDiscoverQuery,\n CvmRequestOptions,\n CvmServer,\n CvmServerRef,\n McpMessage,\n} from './cvm-types.js';\n\n/** CVM service version — follows semver. */\nconst CVM_SERVICE_VERSION = '1.0.0';\n\n/**\n * Abstract ContextVM transport. Implementors own Nostr relay access, signing,\n * encryption (CEP-4 gift wrap), JSON-RPC correlation, and MCP initialization.\n */\nexport interface CvmTransport {\n /** Resolve public ContextVM server announcements matching the query. */\n discover(query?: CvmDiscoverQuery): Promise<CvmServer[]>;\n /** Send a raw MCP message to a server and resolve with the MCP response. */\n request(server: CvmServerRef, message: McpMessage, options?: CvmRequestOptions): Promise<McpMessage>;\n /** Release any session state held for a server (subscriptions, init cache). */\n close(server: CvmServerRef): Promise<void>;\n /**\n * Subscribe to server-pushed MCP messages not correlated to a single\n * request (e.g. notifications). Returns a handle whose `close()` detaches.\n */\n onEvent(handler: (server: CvmServerRef, message: McpMessage) => void): { close(): void };\n}\n\n/** Options for {@link createCvmService}. */\nexport interface CvmServiceOptions {\n /** The ContextVM transport the shell uses to reach servers. Required. */\n transport: CvmTransport;\n}\n\n/** The created CVM service, exposing the handler plus a disposal hook. */\nexport interface CvmService extends ServiceHandler {\n /** Detach the transport event subscription. Idempotent. */\n dispose(): void;\n}\n\ntype Send = (msg: NappletMessage) => void;\n\nconst CVM_DESCRIPTOR: ServiceDescriptor = {\n name: 'cvm',\n version: CVM_SERVICE_VERSION,\n description: 'NAP-CVM ContextVM bridge — MCP over Nostr',\n};\n\n/**\n * Create the NAP-CVM service handler.\n *\n * @param options - Must provide a {@link CvmTransport}.\n * @returns A {@link CvmService} (a `ServiceHandler` with a `dispose()` hook).\n * @throws If `options.transport` is missing.\n */\nexport function createCvmService(options: CvmServiceOptions): CvmService {\n if (!options || typeof options.transport !== 'object' || options.transport === null) {\n throw new Error('createCvmService: options.transport is required');\n }\n const { transport } = options;\n\n // Per-window send callbacks, captured at request time for cvm.event fan-out.\n const sendByWindow = new Map<string, Send>();\n // serverPubkey -> set of windowIds with an active session.\n const windowsByServer = new Map<string, Set<string>>();\n\n function openSession(windowId: string, server: CvmServerRef, send: Send): void {\n sendByWindow.set(windowId, send);\n let windows = windowsByServer.get(server.pubkey);\n if (!windows) {\n windows = new Set<string>();\n windowsByServer.set(server.pubkey, windows);\n }\n windows.add(windowId);\n }\n\n function closeSession(windowId: string, serverPubkey: string): void {\n const windows = windowsByServer.get(serverPubkey);\n if (windows) {\n windows.delete(windowId);\n if (windows.size === 0) windowsByServer.delete(serverPubkey);\n }\n }\n\n const eventSub = transport.onEvent((server, message) => {\n const windows = windowsByServer.get(server.pubkey);\n if (!windows) return;\n for (const windowId of windows) {\n const send = sendByWindow.get(windowId);\n send?.({ type: 'cvm.event', server, message } as NappletMessage);\n }\n });\n\n function handleDiscover(msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; query?: CvmDiscoverQuery };\n const id = m.id ?? '';\n void transport\n .discover(m.query)\n .then((servers) => send({ type: 'cvm.discover.result', id, servers } as NappletMessage))\n .catch((err) =>\n send({ type: 'cvm.discover.result', id, servers: [], error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n function handleRequest(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & {\n id?: string;\n server?: CvmServerRef;\n message?: McpMessage;\n options?: CvmRequestOptions;\n };\n const id = m.id ?? '';\n if (!m.server || typeof m.server.pubkey !== 'string' || m.server.pubkey.length === 0) {\n send({ type: 'cvm.request.result', id, error: 'server not found' } as NappletMessage);\n return;\n }\n if (!m.message || typeof m.message !== 'object') {\n send({ type: 'cvm.request.result', id, error: 'unsupported method' } as NappletMessage);\n return;\n }\n openSession(windowId, m.server, send);\n void transport\n .request(m.server, m.message, m.options)\n .then((message) => send({ type: 'cvm.request.result', id, message } as NappletMessage))\n .catch((err) =>\n send({ type: 'cvm.request.result', id, error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n function handleClose(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; server?: CvmServerRef };\n const id = m.id ?? '';\n if (!m.server || typeof m.server.pubkey !== 'string') {\n send({ type: 'cvm.close.result', id, error: 'server not found' } as NappletMessage);\n return;\n }\n closeSession(windowId, m.server.pubkey);\n void transport\n .close(m.server)\n .then(() => send({ type: 'cvm.close.result', id } as NappletMessage))\n .catch((err) => send({ type: 'cvm.close.result', id, error: toErrorMessage(err) } as NappletMessage));\n }\n\n return {\n descriptor: CVM_DESCRIPTOR,\n handleMessage(windowId: string, message: NappletMessage, send: Send): void {\n switch (message.type) {\n case 'cvm.discover':\n handleDiscover(message, send);\n return;\n case 'cvm.request':\n handleRequest(windowId, message, send);\n return;\n case 'cvm.close':\n handleClose(windowId, message, send);\n return;\n default:\n // Unknown cvm.* action — silently ignored (forward-compatible).\n return;\n }\n },\n onWindowDestroyed(windowId: string): void {\n sendByWindow.delete(windowId);\n for (const [pubkey, windows] of windowsByServer) {\n windows.delete(windowId);\n if (windows.size === 0) windowsByServer.delete(pubkey);\n }\n },\n dispose(): void {\n eventSub.close();\n },\n };\n}\n\nfunction toErrorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return 'cvm request failed';\n}\n"],"mappings":"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","names":["notify","result","deliver"]}
|
|
1
|
+
{"version":3,"sources":["../src/audio-service.ts","../src/notification-service.ts","../src/identity-service.ts","../src/relay-pool-service.ts","../src/cache-service.ts","../src/coordinated-relay.ts","../src/keys-service.ts","../src/browser-media-bridge.ts","../src/media-service.ts","../src/notify-service.ts","../src/theme-service.ts","../src/config-service.ts","../src/resource-service.ts","../src/outbox-service.ts","../src/relay-pool-outbox-router.ts","../src/upload-service.ts","../src/http-uploader.ts","../src/cvm-service.ts"],"sourcesContent":["/**\n * audio-service.ts — Audio source registry as a ServiceHandler.\n *\n * Tracks which napplet windows are producing audio. Shell hosts wire this\n * into the runtime via registerService('audio', createAudioService(opts)).\n * Browser-agnostic — no DOM, no window, no postMessage.\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type { AudioSource, AudioServiceOptions } from './types.js';\n\n/** Audio service version — follows semver. */\nconst AUDIO_SERVICE_VERSION = '1.0.0';\n\n/**\n * Create an audio service handler.\n *\n * The audio service is a state registry that tracks active audio sources\n * per napplet window. Napplets announce audio state via `audio:*` topic\n * events; the service tracks sources and can relay mute commands back.\n *\n * @param options - Optional configuration (onChange callback for UI updates)\n * @returns A ServiceHandler to register with the runtime\n *\n * @example\n * ```ts\n * import { createAudioService } from '@kehto/services';\n *\n * const audio = createAudioService({\n * onChange: (sources) => {\n * // Update UI with current audio sources\n * for (const [windowId, source] of sources) {\n * console.log(`${source.title} (${source.muted ? 'muted' : 'playing'})`);\n * }\n * },\n * });\n *\n * runtime.registerService('audio', audio);\n * ```\n */\nexport function createAudioService(options?: AudioServiceOptions): ServiceHandler {\n const sources = new Map<string, AudioSource>();\n const onChange = options?.onChange;\n\n function notify(): void {\n onChange?.(new Map(sources));\n }\n\n const descriptor: ServiceDescriptor = {\n name: 'audio',\n version: AUDIO_SERVICE_VERSION,\n description: 'Audio source registry — tracks active audio sources per napplet window',\n };\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n if (message.type !== 'ifc.emit') return;\n const ifcMessage = message as NappletMessage & { topic?: unknown; payload?: unknown };\n const topic = typeof ifcMessage.topic === 'string' ? ifcMessage.topic : undefined;\n if (!topic?.startsWith('audio:')) return;\n\n const action = topic.slice(6); // 'audio:'.length === 6\n const payload = ifcMessage.payload && typeof ifcMessage.payload === 'object'\n ? ifcMessage.payload as Record<string, unknown>\n : {};\n\n switch (action) {\n case 'register': {\n const nappletClass = typeof payload.nappletClass === 'string' ? payload.nappletClass : '';\n const title = typeof payload.title === 'string' ? payload.title : '';\n sources.set(windowId, { windowId, nappletClass, title, muted: false });\n notify();\n break;\n }\n\n case 'unregister': {\n if (sources.delete(windowId)) {\n notify();\n }\n break;\n }\n\n case 'state-changed': {\n const source = sources.get(windowId);\n if (!source) return;\n if (typeof payload.title === 'string') {\n source.title = payload.title;\n }\n notify();\n break;\n }\n\n case 'mute': {\n const targetWindowId = typeof payload.windowId === 'string'\n ? payload.windowId\n : windowId;\n const muted = payload.muted === true;\n\n const source = sources.get(targetWindowId);\n if (source) {\n source.muted = muted;\n notify();\n }\n\n send({ type: 'ifc.event', topic: 'napplet:audio-muted', payload: { muted } } as NappletMessage);\n break;\n }\n\n default:\n // Unknown audio action — ignore\n break;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n if (sources.delete(windowId)) {\n notify();\n }\n },\n };\n}\n","\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type { Notification, NotificationServiceOptions } from './types.js';\n\n/** Notification service version — follows semver. */\nconst NOTIFICATION_SERVICE_VERSION = '1.0.0';\n\n/** Default maximum notifications per window. */\nconst DEFAULT_MAX_PER_WINDOW = 100;\n\n/** Counter for generating unique notification IDs. */\nlet idCounter = 0;\n\n/**\n * Generate a unique notification ID.\n */\nfunction generateId(): string {\n idCounter++;\n return `notif-${Date.now()}-${idCounter}`;\n}\n\ninterface NotificationStore {\n notifications: Map<string, Notification[]>;\n onChange?: (notifications: Notification[]) => void;\n maxPerWindow: number;\n}\n\nfunction getAllNotifications(store: NotificationStore): Notification[] {\n const all: Notification[] = [];\n for (const windowNotifs of store.notifications.values()) {\n all.push(...windowNotifs);\n }\n return all;\n}\n\nfunction notify(store: NotificationStore): void {\n store.onChange?.(getAllNotifications(store));\n}\n\nfunction getWindowNotifications(store: NotificationStore, windowId: string): Notification[] {\n let list = store.notifications.get(windowId);\n if (!list) {\n list = [];\n store.notifications.set(windowId, list);\n }\n return list;\n}\n\nfunction enforceLimit(store: NotificationStore, list: Notification[]): void {\n while (list.length > store.maxPerWindow) {\n list.shift();\n }\n}\n\nfunction findById(store: NotificationStore, id: string): [string, Notification, number] | undefined {\n for (const [windowId, list] of store.notifications) {\n const index = list.findIndex((n) => n.id === id);\n if (index !== -1) {\n return [windowId, list[index], index];\n }\n }\n return undefined;\n}\n\nfunction createNotification(\n store: NotificationStore,\n windowId: string,\n title: string,\n body: string,\n): Notification {\n const notification: Notification = {\n id: generateId(),\n windowId,\n title,\n body,\n read: false,\n createdAt: Math.floor(Date.now() / 1000),\n };\n const list = getWindowNotifications(store, windowId);\n list.push(notification);\n enforceLimit(store, list);\n notify(store);\n return notification;\n}\n\nfunction dismissNotification(store: NotificationStore, id: string): void {\n const found = findById(store, id);\n if (!found) return;\n const [foundWindowId, , index] = found;\n const list = store.notifications.get(foundWindowId);\n if (!list) return;\n list.splice(index, 1);\n if (list.length === 0) store.notifications.delete(foundWindowId);\n notify(store);\n}\n\nfunction markNotificationRead(store: NotificationStore, id: string): void {\n const found = findById(store, id);\n if (!found) return;\n const [, notification] = found;\n if (!notification.read) {\n notification.read = true;\n notify(store);\n }\n}\n\nfunction handleNotifyEnvelope(\n store: NotificationStore,\n windowId: string,\n action: string,\n msg: NappletMessage & Record<string, unknown>,\n send: (msg: NappletMessage) => void,\n): void {\n switch (action) {\n case 'create': {\n const title = typeof msg.title === 'string' ? msg.title : '';\n const body = typeof msg.body === 'string' ? msg.body : '';\n const notification = createNotification(store, windowId, title, body);\n send({ type: 'notify.created', id: notification.id } as NappletMessage);\n break;\n }\n\n case 'dismiss': {\n const notifId = typeof msg.notificationId === 'string' ? msg.notificationId : '';\n if (notifId) dismissNotification(store, notifId);\n break;\n }\n\n case 'read': {\n const notifId = typeof msg.notificationId === 'string' ? msg.notificationId : '';\n if (notifId) markNotificationRead(store, notifId);\n break;\n }\n\n case 'list': {\n const windowNotifs = store.notifications.get(windowId) ?? [];\n send({ type: 'notify.listed', notifications: windowNotifs } as NappletMessage);\n break;\n }\n\n default:\n break;\n }\n}\n\nfunction handleIfcNotification(\n store: NotificationStore,\n windowId: string,\n action: string,\n payload: Record<string, unknown>,\n send: (msg: NappletMessage) => void,\n): void {\n switch (action) {\n case 'create': {\n const title = typeof payload.title === 'string' ? payload.title : '';\n const body = typeof payload.body === 'string' ? payload.body : '';\n const notification = createNotification(store, windowId, title, body);\n send({ type: 'ifc.event', topic: 'notifications:created', payload: { id: notification.id } } as NappletMessage);\n break;\n }\n\n case 'dismiss': {\n const id = typeof payload.id === 'string' ? payload.id : '';\n if (id) dismissNotification(store, id);\n break;\n }\n\n case 'read': {\n const id = typeof payload.id === 'string' ? payload.id : '';\n if (id) markNotificationRead(store, id);\n break;\n }\n\n case 'list': {\n const windowNotifs = store.notifications.get(windowId) ?? [];\n send({ type: 'ifc.event', topic: 'notifications:listed', payload: { notifications: windowNotifs } } as NappletMessage);\n break;\n }\n\n default:\n break;\n }\n}\n\n/**\n * Create a notification service handler.\n *\n * The notification service is a state registry that tracks notifications\n * per napplet window. Napplets create and manage notifications via\n * `notifications:*` topic events; the shell host controls presentation\n * via the onChange callback.\n *\n * @param options - Optional configuration (onChange callback, maxPerWindow limit)\n * @returns A ServiceHandler to register with the runtime\n *\n * @example\n * ```ts\n * import { createNotificationService } from '@kehto/services';\n *\n * const notifications = createNotificationService({\n * onChange: (list) => {\n * const unread = list.filter(n => !n.read);\n * updateBadge(unread.length);\n * },\n * maxPerWindow: 50,\n * });\n *\n * runtime.registerService('notifications', notifications);\n * ```\n */\nexport function createNotificationService(options?: NotificationServiceOptions): ServiceHandler {\n const store: NotificationStore = {\n notifications: new Map<string, Notification[]>(),\n onChange: options?.onChange,\n maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW,\n };\n\n const descriptor: ServiceDescriptor = {\n name: 'notifications',\n version: NOTIFICATION_SERVICE_VERSION,\n description: 'Notification state registry — tracks notifications per napplet window',\n };\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const msg = message as NappletMessage & Record<string, unknown>;\n\n if (message.type.startsWith('notify.')) {\n handleNotifyEnvelope(store, windowId, message.type.slice(7), msg, send);\n return;\n }\n\n if (message.type !== 'ifc.emit') return;\n const topic = msg.topic as string | undefined;\n if (!topic?.startsWith('notifications:')) return;\n\n const payload = ((msg.payload ?? {}) as Record<string, unknown>);\n handleIfcNotification(store, windowId, topic.slice(14), payload, send);\n },\n\n onWindowDestroyed(windowId: string): void {\n if (store.notifications.delete(windowId)) {\n notify(store);\n }\n },\n };\n}\n","/**\n * identity-service.ts — NIP-5D identity nub reference service.\n *\n * MIGRATION from signer-service (v1.1 -> v1.2):\n * - signer.getPublicKey -> identity.getPublicKey (same shell state)\n * - signer.getRelays -> identity.getRelays (same shell state)\n * - signer.signEvent -> DELETED (no napplet-visible path; shell signs\n * internally inside relay.publish)\n * - signer.nip04.encrypt/decrypt -> DELETED\n * - signer.nip44.encrypt/decrypt -> DELETED (shell encrypts internally\n * inside relay.publishEncrypted)\n * - identity.decrypt -> ADDED in v1.8 as a shell-mediated decrypt request\n * with class gate + typed error union.\n *\n * See REQUIREMENTS.md DEPS-03 (Phase 15 changelog).\n *\n * Handles 10 identity.* request types from @napplet/nub/identity. getPublicKey\n * and getRelays return real values sourced from hooks.auth.getSigner(); the\n * remaining 7 (getProfile/getFollows/getList/getZaps/getMutes/getBlocked/\n * getBadges) are stub-level; decrypt delegates to a host-supplied bridge.\n * Host apps plug real backends via runtime.registerService('identity', realHandler).\n */\n\nimport type { NappletMessage, NostrEvent } from '@napplet/core';\nimport type { ServiceHandler, Signer } from '@kehto/runtime';\nimport type {\n IdentityGetPublicKeyResultMessage,\n IdentityGetRelaysResultMessage,\n IdentityGetProfileResultMessage,\n IdentityGetFollowsResultMessage,\n IdentityGetListResultMessage,\n IdentityGetZapsResultMessage,\n IdentityGetMutesResultMessage,\n IdentityGetBlockedResultMessage,\n IdentityGetBadgesResultMessage,\n RelayPermission,\n} from '@napplet/nub/identity/types';\n\n/** Identity service version — follows semver. */\nconst IDENTITY_SERVICE_VERSION = '1.0.0';\n\ntype EncryptionMode = 'nip04' | 'nip44-direct' | 'nip17';\n\nexport type IdentityDecryptErrorCode =\n | 'class-forbidden'\n | 'signer-denied'\n | 'signer-unavailable'\n | 'decrypt-failed'\n | 'malformed-wrap'\n | 'impersonation'\n | 'unsupported-encryption'\n | 'policy-denied';\n\nexport interface Rumor {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n}\n\nexport interface IdentityDecryptMessage extends NappletMessage {\n type: 'identity.decrypt';\n id: string;\n event: NostrEvent;\n}\n\nexport interface IdentityDecryptResultMessage extends NappletMessage {\n type: 'identity.decrypt.result';\n id: string;\n rumor: Rumor;\n sender: string;\n}\n\nexport interface IdentityDecryptErrorMessage extends NappletMessage {\n type: 'identity.decrypt.error';\n id: string;\n error: IdentityDecryptErrorCode;\n}\n\nconst DECRYPT_ERROR_CODES: readonly IdentityDecryptErrorCode[] = [\n 'class-forbidden',\n 'signer-denied',\n 'signer-unavailable',\n 'decrypt-failed',\n 'malformed-wrap',\n 'impersonation',\n 'unsupported-encryption',\n 'policy-denied',\n];\n\nconst DECRYPT_ERROR_CODE_SET = new Set<string>(DECRYPT_ERROR_CODES);\n\nexport interface GiftWrapDecryptResult {\n seal: NostrEvent;\n rumor: Rumor;\n}\n\nexport interface HostDecryptBridge {\n nip04Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;\n nip44Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;\n unwrapGiftWrap(wrap: NostrEvent): Promise<GiftWrapDecryptResult>;\n}\n\nexport type VerifyEvent = (event: NostrEvent) => boolean | Promise<boolean>;\n\nfunction isDecryptErrorCode(value: unknown): value is IdentityDecryptErrorCode {\n return typeof value === 'string' && DECRYPT_ERROR_CODE_SET.has(value);\n}\n\nfunction normalizeDecryptError(error: unknown): IdentityDecryptErrorCode {\n if (isDecryptErrorCode(error)) return error;\n if (typeof error === 'object' && error !== null) {\n const candidate = error as { code?: unknown; error?: unknown; message?: unknown };\n if (isDecryptErrorCode(candidate.code)) return candidate.code;\n if (isDecryptErrorCode(candidate.error)) return candidate.error;\n if (isDecryptErrorCode(candidate.message)) return candidate.message;\n }\n return 'decrypt-failed';\n}\n\nfunction sendDecryptError(\n id: string,\n error: IdentityDecryptErrorCode,\n send: (msg: NappletMessage) => void,\n): void {\n const result: IdentityDecryptErrorMessage = {\n type: 'identity.decrypt.error',\n id,\n error,\n };\n send(result);\n}\n\nfunction isStringArrayArray(value: unknown): value is string[][] {\n return Array.isArray(value) && value.every(\n (tag) => Array.isArray(tag) && tag.every((part) => typeof part === 'string'),\n );\n}\n\nfunction isNostrEvent(value: unknown): value is NostrEvent {\n const event = value as Partial<NostrEvent> | null;\n return typeof event === 'object' &&\n event !== null &&\n typeof event.id === 'string' &&\n typeof event.pubkey === 'string' &&\n typeof event.created_at === 'number' &&\n typeof event.kind === 'number' &&\n isStringArrayArray(event.tags) &&\n typeof event.content === 'string' &&\n typeof event.sig === 'string';\n}\n\nfunction isRumor(value: unknown): value is Rumor {\n const rumor = value as Partial<Rumor> | null;\n return typeof rumor === 'object' &&\n rumor !== null &&\n typeof rumor.id === 'string' &&\n typeof rumor.pubkey === 'string' &&\n typeof rumor.created_at === 'number' &&\n typeof rumor.kind === 'number' &&\n isStringArrayArray(rumor.tags) &&\n typeof rumor.content === 'string';\n}\n\nfunction isGiftWrapDecryptResult(value: unknown): value is GiftWrapDecryptResult {\n const result = value as Partial<GiftWrapDecryptResult> | null;\n return typeof result === 'object' &&\n result !== null &&\n isNostrEvent(result.seal) &&\n isRumor(result.rumor);\n}\n\nfunction firstDecodedByte(content: string): number | null {\n const trimmed = content.trim();\n if (trimmed.length === 0) return null;\n const normalized = trimmed.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n try {\n const decoded = atob(padded);\n return decoded.length > 0 ? decoded.charCodeAt(0) : null;\n } catch {\n return null;\n }\n}\n\nfunction detectEncryptionMode(event: NostrEvent): EncryptionMode | null {\n if (event.kind === 4) return 'nip04';\n if (event.kind === 1059) return 'nip17';\n if (event.kind === 14 || firstDecodedByte(event.content) === 0x02) {\n return 'nip44-direct';\n }\n return null;\n}\n\nfunction rumorFromSignedEvent(event: NostrEvent, content: string): Rumor {\n return {\n id: event.id,\n pubkey: event.pubkey,\n kind: event.kind,\n tags: event.tags,\n created_at: event.created_at,\n content,\n };\n}\n\nasync function handleDecrypt(\n id: string,\n message: IdentityDecryptMessage,\n send: (msg: NappletMessage) => void,\n options: IdentityServiceOptions,\n): Promise<void> {\n const event = (message as IdentityDecryptMessage & { event?: unknown }).event;\n if (!isNostrEvent(event)) {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n\n const verifyEvent = options.verifyEvent ?? (() => true);\n let verified: boolean;\n try {\n verified = await Promise.resolve(verifyEvent(event));\n } catch {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n if (!verified) {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n\n const mode = detectEncryptionMode(event);\n if (!mode) {\n sendDecryptError(id, 'unsupported-encryption', send);\n return;\n }\n\n const decryptor = options.getDecryptor?.() ?? null;\n if (!decryptor) {\n sendDecryptError(id, 'signer-unavailable', send);\n return;\n }\n\n try {\n if (mode === 'nip04') {\n const plaintext = await decryptor.nip04Decrypt(event.pubkey, event.content);\n const result: IdentityDecryptResultMessage = {\n type: 'identity.decrypt.result',\n id,\n rumor: rumorFromSignedEvent(event, plaintext),\n sender: event.pubkey,\n };\n send(result);\n return;\n }\n\n if (mode === 'nip44-direct') {\n const plaintext = await decryptor.nip44Decrypt(event.pubkey, event.content);\n const result: IdentityDecryptResultMessage = {\n type: 'identity.decrypt.result',\n id,\n rumor: rumorFromSignedEvent(event, plaintext),\n sender: event.pubkey,\n };\n send(result);\n return;\n }\n\n const unwrapped = await decryptor.unwrapGiftWrap(event);\n if (!isGiftWrapDecryptResult(unwrapped)) {\n sendDecryptError(id, 'malformed-wrap', send);\n return;\n }\n if (unwrapped.seal.pubkey !== unwrapped.rumor.pubkey) {\n sendDecryptError(id, 'impersonation', send);\n return;\n }\n const result: IdentityDecryptResultMessage = {\n type: 'identity.decrypt.result',\n id,\n rumor: unwrapped.rumor,\n sender: unwrapped.seal.pubkey,\n };\n send(result);\n } catch (error) {\n sendDecryptError(id, normalizeDecryptError(error), send);\n }\n}\n\n/**\n * Options for creating the identity service.\n *\n * @example\n * ```ts\n * const identityService = createIdentityService({\n * getSigner: () => window.nostr ?? null,\n * });\n * runtime.registerService('identity', identityService);\n * ```\n */\nexport interface IdentityServiceOptions {\n /**\n * Return the NIP-07-compatible signer (or null) used to resolve\n * identity.getPublicKey / identity.getRelays. Called on every request —\n * availability can change dynamically.\n */\n getSigner: () => Signer | null;\n\n /**\n * Return the host decrypt bridge. Called only after outer event signature\n * verification and encryption-mode detection succeed. Null means decrypt is\n * unavailable while the rest of the identity service remains usable.\n */\n getDecryptor?: () => HostDecryptBridge | null;\n\n /**\n * Verify a received event before any decrypt attempt. Host shells should\n * wire this to their canonical Nostr event verifier; tests and old hosts\n * default to true for backward compatibility with the 9 read-only actions.\n */\n verifyEvent?: VerifyEvent;\n}\n\n/**\n * Create an identity service that handles NIP-5D identity.* envelope messages.\n *\n * Supports all 10 identity.* request types from @napplet/nub/identity. The two\n * read-only nostr-info queries (getPublicKey, getRelays) resolve through the\n * caller-supplied signer; the remaining 7 return default/empty payloads with\n * spec-correct envelope shapes so napplets always receive a result envelope.\n * identity.decrypt delegates to the host decrypt bridge.\n *\n * @param options - Identity service configuration (getSigner)\n * @returns A ServiceHandler ready for runtime.registerService('identity', handler)\n *\n * @example\n * ```ts\n * import { createIdentityService } from '@kehto/services';\n *\n * const identity = createIdentityService({\n * getSigner: () => mySignerAdapter,\n * });\n * runtime.registerService('identity', identity);\n * ```\n */\nexport function createIdentityService(options: IdentityServiceOptions): ServiceHandler {\n return {\n descriptor: {\n name: 'identity',\n version: IDENTITY_SERVICE_VERSION,\n description: 'NIP-5D identity NUB reference handler (9 read-only identity queries)',\n },\n\n handleMessage(\n _windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n\n function sendError(typeBase: string, error: string): void {\n send({ type: `${typeBase}.error`, id, error } as NappletMessage);\n }\n\n function sendSignerError(typeBase: string, fallback: string, err: unknown): void {\n sendError(typeBase, (err as Error)?.message ?? fallback);\n }\n\n const signer = options.getSigner();\n\n switch (message.type) {\n case 'identity.getPublicKey': {\n // Per NIP-5D spec comment \"Always succeeds\" — return empty pubkey when no signer is\n // configured rather than sending an error. The nub-identity shim's getPublicKey()\n // only handles 'identity.getPublicKey.result'; an error response hangs the Promise\n // indefinitely. Empty pubkey is the correct sentinel for \"no signer connected\".\n if (!signer) {\n const result: IdentityGetPublicKeyResultMessage = {\n type: 'identity.getPublicKey.result',\n id,\n pubkey: '',\n };\n send(result);\n return;\n }\n Promise.resolve(signer.getPublicKey?.())\n .then((pubkey) => {\n const result: IdentityGetPublicKeyResultMessage = {\n type: 'identity.getPublicKey.result',\n id,\n pubkey: (pubkey as string) ?? '',\n };\n send(result);\n })\n .catch((err: unknown) => sendSignerError('identity.getPublicKey', 'getPublicKey failed', err));\n return;\n }\n\n case 'identity.getRelays': {\n if (!signer) {\n sendError('identity.getRelays', 'no signer configured');\n return;\n }\n Promise.resolve(signer.getRelays?.() ?? {})\n .then((relays) => {\n const result: IdentityGetRelaysResultMessage = {\n type: 'identity.getRelays.result',\n id,\n relays: relays as Record<string, RelayPermission>,\n };\n send(result);\n })\n .catch((err: unknown) => sendSignerError('identity.getRelays', 'getRelays failed', err));\n return;\n }\n\n case 'identity.getProfile': {\n const result: IdentityGetProfileResultMessage = {\n type: 'identity.getProfile.result',\n id,\n profile: null,\n };\n send(result);\n return;\n }\n\n case 'identity.getFollows': {\n const result: IdentityGetFollowsResultMessage = {\n type: 'identity.getFollows.result',\n id,\n pubkeys: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getList': {\n const result: IdentityGetListResultMessage = {\n type: 'identity.getList.result',\n id,\n entries: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getZaps': {\n const result: IdentityGetZapsResultMessage = {\n type: 'identity.getZaps.result',\n id,\n zaps: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getMutes': {\n const result: IdentityGetMutesResultMessage = {\n type: 'identity.getMutes.result',\n id,\n pubkeys: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getBlocked': {\n const result: IdentityGetBlockedResultMessage = {\n type: 'identity.getBlocked.result',\n id,\n pubkeys: [],\n };\n send(result);\n return;\n }\n\n case 'identity.getBadges': {\n const result: IdentityGetBadgesResultMessage = {\n type: 'identity.getBadges.result',\n id,\n badges: [],\n };\n send(result);\n return;\n }\n\n case 'identity.decrypt': {\n void handleDecrypt(id, message as IdentityDecryptMessage, send, options);\n return;\n }\n\n default:\n sendError(message.type, `Unknown identity method: ${message.type}`);\n }\n },\n\n // Identity service has no per-window state to clean up.\n onWindowDestroyed(_windowId: string): void {\n /* no-op */\n },\n };\n}\n","/**\n * relay-pool-service.ts — Relay pool as a ServiceHandler.\n *\n * Wraps an existing relay pool implementation (subscribe, publish,\n * selectRelayTier, isAvailable) as a ServiceHandler that receives\n * relay NUB envelope messages and manages subscription lifecycle.\n *\n * Handles: relay.subscribe, relay.close, relay.publish, relay.publishEncrypted.\n *\n * Note on `relay.publishEncrypted`: the canonical napplet→shell path routes\n * through @kehto/runtime handleRelayMessage, which performs shell-internal\n * NIP-44/NIP-04 encryption via the signer, then synthesizes a relay.publish\n * envelope handed to this service. The publishEncrypted branch here is a\n * fallback for alternate wirings; by the time the service sees the\n * envelope, content MUST already be ciphertext (the service never encrypts\n * or decrypts).\n */\n\nimport type { NostrEvent, NostrFilter, NappletMessage } from '@napplet/core';\nimport type { ServiceHandler } from '@kehto/runtime';\n\n// Timer globals available in all JS runtimes\ndeclare function setTimeout(callback: () => void, ms: number): unknown;\ndeclare function clearTimeout(id: unknown): void;\n\n/** EOSE fallback timeout in milliseconds. */\nconst EOSE_FALLBACK_MS = 15_000;\n\n/**\n * Options for creating a relay pool service.\n *\n * @example\n * ```ts\n * const relayPoolService = createRelayPoolService({\n * subscribe: (filters, cb, urls) => myPool.subscribe(filters, cb, urls),\n * publish: (event) => myPool.publish(event),\n * selectRelayTier: (filters) => myPool.selectRelays(filters),\n * isAvailable: () => myPool.connected,\n * });\n * ```\n */\nexport interface RelayPoolServiceOptions {\n /**\n * Subscribe to events matching filters. Returns handle with unsubscribe().\n *\n * @param filters - NIP-01 filter objects\n * @param callback - Receives matching events or 'EOSE'\n * @param relayUrls - Optional relay URL hints\n * @returns Handle to cancel the subscription\n */\n subscribe(\n filters: NostrFilter[],\n callback: (item: NostrEvent | 'EOSE') => void,\n relayUrls?: string[],\n ): { unsubscribe(): void };\n\n /**\n * Publish an event to relays.\n *\n * @param event - The event to publish\n */\n publish(event: NostrEvent): void;\n\n /**\n * Select relay URLs appropriate for the given filters.\n *\n * @param filters - NIP-01 filter objects\n * @returns Array of relay URLs\n */\n selectRelayTier(filters: NostrFilter[]): string[];\n\n /**\n * Whether the relay pool is available and connected.\n *\n * @returns true if the relay pool can handle requests\n */\n isAvailable(): boolean;\n}\n\n/** Internal subscription tracking entry. */\ninterface TrackedSubscription {\n handle: { unsubscribe(): void };\n eoseTimer: unknown;\n}\n\ntype RelayServiceMessage = NappletMessage & {\n subId?: unknown;\n filters?: unknown;\n event?: unknown;\n};\n\n/**\n * Create a relay pool service that wraps an existing relay pool\n * implementation as a ServiceHandler.\n *\n * Handles relay.subscribe, relay.close, and relay.publish envelopes.\n * Tracks subscriptions per windowId:subId for lifecycle management.\n * Sets a 15-second EOSE fallback timer on each subscription.\n *\n * @param options - Relay pool implementation to wrap\n * @returns A ServiceHandler ready for runtime.registerService('relay', handler)\n *\n * @example\n * ```ts\n * import { createRelayPoolService } from '@kehto/services';\n *\n * const pool = createRelayPoolService({\n * subscribe: (f, cb, urls) => applesauce.subscribe(f, cb, urls),\n * publish: (e) => applesauce.publish(e),\n * selectRelayTier: (f) => applesauce.getRelays(f),\n * isAvailable: () => applesauce.connected,\n * });\n * runtime.registerService('relay', pool);\n * ```\n */\nexport function createRelayPoolService(options: RelayPoolServiceOptions): ServiceHandler {\n const tracked = new Map<string, TrackedSubscription>();\n\n return {\n descriptor: {\n name: 'relay-pool',\n version: '1.0.0',\n description: 'Relay pool subscription and publishing',\n },\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const relayMessage = message as RelayServiceMessage;\n if (message.type === 'relay.subscribe') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const filters = Array.isArray(relayMessage.filters)\n ? relayMessage.filters as NostrFilter[]\n : [];\n const subKey = `${windowId}:${subId}`;\n\n // Cancel existing subscription for this key if any\n const existing = tracked.get(subKey);\n if (existing) {\n existing.handle.unsubscribe();\n clearTimeout(existing.eoseTimer);\n tracked.delete(subKey);\n }\n\n if (!options.isAvailable()) {\n send({ type: 'relay.eose', subId } as NappletMessage);\n return;\n }\n\n const relayUrls = options.selectRelayTier(filters);\n let eoseSent = false;\n\n const eoseTimer = setTimeout(() => {\n if (!eoseSent) {\n eoseSent = true;\n send({ type: 'relay.eose', subId } as NappletMessage);\n }\n }, EOSE_FALLBACK_MS);\n\n const handle = options.subscribe(filters, (item) => {\n if (item === 'EOSE') {\n clearTimeout(eoseTimer);\n if (!eoseSent) {\n eoseSent = true;\n send({ type: 'relay.eose', subId } as NappletMessage);\n }\n return;\n }\n send({ type: 'relay.event', subId, event: item } as NappletMessage);\n }, relayUrls);\n\n tracked.set(subKey, { handle, eoseTimer });\n return;\n }\n\n if (message.type === 'relay.close') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const subKey = `${windowId}:${subId}`;\n const entry = tracked.get(subKey);\n if (entry) {\n entry.handle.unsubscribe();\n clearTimeout(entry.eoseTimer);\n tracked.delete(subKey);\n }\n return;\n }\n\n if (message.type === 'relay.publish') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (event && typeof event === 'object' && options.isAvailable()) {\n options.publish(event);\n }\n return;\n }\n\n if (message.type === 'relay.publishEncrypted') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (event && typeof event === 'object' && options.isAvailable()) {\n options.publish(event);\n }\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const prefix = `${windowId}:`;\n for (const [key, entry] of tracked) {\n if (key.startsWith(prefix)) {\n entry.handle.unsubscribe();\n clearTimeout(entry.eoseTimer);\n tracked.delete(key);\n }\n }\n },\n };\n}\n","\nimport type { NostrEvent, NostrFilter, NappletMessage } from '@napplet/core';\nimport type { ServiceHandler } from '@kehto/runtime';\n\ntype RelayServiceMessage = NappletMessage & {\n subId?: unknown;\n filters?: unknown;\n event?: unknown;\n};\n\n/**\n * Options for creating a cache service.\n *\n * @example\n * ```ts\n * const cacheService = createCacheService({\n * query: (filters) => myIndexedDB.query(filters),\n * store: (event) => myIndexedDB.store(event),\n * isAvailable: () => true,\n * });\n * ```\n */\nexport interface CacheServiceOptions {\n /**\n * Query cached events matching the given filters.\n *\n * @param filters - NIP-01 filter objects\n * @returns Promise resolving to matching cached events\n */\n query(filters: NostrFilter[]): Promise<NostrEvent[]>;\n\n /**\n * Store an event in cache. Best-effort, may silently fail.\n *\n * @param event - The event to store\n */\n store(event: NostrEvent): void;\n\n /**\n * Whether the cache is available.\n *\n * @returns true if the cache can handle requests\n */\n isAvailable(): boolean;\n}\n\n/**\n * @kehto/services cross-package naming-parity alias for {@link CacheServiceOptions}.\n *\n * `HostCacheBridge` matches the v1.4 `HostKeysBridge` / `HostMediaBridge`\n * convention — it is a pure type alias for `CacheServiceOptions`, NOT a new\n * type. Existing consumers of `CacheServiceOptions` continue to work\n * unchanged; new consumers may prefer `HostCacheBridge` for consistency\n * with the other Host*Bridge names in `@kehto/services`.\n *\n * Anti-feature note (PITFALLS.md M-02): `CacheServiceOptions` MUST remain\n * the primary export. This alias is additive; do not rename or delete\n * `CacheServiceOptions` when other Host*Bridge names eventually\n * stabilize.\n *\n * @example\n * ```ts\n * import type { HostCacheBridge } from '@kehto/services';\n * const cache: HostCacheBridge = {\n * query: (filters) => myIndexedDB.query(filters),\n * store: (event) => myIndexedDB.store(event),\n * isAvailable: () => true,\n * };\n * ```\n */\nexport type HostCacheBridge = CacheServiceOptions;\n\n/**\n * Create a cache service that wraps an existing cache implementation\n * as a ServiceHandler.\n *\n * Cache relay.subscribe subscriptions are one-shot — they query, deliver\n * results, send EOSE, and are done. No long-lived subscription tracking needed.\n * Cache query failures are best-effort: EOSE is sent even on failure.\n *\n * @param options - Cache implementation to wrap\n * @returns A ServiceHandler ready for runtime.registerService('cache', handler)\n *\n * @example\n * ```ts\n * import { createCacheService } from '@kehto/services';\n *\n * const cache = createCacheService({\n * query: (f) => workerRelay.query(f),\n * store: (e) => workerRelay.store(e),\n * isAvailable: () => workerRelay.ready,\n * });\n * runtime.registerService('cache', cache);\n * ```\n */\nexport function createCacheService(options: CacheServiceOptions): ServiceHandler {\n return {\n descriptor: {\n name: 'cache',\n version: '1.0.0',\n description: 'Local event cache (IndexedDB, worker relay, etc.)',\n },\n\n handleMessage(_windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const relayMessage = message as RelayServiceMessage;\n if (message.type === 'relay.subscribe') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const filters = Array.isArray(relayMessage.filters)\n ? relayMessage.filters as NostrFilter[]\n : [];\n\n if (!options.isAvailable()) {\n send({ type: 'relay.eose', subId } as NappletMessage);\n return;\n }\n\n options\n .query(filters)\n .then((events) => {\n for (const event of events) {\n send({ type: 'relay.event', subId, event } as NappletMessage);\n }\n send({ type: 'relay.eose', subId } as NappletMessage);\n })\n .catch(() => {\n // Cache query is best-effort — send EOSE even on failure\n send({ type: 'relay.eose', subId } as NappletMessage);\n });\n return;\n }\n\n if (message.type === 'relay.publish') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (event && typeof event === 'object' && options.isAvailable()) {\n try {\n options.store(event);\n } catch {\n /* Cache write is best-effort */\n }\n }\n return;\n }\n },\n\n // Cache has no per-window state to clean up\n onWindowDestroyed(_windowId: string): void {\n /* no-op */\n },\n };\n}\n","\nimport type { NostrEvent, NostrFilter, NappletMessage } from '@napplet/core';\nimport type { ServiceHandler } from '@kehto/runtime';\nimport type { RelayPoolServiceOptions } from './relay-pool-service.js';\nimport type { CacheServiceOptions } from './cache-service.js';\n\n// Timer globals available in all JS runtimes\ndeclare function setTimeout(callback: () => void, ms: number): unknown;\ndeclare function clearTimeout(id: unknown): void;\n\n/** Default EOSE fallback timeout. */\nconst DEFAULT_EOSE_TIMEOUT_MS = 15_000;\n\n/**\n * Options for creating a coordinated relay service.\n *\n * @example\n * ```ts\n * const relay = createCoordinatedRelay({\n * relayPool: {\n * subscribe: (f, cb, urls) => pool.subscribe(f, cb, urls),\n * publish: (e) => pool.publish(e),\n * selectRelayTier: (f) => pool.selectRelays(f),\n * isAvailable: () => pool.connected,\n * },\n * cache: {\n * query: (f) => db.query(f),\n * store: (e) => db.store(e),\n * isAvailable: () => db.ready,\n * },\n * });\n * runtime.registerService('relay', relay);\n * ```\n */\nexport interface CoordinatedRelayOptions {\n /**\n * Relay pool implementation.\n * Uses the same interface as RelayPoolServiceOptions.\n */\n relayPool: RelayPoolServiceOptions;\n\n /**\n * Local cache implementation.\n * Uses the same interface as CacheServiceOptions.\n */\n cache: CacheServiceOptions;\n\n /**\n * EOSE fallback timeout in milliseconds.\n * Sent if relay pool doesn't respond within this time.\n * Default: 15000 (15 seconds).\n */\n eoseTimeoutMs?: number;\n}\n\n/** Internal state for a tracked subscription. */\ninterface TrackedSub {\n seenIds: Set<string>;\n cacheEose: boolean;\n relayEose: boolean;\n eoseSent: boolean;\n eoseTimer: unknown;\n relayHandle: { unsubscribe(): void } | null;\n}\n\ntype RelayServiceMessage = NappletMessage & {\n subId?: unknown;\n filters?: unknown;\n event?: unknown;\n};\n\n/**\n * Create a coordinated relay service that combines relay pool and cache\n * into a single ServiceHandler with dedup and unified EOSE.\n *\n * On relay.subscribe: queries cache first, then subscribes to relay pool.\n * Events are deduplicated by ID. EOSE is sent after both sources complete.\n * On relay.publish: publishes to relay pool and stores in cache.\n * On relay.close: cancels relay pool subscription.\n *\n * @param options - Relay pool and cache implementations to coordinate\n * @returns A ServiceHandler ready for runtime.registerService('relay', handler)\n *\n * @example\n * ```ts\n * import { createCoordinatedRelay } from '@kehto/services';\n *\n * const relay = createCoordinatedRelay({ relayPool: myPool, cache: myCache });\n * runtime.registerService('relay', relay);\n * ```\n */\nexport function createCoordinatedRelay(options: CoordinatedRelayOptions): ServiceHandler {\n const timeoutMs = options.eoseTimeoutMs ?? DEFAULT_EOSE_TIMEOUT_MS;\n const subs = new Map<string, TrackedSub>();\n\n function maybeSendEose(subKey: string, subId: string, send: (msg: NappletMessage) => void): void {\n const sub = subs.get(subKey);\n if (!sub || sub.eoseSent) return;\n if (sub.cacheEose && sub.relayEose) {\n sub.eoseSent = true;\n clearTimeout(sub.eoseTimer);\n send({ type: 'relay.eose', subId } as NappletMessage);\n }\n }\n\n return {\n descriptor: {\n name: 'relay',\n version: '1.0.0',\n description: 'Coordinated relay pool + cache with dedup and unified EOSE',\n },\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n const relayMessage = message as RelayServiceMessage;\n if (message.type === 'relay.subscribe') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const filters = Array.isArray(relayMessage.filters)\n ? relayMessage.filters as NostrFilter[]\n : [];\n const subKey = `${windowId}:${subId}`;\n\n // Cancel existing subscription for this key\n const existing = subs.get(subKey);\n if (existing) {\n existing.relayHandle?.unsubscribe();\n clearTimeout(existing.eoseTimer);\n subs.delete(subKey);\n }\n\n const cacheAvailable = options.cache.isAvailable();\n const relayAvailable = options.relayPool.isAvailable();\n\n // Neither source available — send EOSE immediately\n if (!cacheAvailable && !relayAvailable) {\n send({ type: 'relay.eose', subId } as NappletMessage);\n return;\n }\n\n const tracked: TrackedSub = {\n seenIds: new Set(),\n cacheEose: !cacheAvailable, // mark done if cache not available\n relayEose: !relayAvailable, // mark done if relay not available\n eoseSent: false,\n eoseTimer: null as unknown,\n relayHandle: null,\n };\n subs.set(subKey, tracked);\n\n function deliver(event: NostrEvent): void {\n if (tracked.seenIds.has(event.id)) return;\n tracked.seenIds.add(event.id);\n if (subs.has(subKey)) send({ type: 'relay.event', subId, event } as NappletMessage);\n }\n\n // Query cache (async)\n if (cacheAvailable) {\n options.cache\n .query(filters)\n .then((events) => {\n for (const event of events) deliver(event);\n tracked.cacheEose = true;\n maybeSendEose(subKey, subId, send);\n })\n .catch(() => {\n // Cache query is best-effort\n tracked.cacheEose = true;\n maybeSendEose(subKey, subId, send);\n });\n }\n\n // Subscribe to relay pool\n if (relayAvailable) {\n tracked.eoseTimer = setTimeout(() => {\n if (!tracked.eoseSent) {\n tracked.relayEose = true;\n maybeSendEose(subKey, subId, send);\n }\n }, timeoutMs);\n\n const relayUrls = options.relayPool.selectRelayTier(filters);\n tracked.relayHandle = options.relayPool.subscribe(filters, (item) => {\n if (item === 'EOSE') {\n clearTimeout(tracked.eoseTimer);\n tracked.relayEose = true;\n maybeSendEose(subKey, subId, send);\n return;\n }\n deliver(item);\n // Store relay events in cache\n if (cacheAvailable) {\n try { options.cache.store(item); } catch { /* best-effort */ }\n }\n }, relayUrls);\n }\n return;\n }\n\n if (message.type === 'relay.close') {\n const subId = relayMessage.subId;\n if (typeof subId !== 'string') return;\n const subKey = `${windowId}:${subId}`;\n const entry = subs.get(subKey);\n if (entry) {\n entry.relayHandle?.unsubscribe();\n clearTimeout(entry.eoseTimer);\n subs.delete(subKey);\n }\n return;\n }\n\n if (message.type === 'relay.publish') {\n const event = relayMessage.event as NostrEvent | undefined;\n if (!event || typeof event !== 'object') return;\n // Publish to relay pool\n if (options.relayPool.isAvailable()) {\n options.relayPool.publish(event);\n }\n if (options.cache.isAvailable()) {\n try { options.cache.store(event); } catch { /* best-effort */ }\n }\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const prefix = `${windowId}:`;\n for (const [key, entry] of subs) {\n if (key.startsWith(prefix)) {\n entry.relayHandle?.unsubscribe();\n clearTimeout(entry.eoseTimer);\n subs.delete(key);\n }\n }\n },\n };\n}\n","/**\n * keys-service.ts — NIP-5D keys NUB reference document-level listener implementation.\n *\n * Handles the 3 napplet -> shell request types from @napplet/nub/keys:\n * - keys.forward -> invokes options.onForward (hotkey passthrough, fire-and-forget)\n * - keys.registerAction -> parses action.defaultKey into a chord spec, stores the\n * subscription in an in-memory registry keyed by actionId,\n * tracks windowId ownership so onWindowDestroyed can auto-\n * unsubscribe, and echoes { actionId, binding } as .result\n * - keys.unregisterAction -> removes the subscription; fire-and-forget (no envelope)\n *\n * Real listener: on service construction the handler attaches a single\n * `keydown` listener to `options.listenerTarget` (default: `document`). Each\n * keydown is matched against the chord-subscription registry; matches invoke\n * `options.onForward` with the DOM-shape payload AND push a canonical\n * `keys.action` envelope back to the owning napplet via the per-window `send`\n * callback captured at `keys.registerAction` time. Subscriptions persist\n * across messages; `onWindowDestroyed(windowId)` drops all subscriptions owned\n * by the destroyed window as well as its cached `send` handle.\n *\n * On each document keydown matching a registered action, the service\n * additionally emits a `keys.action` envelope to the action's owning napplet\n * via the per-window `send` callback — this is the canonical @napplet/nub/keys\n * surface the SDK's `keys.onAction(...)` helper consumes. The shape is a\n * superset of `KeysActionMessage`: `{ type, actionId, chord }` where `chord`\n * is the parsed `{ ctrl, alt, shift, meta, key }` struct (extension field;\n * base shape unchanged, downstream SDKs that only read `{ type, actionId }`\n * ignore `chord` silently).\n *\n * Field-name translation: @napplet/nub/keys uses the compact\n * { ctrl, alt, shift, meta } form on the wire; the shell's HotkeyHooks\n * (packages/shell/src/types.ts) expects the DOM-compatible\n * { ctrlKey, altKey, shiftKey, metaKey } form. This service performs the\n * translation so callers of `onForward` see the DOM shape.\n *\n * Shell -> napplet push envelopes `keys.bindings` remain the shell-side keys\n * forwarder's responsibility (DRIFT-SHELL-06, tracked under Plan 12-11 /\n * future phase); `keys.action` is emitted here per Plan 26-01.\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n KeysForwardMessage,\n KeysRegisterActionMessage,\n KeysRegisterActionResultMessage,\n KeysActionMessage,\n} from '@napplet/nub/keys/types';\n\n/**\n * Minimal structural subset of the DOM `KeyboardEvent` exposed to\n * `HostKeysBridge` subscribe callbacks. DOM `KeyboardEvent` satisfies this\n * structurally with no adapter needed. OS-bridge impls (Electron, Tauri —\n * out of v1.4 scope) synthesize this from native key events.\n */\nexport interface HostKeyEvent {\n key: string;\n code: string;\n ctrlKey: boolean;\n altKey: boolean;\n shiftKey: boolean;\n metaKey: boolean;\n /** True for OS autorepeat; the service filters these by default. */\n repeat?: boolean;\n}\n\n/**\n * Host-bridge contract for pluggable keyboard backends.\n *\n * The browser reference implementation (the default {@link createKeysService}\n * behaviour when `hostBridge` is omitted) registers a `document`-level keydown\n * listener and satisfies this interface structurally — it exposes\n * `subscribe(chord, callback) => unsubscribe` semantics but omits the two\n * OS-level optional fields (browsers cannot register global hotkeys without\n * privileged APIs).\n *\n * Host apps (Electron, Tauri) implement this interface in their own code and\n * pass it via `createKeysService({ hostBridge: myBridge })` — the service\n * then delegates subscription lifecycle to the bridge and remains browser-free.\n *\n * Reference implementations for Electron / Tauri are explicitly out of v1.4\n * scope and live in host-app examples / follow-up milestones (see\n * REQUIREMENTS.md \"Future Requirements\").\n *\n * @example\n * ```ts\n * // Host-app pseudocode (Electron main-process relay):\n * const electronBridge: HostKeysBridge = {\n * subscribe(chord, cb) {\n * const handle = globalShortcut.register(chord, () => cb({ key: '', code: '', ctrlKey: false, altKey: false, shiftKey: false, metaKey: false }));\n * return () => globalShortcut.unregister(chord);\n * },\n * registerGlobalHotkey: (chord) => globalShortcut.register(chord, () => {}),\n * onGlobalHotkey: (cb) => globalHotkeyBridge.on('global-hotkey', (_, chord) => cb(chord)),\n * };\n *\n * const keys = createKeysService({ hostBridge: electronBridge });\n * runtime.registerService('keys', keys);\n * ```\n */\nexport interface HostKeysBridge {\n /**\n * Subscribe a callback to a chord. Returns an unsubscribe handle.\n *\n * Implementations MUST:\n * - invoke `callback` exactly once per matching chord event (implementations\n * are responsible for any OS-autorepeat filtering)\n * - invoke `callback` synchronously during the event delivery\n * - accept the string chord format documented by @napplet/nub/keys\n * (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)\n */\n subscribe(chord: string, callback: (event: KeyboardEvent | HostKeyEvent) => void): () => void;\n\n /**\n * Optional: register an OS-level global hotkey (works even when the host\n * window is not focused). Returns true on success, false if the chord\n * cannot be registered (e.g. already claimed by another app).\n *\n * Omitted by the browser reference implementation — browsers cannot\n * register OS-level global hotkeys without privileged APIs. Electron\n * (`globalShortcut`) and Tauri (`GlobalShortcut`) provide this.\n */\n registerGlobalHotkey?(chord: string): boolean;\n\n /**\n * Optional: subscribe to OS-level global hotkey events (regardless of\n * focus). Returns an unsubscribe handle.\n *\n * Omitted by the browser reference implementation. See\n * {@link HostKeysBridge.registerGlobalHotkey}.\n */\n onGlobalHotkey?(callback: (chord: string) => void): () => void;\n}\n\n/** Keys service version — follows semver. */\nconst KEYS_SERVICE_VERSION = '1.2.0';\n\n/**\n * Parsed chord struct — internal, never on the wire. The napplet-facing API\n * (and the `action.defaultKey` field) is a string like `\"Ctrl+Shift+K\"`;\n * `parseChord` lowers those strings into this struct for efficient matching\n * against `KeyboardEvent` modifier flags.\n */\ninterface ChordSpec {\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n meta: boolean;\n /** Normalized uppercase single character or DOM key name (e.g. 'K', 'Enter'). */\n key: string;\n}\n\n/** Registry entry — maps a registered actionId back to its owning window + chord. */\ninterface ActionEntry {\n chord: ChordSpec;\n /** Original chord string, preserved for the .result `binding` field. */\n chordString: string;\n windowId: string;\n}\n\n/**\n * Options for creating a keys service via createKeysService().\n *\n * @example\n * ```ts\n * const keys = createKeysService({\n * onForward: (event) => {\n * // event has DOM-compatible field names: ctrlKey, altKey, etc.\n * hotkeyDispatcher.dispatch(event);\n * },\n * });\n * ```\n */\nexport interface KeysServiceOptions {\n /**\n * Called on `keys.forward` (napplet-forwarded chord) AND on document keydown\n * matching a registered action. Receives the DOM-style field names\n * (ctrlKey/altKey/shiftKey/metaKey) to match the shell's HotkeyHooks\n * contract. The service translates from the wire shape\n * ({ ctrl, alt, shift, meta }) before invoking this callback.\n */\n onForward?: (event: {\n key: string;\n code: string;\n ctrlKey: boolean;\n altKey: boolean;\n shiftKey: boolean;\n metaKey: boolean;\n }) => void;\n /**\n * EventTarget to attach the default keydown listener to. Defaults to the\n * global `document` when running in a DOM environment, else an isolated\n * `new EventTarget()` (SSR / Node-test safe). Passing a fresh\n * `new EventTarget()` is useful for unit tests. Mirrors the pattern used\n * by `@kehto/shell`'s `createKeysForwarder`.\n *\n * Ignored when `hostBridge` is provided — the bridge owns subscription\n * lifecycle and no document listener is attached.\n */\n listenerTarget?: EventTarget;\n /**\n * Optional pluggable backend for chord subscription. When provided, the\n * service delegates `keys.registerAction` → `bridge.subscribe(chord, cb)`\n * and stores the returned unsubscribe handle keyed on `actionId`. The\n * default document-listener path is NOT attached when `hostBridge` is\n * provided — the bridge is authoritative. See {@link HostKeysBridge}.\n */\n hostBridge?: HostKeysBridge;\n /**\n * Optional set of shell-reserved chords. Strings in the `@napplet/nub/keys`\n * wire format (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`). When a napplet sends\n * `keys.forward` with a chord matching this set — or when a document\n * keydown matches a reserved chord — the service invokes `onForward`\n * (or the `hostBridge`-registered handler) but suppresses the\n * `keys.action` push to any napplet that registered the same chord via\n * `keys.registerAction`. Precedence: reserved > registered. The shell\n * WANTS the forward — that is why it reserved the chord.\n *\n * Normalized once at service construction via the same chord parser used\n * for `action.defaultKey` — `'Ctrl+K'` / `'Control+k'` / `'ctrl+K'` all\n * match. Static; no runtime mutation. For dynamic reservation see the\n * deferred `HostKeysBridge.reserveAbsolute(chords)` extension.\n *\n * @example\n * ```ts\n * const keys = createKeysService({\n * reservedChords: ['Ctrl+Alt+T', 'Super+Space'],\n * onForward: (event) => wm.dispatch(event),\n * });\n * ```\n */\n reservedChords?: ReadonlyArray<string>;\n}\n\n/** Modifier-token aliases accepted by `parseChord` (case-insensitive). */\nconst MODIFIER_ALIASES: Record<string, keyof Pick<ChordSpec, 'ctrl' | 'alt' | 'shift' | 'meta'>> = {\n ctrl: 'ctrl',\n control: 'ctrl',\n alt: 'alt',\n option: 'alt',\n shift: 'shift',\n meta: 'meta',\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n super: 'meta',\n};\n\n/**\n * Parse a chord string into a `ChordSpec`. Modifier tokens are case-insensitive\n * and recognize common aliases (Cmd/Command/Win/Super → meta, Control → ctrl,\n * Option → alt). Single-character keys are normalized to uppercase so chord\n * matching is case-insensitive; multi-character DOM key names (`Enter`,\n * `ArrowUp`, `F4`) preserve their original casing.\n *\n * Examples:\n * parseChord('Ctrl+Shift+K') → { ctrl: true, alt: false, shift: true, meta: false, key: 'K' }\n * parseChord('ctrl+s') → { ctrl: true, alt: false, shift: false, meta: false, key: 'S' }\n * parseChord('Cmd+P') → { ctrl: false, alt: false, shift: false, meta: true, key: 'P' }\n * parseChord('K') → { ctrl: false, alt: false, shift: false, meta: false, key: 'K' }\n * parseChord('Ctrl++') → { ctrl: true, alt: false, shift: false, meta: false, key: '+' }\n *\n * @throws Error('empty chord') when the input is the empty string.\n * @throws Error(`unknown modifier: ${tok}`) when a non-final token isn't a recognized modifier.\n * @throws Error(`empty key in chord: ${chord}`) when the final token is empty/whitespace.\n */\nfunction parseChord(chord: string): ChordSpec {\n if (chord.length === 0) throw new Error('empty chord');\n const parts = chord.split('+');\n const out: ChordSpec = { ctrl: false, alt: false, shift: false, meta: false, key: '' };\n // The final token is always the key — even if it is literally '+' (chord like 'Ctrl++').\n // All preceding tokens must be modifiers.\n for (let i = 0; i < parts.length - 1; i++) {\n const tok = parts[i].trim().toLowerCase();\n if (tok.length === 0) continue; // tolerate stray whitespace\n const slot = MODIFIER_ALIASES[tok];\n if (!slot) throw new Error(`unknown modifier: ${parts[i]}`);\n out[slot] = true;\n }\n const keyTok = parts[parts.length - 1].trim();\n if (keyTok.length === 0) throw new Error(`empty key in chord: ${chord}`);\n // Single characters normalize to uppercase for case-insensitive comparison;\n // multi-character DOM key names (Enter, ArrowUp, F4) preserve their original casing.\n out.key = keyTok.length === 1 ? keyTok.toUpperCase() : keyTok;\n return out;\n}\n\n/**\n * Create a keys service handler.\n *\n * Attaches a single `keydown` listener to `options.listenerTarget`\n * (default `document`). Matching chord subscriptions invoke `options.onForward`\n * with a DOM-shape payload AND push a `keys.action` envelope back to the\n * owning napplet via the per-window `send` callback captured at\n * `keys.registerAction` time. Returns a `ServiceHandler` augmented with a\n * `destroy()` method that detaches the listener and clears all registries.\n *\n * @param options - Optional configuration (onForward callback, listenerTarget)\n * @returns A ServiceHandler (with `destroy()`) to register with the runtime\n *\n * @example\n * ```ts\n * import { createKeysService } from '@kehto/services';\n *\n * const keys = createKeysService({\n * onForward: (event) => shellHotkeyDispatcher.execute(event),\n * });\n *\n * runtime.registerService('keys', keys);\n * // Later, on shell teardown:\n * keys.destroy();\n * ```\n */\nexport function createKeysService(\n options: KeysServiceOptions = {},\n): ServiceHandler & { destroy(): void } {\n const descriptor: ServiceDescriptor = {\n name: 'keys',\n version: KEYS_SERVICE_VERSION,\n description: options.hostBridge\n ? 'NIP-5D keys NUB reference handler (host-bridge delegated)'\n : 'NIP-5D keys NUB reference handler (document-level chord listener)',\n };\n\n function chordSpecKey(spec: {\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n meta: boolean;\n key: string;\n }): string {\n return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;\n }\n const reservedChordKeys: Set<string> = new Set();\n if (options.reservedChords) {\n for (const chordStr of options.reservedChords) {\n // parseChord throws on malformed input — let it bubble up at construction\n // so misconfigured shells fail loudly at boot, not silently at runtime.\n reservedChordKeys.add(chordSpecKey(parseChord(chordStr)));\n }\n }\n // Canonicalize a wire-shape keys.forward payload into the same key.\n function forwardKey(m: {\n key: string;\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n meta: boolean;\n }): string {\n const k = m.key.length === 1 ? m.key.toUpperCase() : m.key;\n return `${m.ctrl}|${m.alt}|${m.shift}|${m.meta}|${k}`;\n }\n function forwardPayload(m: KeysForwardMessage): Omit<HostKeyEvent, 'repeat'> {\n return {\n key: m.key,\n code: m.code,\n ctrlKey: m.ctrl,\n altKey: m.alt,\n shiftKey: m.shift,\n metaKey: m.meta,\n };\n }\n // Canonicalize a DOM KeyboardEvent into the same key (for Branch B keydown listener).\n function eventKey(ev: KeyboardEvent): string {\n const k = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;\n return `${ev.ctrlKey}|${ev.altKey}|${ev.shiftKey}|${ev.metaKey}|${k}`;\n }\n\n if (options.hostBridge) {\n const bridge = options.hostBridge;\n // windowId → Set<actionId> — parallels Branch B for scoped cleanup.\n const bridgeWindowActions = new Map<string, Set<string>>();\n // actionId → unsubscribe handle returned from bridge.subscribe.\n const unsubscribeHandles = new Map<string, () => void>();\n\n return {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'keys.forward': {\n // Legacy napplet-forwarded path still works identically — preserves wire contract.\n // Phase 33 / KEYS-04-05: reserved chords take precedence. The Branch-A\n // handler never dispatches keys.action to napplets on forward (bridge\n // owns chord → napplet routing via its own subscribe callback), so\n // reservation here is observationally identical to the base case —\n // but we compute and check the reservation explicitly to document\n // the contract for future edits and keep both branches uniform.\n const m = message as KeysForwardMessage;\n const reserved = reservedChordKeys.has(forwardKey(m));\n options.onForward?.(forwardPayload(m));\n if (reserved) {\n return;\n }\n return;\n }\n\n case 'keys.registerAction': {\n const m = message as KeysRegisterActionMessage;\n if (m.action.defaultKey) {\n try {\n const unsubscribe = bridge.subscribe(m.action.defaultKey, (ev) => {\n // Normalize either KeyboardEvent or HostKeyEvent to the DOM-shape onForward payload.\n const e = ev as KeyboardEvent | HostKeyEvent;\n // Bridges may not filter autorepeat — we do, matching Branch B semantics.\n if ('repeat' in e && e.repeat) return;\n options.onForward?.({\n key: e.key,\n code: e.code,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n shiftKey: e.shiftKey,\n metaKey: e.metaKey,\n });\n // Canonical shell→napplet push: emit keys.action to the owning\n // napplet. Shape matches Branch B (superset of KeysActionMessage);\n // the `chord` extension is omitted here because bridges deliver\n // pre-parsed chord events without the internal ChordSpec struct.\n const payload: KeysActionMessage = {\n type: 'keys.action',\n actionId: m.action.id,\n };\n send(payload as NappletMessage);\n });\n unsubscribeHandles.set(m.action.id, unsubscribe);\n if (!bridgeWindowActions.has(windowId)) bridgeWindowActions.set(windowId, new Set());\n bridgeWindowActions.get(windowId)!.add(m.action.id);\n } catch (err) {\n const id = m.id ?? '';\n send({\n type: 'keys.registerAction.error',\n id,\n error: `bridge subscribe failed: ${(err as Error).message}`,\n } as NappletMessage);\n return;\n }\n }\n const result: KeysRegisterActionResultMessage = {\n type: 'keys.registerAction.result',\n id: m.id,\n actionId: m.action.id,\n ...(m.action.defaultKey ? { binding: m.action.defaultKey } : {}),\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'keys.unregisterAction': {\n const m = message as NappletMessage & { actionId?: string };\n if (m.actionId) {\n const unsubscribe = unsubscribeHandles.get(m.actionId);\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch {\n /* best-effort */\n }\n unsubscribeHandles.delete(m.actionId);\n // Prune the bridgeWindowActions entry that owns this actionId.\n for (const [wid, set] of bridgeWindowActions.entries()) {\n if (set.delete(m.actionId) && set.size === 0) bridgeWindowActions.delete(wid);\n }\n }\n }\n return;\n }\n\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown keys method: ${message.type}`,\n } as NappletMessage);\n return;\n }\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const actions = bridgeWindowActions.get(windowId);\n if (!actions) return;\n for (const actionId of actions) {\n const unsubscribe = unsubscribeHandles.get(actionId);\n if (unsubscribe) {\n try {\n unsubscribe();\n } catch {\n /* best-effort */\n }\n }\n unsubscribeHandles.delete(actionId);\n }\n bridgeWindowActions.delete(windowId);\n },\n\n destroy(): void {\n for (const unsubscribe of unsubscribeHandles.values()) {\n try {\n unsubscribe();\n } catch {\n /* best-effort */\n }\n }\n unsubscribeHandles.clear();\n bridgeWindowActions.clear();\n },\n };\n }\n\n const actionRegistry = new Map<string, ActionEntry>(); // actionId → {chord, chordString, windowId}\n const windowActions = new Map<string, Set<string>>(); // windowId → Set<actionId>\n // Per-window `send` callback captured at registerAction time. Used to push\n // keys.action envelopes back to the owning napplet on chord match — this is\n // the canonical @napplet/nub/keys surface the SDK's `keys.onAction(...)`\n // helper consumes.\n const sendHandles = new Map<string, (msg: NappletMessage) => void>(); // windowId → send\n\n // ─── Listener target (SSR / test-safe fallback, mirrors keys-forwarder.ts) ─\n const target: EventTarget =\n options.listenerTarget ??\n (typeof document !== 'undefined' ? document : new EventTarget());\n\n function chordMatches(spec: ChordSpec, ev: KeyboardEvent): boolean {\n if (spec.ctrl !== ev.ctrlKey) return false;\n if (spec.alt !== ev.altKey) return false;\n if (spec.shift !== ev.shiftKey) return false;\n if (spec.meta !== ev.metaKey) return false;\n const evKey = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;\n return spec.key === evKey;\n }\n\n const listener = (rawEv: Event): void => {\n const ev = rawEv as KeyboardEvent;\n if (ev.repeat) return; // ignore OS autorepeat — matches \"I pressed it once\" intent (CONTEXT Area 1)\n\n // Phase 33 / KEYS-04-05: reserved chords fire onForward but suppress\n // keys.action fan-out to napplets. Check ONCE up front.\n const isReserved = reservedChordKeys.has(eventKey(ev));\n\n // Determine if any registered action would match — needed to decide\n // whether to fire onForward on a non-reserved keydown (legacy parity).\n // A reserved chord fires onForward regardless of napplet registration\n // (WM-launcher case: shell declares chord, no napplet registers it).\n let anyMatch = false;\n for (const entry of actionRegistry.values()) {\n if (chordMatches(entry.chord, ev)) {\n anyMatch = true;\n break;\n }\n }\n\n if (isReserved || anyMatch) {\n options.onForward?.({\n key: ev.key,\n code: ev.code,\n ctrlKey: ev.ctrlKey,\n altKey: ev.altKey,\n shiftKey: ev.shiftKey,\n metaKey: ev.metaKey,\n });\n }\n\n if (isReserved) return; // reserved → no napplet fan-out\n\n // (2) Canonical shell→napplet push: emit keys.action to the owning\n // napplet via its captured send callback. The SDK's keys.onAction\n // helper subscribes to this envelope. We attach a `chord` extension\n // field so the demo napplet can display the fired chord without\n // reconstructing it from the original registration.\n for (const [actionId, entry] of actionRegistry.entries()) {\n if (chordMatches(entry.chord, ev)) {\n const send = sendHandles.get(entry.windowId);\n if (send) {\n const payload: KeysActionMessage & { chord: ChordSpec } = {\n type: 'keys.action',\n actionId,\n chord: entry.chord,\n };\n send(payload as NappletMessage);\n }\n // Intentionally no `break` — two actions subscribing to the same chord\n // both receive the event. Conflict resolution is an explicit v1.5+\n // concern per CONTEXT.md Deferred Ideas.\n }\n }\n };\n\n target.addEventListener('keydown', listener);\n\n return {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'keys.forward': {\n // Legacy passthrough: napplet-forwarded keydown translation.\n // Preserved bit-for-bit from the stub — existing tests + the\n // keys-forwarder.ts -> service.handleMessage path depend on this shape.\n //\n // Phase 33 / KEYS-04-05: Branch B's keys.forward handler does NOT\n // emit keys.action (fan-out happens in the document keydown listener),\n // so reservation is observationally identical to the base case.\n // The explicit guard below pins the contract for future edits.\n const m = message as KeysForwardMessage;\n const reserved = reservedChordKeys.has(forwardKey(m));\n options.onForward?.(forwardPayload(m));\n if (reserved) return;\n return;\n }\n\n case 'keys.registerAction': {\n const m = message as KeysRegisterActionMessage;\n // Capture (or refresh) the per-window send callback. The runtime's\n // service-handler contract guarantees `send` remains valid for this\n // windowId until onWindowDestroyed(windowId) fires — we cache the\n // most recent invocation so the keydown listener can push\n // keys.action envelopes back to the owning napplet.\n sendHandles.set(windowId, send);\n\n if (m.action.defaultKey) {\n try {\n const chord = parseChord(m.action.defaultKey);\n actionRegistry.set(m.action.id, {\n chord,\n chordString: m.action.defaultKey,\n windowId,\n });\n if (!windowActions.has(windowId)) windowActions.set(windowId, new Set());\n windowActions.get(windowId)!.add(m.action.id);\n } catch (err) {\n // Parse failure: respond with .error envelope (unknown-method pattern).\n const id = m.id ?? '';\n send({\n type: 'keys.registerAction.error',\n id,\n error: `invalid chord: ${(err as Error).message}`,\n } as NappletMessage);\n return;\n }\n }\n const result: KeysRegisterActionResultMessage = {\n type: 'keys.registerAction.result',\n id: m.id,\n actionId: m.action.id,\n ...(m.action.defaultKey ? { binding: m.action.defaultKey } : {}),\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'keys.unregisterAction': {\n // Fire-and-forget per @napplet/nub/keys spec. Remove subscription if present.\n const m = message as NappletMessage & { actionId?: string };\n if (m.actionId && actionRegistry.has(m.actionId)) {\n const entry = actionRegistry.get(m.actionId)!;\n actionRegistry.delete(m.actionId);\n const set = windowActions.get(entry.windowId);\n if (set) {\n set.delete(m.actionId);\n // If the window has no remaining actions, drop its cached send\n // that no longer subscribes to anything.\n if (set.size === 0) {\n windowActions.delete(entry.windowId);\n sendHandles.delete(entry.windowId);\n }\n }\n }\n return;\n }\n\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown keys method: ${message.type}`,\n } as NappletMessage);\n return;\n }\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n const actions = windowActions.get(windowId);\n if (actions) {\n for (const actionId of actions) actionRegistry.delete(actionId);\n windowActions.delete(windowId);\n }\n sendHandles.delete(windowId);\n },\n\n destroy(): void {\n target.removeEventListener('keydown', listener);\n actionRegistry.clear();\n windowActions.clear();\n sendHandles.clear();\n },\n };\n}\n","import type { MediaAction, MediaMetadata } from '@napplet/nub/media/types';\nimport type { HostMediaBridge } from './media-service.js';\n\n/** Silent-audio prime data URL (4 kHz silent WAV, 44 bytes, zero network dependency).\n * Browsers refuse to render OS media controls without a playing audio element —\n * this silent loop primes the MediaSession API. Per CONTEXT.md Area 1. */\nconst SILENT_AUDIO_DATA_URL =\n 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=';\n\n/** Default action set — all 5 nub-media transport actions. */\nexport const DEFAULT_MEDIA_ACTIONS: readonly MediaAction[] = ['play', 'pause', 'next', 'prev', 'seek'];\n\n/** Minimal subset of navigator.mediaSession the browser bridge depends on. Makes the bridge\n * Node/test-safe: unit tests pass a MockMediaSession via mediaSessionTarget.\n * The handler parameter uses `details?` (optional) so both the real DOM impl\n * (which always passes an object) and test mocks that omit details both satisfy\n * this type structurally. */\nexport type MediaSessionTarget = {\n metadata: MediaMetadataLike | null;\n playbackState: 'none' | 'playing' | 'paused';\n setActionHandler(action: string, handler: ((details?: { action?: string; seekTime?: number }) => void) | null): void;\n};\n\n/** Structural subset of the DOM MediaMetadata class — assignable from a plain object\n * with title/artist/album/artwork fields. The browser impl uses `new MediaMetadata({...})`;\n * tests can pass a plain object. */\nexport type MediaMetadataLike = { title?: string; artist?: string; album?: string; artwork?: unknown };\n\n/** Mapping from DOM MediaSession action names to nub-media MediaAction literals. */\nconst ACTION_MATRIX: ReadonlyArray<[string, MediaAction]> = [\n ['play', 'play'],\n ['pause', 'pause'],\n ['nexttrack', 'next'],\n ['previoustrack', 'prev'],\n ['seekto', 'seek'],\n];\n\n/**\n * Reference browser implementation of {@link HostMediaBridge}.\n *\n * Mirrors metadata to `navigator.mediaSession.metadata` (via the DOM\n * `MediaMetadata` constructor when available; plain-object fallback in test\n * envs). Mirrors playback state to `navigator.mediaSession.playbackState`\n * with the canonical mapping: 'playing' maps to 'playing', 'paused' maps to\n * 'paused', 'buffering' maps to 'paused', 'stopped' maps to 'none'. Installs\n * `setActionHandler` callbacks for play/pause/nexttrack/previoustrack/seekto\n * that fan into the onAction subscriber with the mapped `MediaAction` literal\n * (and `value` from `details.seekTime` for seekto). When `setActiveSession` is\n * called with a non-null `actions` parameter, only the declared actions get\n * active handlers — the remaining are cleared (matching the capabilities\n * narrowing behavior of Plan 27-01's inline implementation).\n *\n * Installs a silent-audio prime (4 kHz silent WAV data URL) when the first\n * session becomes active (setActiveSession called with a non-null sessionId) —\n * browsers refuse to render OS media controls without a playing audio element.\n * Removes the element when destroySession brings the active session count to\n * zero.\n *\n * @param opts.mediaSessionTarget - Override navigator.mediaSession (tests).\n * @param opts.documentTarget - Override document (tests; pass null to disable silent-audio prime).\n */\nexport function createBrowserMediaBridge(opts: {\n mediaSessionTarget?: MediaSessionTarget;\n documentTarget?: Document | null;\n} = {}): HostMediaBridge {\n const ms: MediaSessionTarget | null =\n opts.mediaSessionTarget\n ?? (typeof navigator !== 'undefined' && 'mediaSession' in navigator\n ? (navigator.mediaSession as MediaSessionTarget)\n : null);\n const doc: Document | null =\n opts.documentTarget !== undefined\n ? opts.documentTarget\n : (typeof document !== 'undefined' ? document : null);\n\n let silentAudioEl: HTMLAudioElement | null = null;\n let activeSessionId: string | null = null;\n let sessionsActive = 0;\n const actionCallbacks = new Set<(sessionId: string, action: MediaAction, value?: number) => void>();\n\n function primeSilentAudio(): void {\n if (silentAudioEl || !doc) return;\n const el = doc.createElement('audio') as HTMLAudioElement;\n el.src = SILENT_AUDIO_DATA_URL;\n el.loop = true;\n el.style.display = 'none';\n (el as HTMLAudioElement).setAttribute('data-kehto-silent-audio-prime', 'true');\n doc.body.appendChild(el);\n void el.play().catch(() => { /* autoplay refused — metadata mirror still works */ });\n silentAudioEl = el;\n }\n\n function teardownSilentAudio(): void {\n if (!silentAudioEl) return;\n try { silentAudioEl.pause(); } catch { /* best-effort */ }\n try { silentAudioEl.remove(); } catch { /* best-effort */ }\n silentAudioEl = null;\n }\n\n /**\n * Install action handlers for the given set of nub-media actions. Actions not\n * in the set get their handler cleared. Matches Plan 27-01's installActionHandlersFor\n * behavior exactly so capabilities-narrowing tests continue to pass.\n */\n function applyActionHandlers(actions: readonly MediaAction[] = DEFAULT_MEDIA_ACTIONS): void {\n if (!ms) return;\n for (const [domAction, nubAction] of ACTION_MATRIX) {\n if (!actions.includes(nubAction)) {\n try { ms.setActionHandler(domAction, null); } catch { /* best-effort */ }\n continue;\n }\n ms.setActionHandler(domAction, (details) => {\n if (!activeSessionId) return;\n const value = nubAction === 'seek' && typeof details?.seekTime === 'number' ? details.seekTime : undefined;\n for (const cb of actionCallbacks) {\n cb(activeSessionId, nubAction, value);\n }\n });\n }\n }\n\n function writeMetadata(metadata: MediaMetadata | undefined): void {\n if (!ms) return;\n if (!metadata) { ms.metadata = null; return; }\n const artwork = metadata.artwork?.url ? [{ src: metadata.artwork.url }] : undefined;\n const init: MediaMetadataLike & { artwork?: unknown } = {\n title: metadata.title ?? '',\n artist: metadata.artist ?? '',\n album: metadata.album ?? '',\n ...(artwork ? { artwork } : {}),\n };\n try {\n const ctor = (globalThis as typeof globalThis & { MediaMetadata?: new (init: MediaMetadataLike) => MediaMetadataLike }).MediaMetadata;\n ms.metadata = ctor ? new ctor(init) : (init as MediaMetadataLike);\n } catch {\n ms.metadata = init as MediaMetadataLike;\n }\n }\n\n return {\n setMetadata(sessionId, metadata) {\n if (sessionId === activeSessionId) writeMetadata(metadata);\n },\n setPlaybackState(sessionId, state) {\n if (!ms || sessionId !== activeSessionId) return;\n ms.playbackState =\n state === 'playing' ? 'playing'\n : state === 'paused' || state === 'buffering' ? 'paused'\n : 'none';\n },\n onAction(callback) {\n actionCallbacks.add(callback);\n return () => { actionCallbacks.delete(callback); };\n },\n setActiveSession(sessionId, actions) {\n activeSessionId = sessionId;\n if (!sessionId) {\n if (ms) {\n ms.metadata = null;\n ms.playbackState = 'none';\n for (const [domAction] of ACTION_MATRIX) {\n try { ms.setActionHandler(domAction, null); } catch { /* best-effort */ }\n }\n }\n return;\n }\n // Prime silent-audio on first session becoming active.\n if (sessionsActive === 0) { primeSilentAudio(); sessionsActive = 1; }\n applyActionHandlers(actions ?? DEFAULT_MEDIA_ACTIONS);\n },\n destroySession(_sessionId) {\n sessionsActive = Math.max(0, sessionsActive - 1);\n if (sessionsActive === 0) teardownSilentAudio();\n },\n };\n}\n","/**\n * media-service.ts — NIP-5D media NUB reference service (navigator.mediaSession\n * reference implementation).\n *\n * Handles the napplet-owned subset of @napplet/nub/media:\n * media.session.create (result), media.session.update, media.session.destroy,\n * media.state, media.capabilities.\n *\n * HostMediaBridge contract: {@link HostMediaBridge} defines the pluggable backend\n * contract for metadata/state mirroring + action routing. The browser reference\n * implementation is {@link createBrowserMediaBridge} (mirrors to navigator.mediaSession\n * with setActionHandler matrix). When no hostBridge option is passed, createMediaService\n * internally uses createBrowserMediaBridge as the default — behavior is identical\n * to the Plan 27-01 single-path implementation.\n *\n * navigator.mediaSession mirroring: on session.create the browser bridge mirrors the\n * napplet-supplied metadata to navigator.mediaSession.metadata via new MediaMetadata()\n * and installs setActionHandler callbacks for the 5 OS transport actions\n * (play / pause / nexttrack / previoustrack / seekto). Each callback emits a canonical\n * media.command envelope to the owning napplet — that is the @napplet/nub/media\n * MediaCommandMessage shape consumed by the SDK's mediaOnCommand() helper.\n *\n * NAP-MEDIA now distinguishes napplet-owned playback from shell-owned\n * playback. This reference backend supports `owner: \"napplet\"` because it\n * mirrors a napplet's own media element to navigator.mediaSession. It rejects\n * `owner: \"shell\"` until a host bridge provides policy-checked source fetching\n * and playback.\n *\n * media.command push: when the OS user clicks a media control (hardware key, lock-screen\n * transport, OS media overlay), the bridge's onAction callback fires. The service looks\n * up the active session's owning napplet, invokes the per-window send callback captured\n * at session.create time, and delivers:\n * { type: 'media.command', sessionId, action, value? }\n * where action is the nub-media MediaAction literal (play|pause|next|prev|seek) and\n * value is the seekTime (seconds) for seek.\n *\n * Silent-audio prime: the browser bridge creates a hidden <audio> element with a\n * 4 kHz silent WAV data URL and plays it when the first session becomes active\n * (setActiveSession with a non-null sessionId). Without a playing audio element in\n * the host page, most browsers refuse to render OS media controls. The element is\n * cleaned up when the last session is destroyed (via bridge.destroySession).\n *\n * Multi-session registry with last-active-wins semantics: every session.create is\n * tracked in a Map<sessionId, SessionEntry>; any media.state report promotes that\n * session to active. The active session's metadata and playback state are reflected\n * via bridge.setMetadata / bridge.setPlaybackState. When the active session is\n * destroyed, the next-most-recently-touched session is promoted automatically.\n *\n * Note: this is SEPARATE from packages/services/src/audio-service.ts, which\n * is the legacy ifc-topic-based audio source registry (audio:* topic events\n * over ifc.emit). media-service is the canonical @napplet/nub/media NIP-5D\n * path and they coexist — audio-service continues to track audio sources for\n * shell UI, while media-service handles the NUB protocol envelope surface.\n *\n * Shell -> Napplet push types (media.command) are emitted here when\n * bridge.onAction callbacks fire — this is the canonical Phase 27 real backend.\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n MediaAction,\n MediaCapabilitiesMessage,\n MediaCommandMessage,\n MediaMetadata,\n MediaSessionDestroyMessage,\n MediaSessionUpdateMessage,\n MediaStateMessage,\n} from '@napplet/nub/media/types';\nimport {\n createBrowserMediaBridge,\n DEFAULT_MEDIA_ACTIONS,\n type MediaSessionTarget,\n} from './browser-media-bridge.js';\n\nexport { createBrowserMediaBridge } from './browser-media-bridge.js';\nexport type {\n MediaMetadataLike,\n MediaSessionTarget,\n} from './browser-media-bridge.js';\n\n/** Registry entry — maps a sessionId back to its owning window, metadata snapshot,\n * state snapshot, and declared capabilities. Internal; never on the wire. */\ninterface SessionEntry {\n sessionId: string;\n windowId: string;\n owner: MediaPlaybackOwner;\n source: MediaSourceRef | undefined;\n metadata: MediaMetadata | undefined;\n state: { status: 'playing' | 'paused' | 'stopped' | 'buffering'; position?: number; duration?: number; volume?: number } | undefined;\n actions: readonly MediaAction[]; // from media.capabilities; defaults to ['play','pause','next','prev','seek']\n /** Monotonic tick updated on every session.create / session.update / state — used for last-active-wins resolution when the current active session is destroyed. */\n lastTouched: number;\n}\n\n/** Media service version — follows semver. */\nconst MEDIA_SERVICE_VERSION = '1.1.0';\n\nexport type MediaPlaybackOwner = 'shell' | 'napplet';\n\nexport interface MediaSourceRef {\n url?: string;\n blossomHash?: string;\n nostr?: {\n eventId?: string;\n address?: string;\n relays?: string[];\n };\n mimeType?: string;\n}\n\nexport interface MediaSessionCreateOptions {\n owner: MediaPlaybackOwner;\n sessionId?: string;\n source?: MediaSourceRef;\n metadata?: MediaMetadata;\n capabilities?: MediaAction[];\n autoplay?: boolean;\n live?: boolean;\n}\n\ntype MediaSessionCreateEnvelope = NappletMessage & Partial<MediaSessionCreateOptions> & {\n type: 'media.session.create';\n id?: string;\n};\n\ntype MediaSessionCreateResultEnvelope = NappletMessage & {\n type: 'media.session.create.result';\n id: string;\n sessionId?: string;\n owner?: MediaPlaybackOwner;\n error?: string;\n};\n\n/**\n * Host-bridge contract for pluggable media backends.\n *\n * The browser reference implementation ({@link createBrowserMediaBridge}) mirrors\n * napplet-reported metadata/state to `navigator.mediaSession` and installs\n * `setActionHandler` callbacks that fan into the bridge's onAction subscribers.\n * It satisfies this interface with all 5 fields implemented (setActiveSession\n * switches the active session and optionally re-applies action-handler narrowing;\n * destroySession tears down the silent-audio prime on last-session teardown).\n *\n * Host apps (Electron, Tauri) implement this interface in their own code and\n * pass it via `createMediaService({ hostBridge: myBridge })` — the service\n * then delegates metadata/state mirroring + action routing to the bridge and\n * remains browser-free. Session-ownership bookkeeping (which windowId owns\n * which sessionId, which send callback routes media.command back to which\n * napplet) stays in the service layer — that's wire-protocol concern, not a\n * bridge concern.\n *\n * Reference implementations for Electron / Tauri are explicitly out of v1.4\n * scope and live in host-app examples / follow-up milestones (see\n * REQUIREMENTS.md \"Future Requirements\").\n *\n * @example\n * ```ts\n * // Host-app pseudocode (Electron main-process relay):\n * const electronBridge: HostMediaBridge = {\n * setMetadata(sessionId, md) { mediaBridge.sendMetadata({ sessionId, md }); },\n * setPlaybackState(sessionId, state) { mediaBridge.sendPlaybackState({ sessionId, state }); },\n * onAction(cb) {\n * const handler = (_: unknown, msg: { sessionId: string; action: MediaAction; value?: number }) =>\n * cb(msg.sessionId, msg.action, msg.value);\n * mediaBridge.onAction(handler);\n * return () => mediaBridge.offAction(handler);\n * },\n * };\n * const media = createMediaService({ hostBridge: electronBridge });\n * runtime.registerService('media', media);\n * ```\n */\nexport interface HostMediaBridge {\n /**\n * Set the metadata displayed on the OS transport surface for a session.\n * Called on session.create (with initial metadata) and on session.update\n * (with merged metadata) whenever the session is the active session.\n * Implementations MUST be idempotent.\n */\n setMetadata(sessionId: string, metadata: MediaMetadata): void;\n\n /**\n * Set the playback state for a session. Called on media.state reports\n * whenever the session is the active session. State strings match\n * nub-media MediaState.status exactly. Implementations MUST be idempotent.\n */\n setPlaybackState(sessionId: string, state: 'playing' | 'paused' | 'stopped' | 'buffering'): void;\n\n /**\n * Subscribe to OS-level action events (user clicks play/pause/seek/next/prev\n * on the transport surface). Returns an unsubscribe handle.\n *\n * The callback receives `(sessionId, action, value?)`. `sessionId` is the\n * bridge's currently-active session (the browser impl tracks this internally\n * via setActionHandler-at-fire-time; native impls track via setActiveSession).\n * `value` is populated for `action === 'seek'` (seek target in seconds) and\n * for `action === 'volume'` (0.0-1.0). The service dispatches the resulting\n * `media.command` envelope to the owning napplet of that session.\n */\n onAction(callback: (sessionId: string, action: MediaAction, value?: number) => void): () => void;\n\n /**\n * Optional: notify the bridge that the active session has changed. The\n * browser reference impl uses this to switch which session's metadata/state\n * is mirrored to the singleton navigator.mediaSession and to install (or\n * clear) action handlers for the session's declared capabilities.\n *\n * The optional `actions` parameter carries the session's declared capability\n * set so the bridge can narrow which OS transport buttons are active. When\n * omitted, the bridge applies its default set. Native OS bridges that track\n * active-session state internally may omit this field entirely.\n */\n setActiveSession?(sessionId: string | null, actions?: readonly MediaAction[]): void;\n\n /**\n * Optional: tear down per-session resources. The browser reference impl\n * uses this to remove the silent-audio prime element when the last session\n * is destroyed. Bridges that need no per-session teardown may omit this field.\n */\n destroySession?(sessionId: string): void;\n}\n\n/**\n * Optional host callbacks for the media service.\n *\n * Host shells can hook session creation and state updates to drive\n * their own UI (e.g., now-playing banner) without replacing the\n * ServiceHandler wholesale.\n *\n * @example\n * ```ts\n * const media = createMediaService({\n * onSessionCreate: (windowId, sessionId, metadata) => {\n * console.log(`[${windowId}] created session ${sessionId}`, metadata);\n * },\n * onState: (windowId, sessionId, state) => {\n * nowPlaying.update(windowId, state);\n * },\n * });\n *\n * runtime.registerService('media', media);\n * ```\n */\nexport interface MediaServiceOptions {\n /** Called when a napplet creates a session. May inspect/record the session. */\n onSessionCreate?: (windowId: string, sessionId: string, metadata?: unknown) => void;\n /** Called on media.state updates — high-frequency; keep handler work minimal. */\n onState?: (windowId: string, sessionId: string, state: unknown) => void;\n /** Called when a napplet destroys a session. */\n onSessionDestroy?: (windowId: string, sessionId: string) => void;\n /** Called when a napplet updates session metadata. */\n onSessionUpdate?: (windowId: string, sessionId: string, metadata: unknown) => void;\n /** Called when a napplet declares capabilities for a session. */\n onCapabilities?: (windowId: string, sessionId: string, actions: unknown) => void;\n\n /**\n * MediaSession target override (used by default browser bridge only).\n * Defaults to `navigator.mediaSession` when running in a browser. Pass a\n * MockMediaSession in unit tests. Ignored when `hostBridge` is provided.\n */\n mediaSessionTarget?: MediaSessionTarget;\n\n /**\n * DOM document override (used by default browser bridge only).\n * Defaults to `document` when available. Set to null to disable the silent-audio\n * prime entirely — useful in unit tests. Ignored when `hostBridge` is provided.\n */\n documentTarget?: Document | null;\n\n /**\n * Optional pluggable backend for metadata/state mirroring + OS action handling.\n * When provided, the service delegates setMetadata / setPlaybackState / onAction\n * to the bridge and skips navigator.mediaSession entirely. When omitted, the\n * service internally uses {@link createBrowserMediaBridge} as the default.\n * See {@link HostMediaBridge}.\n */\n hostBridge?: HostMediaBridge;\n}\n\ninterface MediaServiceState {\n bridge: HostMediaBridge;\n options: MediaServiceOptions;\n sessionRegistry: Map<string, SessionEntry>;\n windowSessions: Map<string, Set<string>>;\n sendHandles: Map<string, (msg: NappletMessage) => void>;\n activeSessionId: string | null;\n touchCounter: number;\n sessionCounter: number;\n}\n\nfunction createMediaServiceState(options: MediaServiceOptions, bridge: HostMediaBridge): MediaServiceState {\n return {\n bridge,\n options,\n sessionRegistry: new Map<string, SessionEntry>(),\n windowSessions: new Map<string, Set<string>>(),\n sendHandles: new Map<string, (msg: NappletMessage) => void>(),\n activeSessionId: null,\n touchCounter: 0,\n sessionCounter: 0,\n };\n}\n\nfunction setActive(state: MediaServiceState, sessionId: string | null, actions?: readonly MediaAction[]): void {\n state.activeSessionId = sessionId;\n state.bridge.setActiveSession?.(sessionId, actions);\n if (!sessionId) return;\n const entry = state.sessionRegistry.get(sessionId);\n if (!entry) return;\n if (entry.metadata) state.bridge.setMetadata(sessionId, entry.metadata);\n if (entry.state) state.bridge.setPlaybackState(sessionId, entry.state.status);\n}\n\nfunction promoteNextActiveOrClear(state: MediaServiceState): void {\n if (state.sessionRegistry.size === 0) {\n setActive(state, null);\n return;\n }\n let latest: SessionEntry | null = null;\n for (const entry of state.sessionRegistry.values()) {\n if (!latest || entry.lastTouched > latest.lastTouched) latest = entry;\n }\n setActive(state, latest ? latest.sessionId : null, latest?.actions);\n}\n\nfunction sendMediaCommand(state: MediaServiceState, sessionId: string, action: MediaAction, value?: number): void {\n const entry = state.sessionRegistry.get(sessionId);\n if (!entry) return;\n const send = state.sendHandles.get(entry.windowId);\n if (!send) return;\n const payload: MediaCommandMessage = {\n type: 'media.command',\n sessionId,\n action,\n ...(typeof value === 'number' ? { value } : {}),\n };\n send(payload as NappletMessage);\n}\n\nfunction registerWindowSession(state: MediaServiceState, windowId: string, sessionId: string): void {\n if (!state.windowSessions.has(windowId)) state.windowSessions.set(windowId, new Set());\n state.windowSessions.get(windowId)!.add(sessionId);\n}\n\nfunction sendSessionCreateResult(\n send: (msg: NappletMessage) => void,\n id: string | undefined,\n fields: Omit<MediaSessionCreateResultEnvelope, 'type' | 'id'>,\n): void {\n send({\n type: 'media.session.create.result',\n id: id ?? '',\n ...fields,\n } as NappletMessage);\n}\n\nfunction isMediaPlaybackOwner(value: unknown): value is MediaPlaybackOwner {\n return value === 'shell' || value === 'napplet';\n}\n\nfunction hasSourceRef(source: MediaSourceRef | undefined): boolean {\n if (!source) return false;\n if (typeof source.url === 'string' && source.url.length > 0) return true;\n if (typeof source.blossomHash === 'string' && source.blossomHash.length > 0) return true;\n if (source.nostr) {\n return Boolean(source.nostr.eventId || source.nostr.address);\n }\n return false;\n}\n\nfunction canonicalizeSessionId(\n state: MediaServiceState,\n windowId: string,\n preferredSessionId: string | undefined,\n): string {\n const trimmed = typeof preferredSessionId === 'string' ? preferredSessionId.trim() : '';\n const hint = trimmed || `session-${++state.sessionCounter}`;\n if (!state.sessionRegistry.has(hint)) return hint;\n\n let next: string;\n do {\n next = `${windowId}:${hint}:${++state.sessionCounter}`;\n } while (state.sessionRegistry.has(next));\n return next;\n}\n\nfunction handleSessionCreate(\n state: MediaServiceState,\n windowId: string,\n message: MediaSessionCreateEnvelope,\n send: (msg: NappletMessage) => void,\n): void {\n if (!isMediaPlaybackOwner(message.owner)) {\n sendSessionCreateResult(send, message.id, { error: 'missing owner' });\n return;\n }\n\n if (message.owner === 'shell') {\n if (!hasSourceRef(message.source)) {\n sendSessionCreateResult(send, message.id, { owner: 'shell', error: 'missing source' });\n return;\n }\n sendSessionCreateResult(send, message.id, { owner: 'shell', error: 'unsupported owner mode' });\n return;\n }\n\n state.sendHandles.set(windowId, send);\n const sessionId = canonicalizeSessionId(state, windowId, message.sessionId);\n const entry: SessionEntry = {\n sessionId,\n windowId,\n owner: message.owner,\n source: message.source,\n metadata: message.metadata,\n state: undefined,\n actions: message.capabilities ?? DEFAULT_MEDIA_ACTIONS,\n lastTouched: ++state.touchCounter,\n };\n state.sessionRegistry.set(sessionId, entry);\n registerWindowSession(state, windowId, sessionId);\n setActive(state, sessionId, entry.actions);\n state.options.onSessionCreate?.(windowId, sessionId, message.metadata);\n sendSessionCreateResult(send, message.id, { sessionId, owner: message.owner });\n}\n\nfunction handleSessionUpdate(state: MediaServiceState, windowId: string, message: MediaSessionUpdateMessage): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry) {\n entry.metadata = { ...entry.metadata, ...message.metadata };\n entry.lastTouched = ++state.touchCounter;\n if (entry.owner === 'napplet' && message.sessionId === state.activeSessionId && entry.metadata) {\n state.bridge.setMetadata(message.sessionId, entry.metadata);\n }\n }\n state.options.onSessionUpdate?.(windowId, message.sessionId, message.metadata);\n}\n\nfunction handleSessionDestroy(state: MediaServiceState, windowId: string, message: MediaSessionDestroyMessage): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry) {\n state.sessionRegistry.delete(message.sessionId);\n const set = state.windowSessions.get(entry.windowId);\n if (set) {\n set.delete(message.sessionId);\n if (set.size === 0) state.windowSessions.delete(entry.windowId);\n }\n state.bridge.destroySession?.(message.sessionId);\n if (message.sessionId === state.activeSessionId) promoteNextActiveOrClear(state);\n }\n state.options.onSessionDestroy?.(windowId, message.sessionId);\n}\n\nfunction handleMediaState(state: MediaServiceState, windowId: string, message: MediaStateMessage): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry?.owner === 'napplet') {\n entry.state = {\n status: message.status,\n position: message.position,\n duration: message.duration,\n volume: message.volume,\n };\n entry.lastTouched = ++state.touchCounter;\n if (state.activeSessionId !== message.sessionId) setActive(state, message.sessionId, entry.actions);\n else state.bridge.setPlaybackState(message.sessionId, message.status);\n }\n state.options.onState?.(windowId, message.sessionId, message);\n}\n\nfunction handleMediaCapabilities(\n state: MediaServiceState,\n windowId: string,\n message: MediaCapabilitiesMessage,\n): void {\n const entry = state.sessionRegistry.get(message.sessionId);\n if (entry?.owner === 'napplet') {\n entry.actions = message.actions;\n entry.lastTouched = ++state.touchCounter;\n if (message.sessionId === state.activeSessionId) {\n state.bridge.setActiveSession?.(message.sessionId, entry.actions);\n }\n }\n state.options.onCapabilities?.(windowId, message.sessionId, message.actions);\n}\n\nfunction handleMediaMessage(\n state: MediaServiceState,\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n): void {\n switch (message.type) {\n case 'media.session.create':\n handleSessionCreate(state, windowId, message as MediaSessionCreateEnvelope, send);\n return;\n case 'media.session.update':\n handleSessionUpdate(state, windowId, message as MediaSessionUpdateMessage);\n return;\n case 'media.session.destroy':\n handleSessionDestroy(state, windowId, message as MediaSessionDestroyMessage);\n return;\n case 'media.state':\n handleMediaState(state, windowId, message as MediaStateMessage);\n return;\n case 'media.capabilities':\n handleMediaCapabilities(state, windowId, message as MediaCapabilitiesMessage);\n return;\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown media method: ${message.type}`,\n } as NappletMessage);\n }\n }\n}\n\nfunction destroyWindowSessions(state: MediaServiceState, windowId: string): void {\n const sessions = state.windowSessions.get(windowId);\n if (sessions) {\n const ownedActive = state.activeSessionId !== null && sessions.has(state.activeSessionId);\n for (const sessionId of sessions) {\n state.sessionRegistry.delete(sessionId);\n state.bridge.destroySession?.(sessionId);\n }\n state.windowSessions.delete(windowId);\n if (ownedActive) promoteNextActiveOrClear(state);\n }\n state.sendHandles.delete(windowId);\n}\n\nfunction destroyMediaState(state: MediaServiceState, unsubscribeAction: () => void): void {\n unsubscribeAction();\n for (const sessionId of state.sessionRegistry.keys()) state.bridge.destroySession?.(sessionId);\n state.bridge.setActiveSession?.(null);\n state.sessionRegistry.clear();\n state.windowSessions.clear();\n state.sendHandles.clear();\n state.activeSessionId = null;\n state.touchCounter = 0;\n state.sessionCounter = 0;\n}\n\n/**\n * Create a media NUB service handler with navigator.mediaSession integration.\n *\n * Implements the 5 napplet->shell media.* request types defined in\n * `@napplet/nub/media`. Only `media.session.create` produces a reply\n * envelope (`media.session.create.result`) — the remaining four\n * (`session.update`, `session.destroy`, `state`, `capabilities`) are\n * fire-and-forget per the NUB spec.\n *\n * Unknown `media.*` actions produce a `<type>.error` envelope so\n * napplets are never left hanging on a malformed request.\n *\n * @param options - Optional host callbacks for session lifecycle + state, plus\n * mediaSessionTarget and documentTarget for test injection, and an optional\n * hostBridge for native-OS media backend delegation.\n * @returns A ServiceHandler (with `destroy()`) to register with the runtime\n *\n * @example\n * ```ts\n * import { createMediaService } from '@kehto/services';\n *\n * const media = createMediaService();\n * runtime.registerService('media', media);\n * // Later, on shell teardown:\n * media.destroy();\n * ```\n */\nexport function createMediaService(options: MediaServiceOptions = {}): ServiceHandler & { destroy(): void } {\n const descriptor: ServiceDescriptor = {\n name: 'media',\n version: MEDIA_SERVICE_VERSION,\n description: options.hostBridge\n ? 'NIP-5D media NUB reference handler (host-bridge delegated)'\n : 'NIP-5D media NUB reference handler (navigator.mediaSession mirror)',\n };\n\n const bridge: HostMediaBridge = options.hostBridge\n ?? createBrowserMediaBridge({\n mediaSessionTarget: options.mediaSessionTarget,\n documentTarget: options.documentTarget,\n });\n const state = createMediaServiceState(options, bridge);\n\n const unsubscribeAction = bridge.onAction((sessionId, action, value) => {\n sendMediaCommand(state, sessionId, action, value);\n });\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n handleMediaMessage(state, windowId, message, send);\n },\n\n onWindowDestroyed(windowId: string): void {\n destroyWindowSessions(state, windowId);\n },\n\n destroy(): void {\n destroyMediaState(state, unsubscribeAction);\n },\n };\n}\n","/**\n * notify-service.ts — NIP-5D notify NUB reference service (stub-level).\n *\n * Handles the 5 napplet -> shell request types from `@napplet/nub/notify`:\n * - `notify.send` -> `notify.send.result` (shell-assigned id)\n * - `notify.dismiss` -> fire-and-forget\n * - `notify.badge` -> fire-and-forget\n * - `notify.channel.register` -> fire-and-forget\n * - `notify.permission.request` -> `notify.permission.result { granted }`\n *\n * Stub-level: no real Notification API calls, no real channel registry.\n * Host apps wire a real backend via\n * `runtime.registerService('notify', realHandler)`.\n *\n * Coexistence note: this is SEPARATE from\n * `packages/services/src/notification-service.ts`, which is the legacy\n * ifc-topic-based notification registry (operates on `ifc.emit` topics\n * under `notifications:*`). `notify-service.ts` is the canonical\n * `@napplet/nub/notify` NIP-5D path and lives alongside the legacy module.\n * If the host app registers this service via\n * `runtime.registerService('notify', ...)`, @napplet/nub/notify messages\n * land here; the legacy ifc-emit topic path remains untouched.\n *\n * Shell -> napplet push messages (`notify.action`, `notify.clicked`,\n * `notify.dismissed`, `notify.controls`) are not emitted by this stub —\n * they are the host app's responsibility and are deferred to a future plan.\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n NotifySendMessage,\n NotifySendResultMessage,\n NotifyPermissionRequestMessage,\n NotifyPermissionResultMessage,\n} from '@napplet/nub/notify/types';\n\n/** Notify service version — follows semver. */\nconst NOTIFY_SERVICE_VERSION = '1.0.0';\n\n/**\n * Optional configuration for `createNotifyService`.\n *\n * @example\n * ```ts\n * const notify = createNotifyService({\n * generateId: () => crypto.randomUUID(),\n * defaultGrant: false,\n * onSend: (windowId, msg) => console.log(`napplet ${windowId} sent ${msg.title}`),\n * });\n * runtime.registerService('notify', notify);\n * ```\n */\nexport interface NotifyServiceOptions {\n /**\n * Generate a shell-assigned notification ID for `notify.send.result`.\n * Default: a monotonically increasing `shell-<n>` counter.\n */\n generateId?: () => string;\n /**\n * Default permission grant for `notify.permission.request`.\n * Host apps that want real permission prompts should replace this\n * service with a real backend via `runtime.registerService`.\n * Default: `true`.\n */\n defaultGrant?: boolean;\n /**\n * Called synchronously when a napplet dispatches `notify.send`.\n * Intended for host-app plumbing (UI toast, logging, etc.) without\n * requiring a full backend replacement.\n */\n onSend?: (windowId: string, payload: NotifySendMessage) => void;\n}\n\n/**\n * Create a stub-level notify service handler.\n *\n * Answers the 5 napplet->shell request types from `@napplet/nub/notify`.\n * Does NOT implement a real backend (no DOM Notification API, no channel\n * registry, no permission prompt). Host apps replace this via\n * `runtime.registerService('notify', realHandler)` when a real backend is\n * needed.\n *\n * @param options - Optional service configuration (see NotifyServiceOptions)\n * @returns A ServiceHandler to register with the runtime under domain `notify`\n *\n * @example\n * ```ts\n * import { createNotifyService } from '@kehto/services';\n *\n * const notify = createNotifyService();\n * runtime.registerService('notify', notify);\n * ```\n */\nexport function createNotifyService(options: NotifyServiceOptions = {}): ServiceHandler {\n let counter = 0;\n const gen = options.generateId ?? ((): string => {\n counter += 1;\n return `shell-${counter}`;\n });\n const defaultGrant = options.defaultGrant ?? true;\n\n const descriptor: ServiceDescriptor = {\n name: 'notify',\n version: NOTIFY_SERVICE_VERSION,\n description: 'NIP-5D notify NUB reference handler (stub)',\n };\n\n return {\n descriptor,\n\n handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void {\n switch (message.type) {\n case 'notify.send': {\n const m = message as NotifySendMessage;\n options.onSend?.(windowId, m);\n const result: NotifySendResultMessage = {\n type: 'notify.send.result',\n id: m.id,\n notificationId: gen(),\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'notify.dismiss':\n case 'notify.badge':\n case 'notify.channel.register':\n // state to mutate; host apps with real backends override.\n return;\n\n case 'notify.permission.request': {\n const m = message as NotifyPermissionRequestMessage;\n const result: NotifyPermissionResultMessage = {\n type: 'notify.permission.result',\n id: m.id,\n granted: defaultGrant,\n };\n send(result as NappletMessage);\n return;\n }\n\n default: {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown notify method: ${message.type}`,\n } as NappletMessage);\n }\n }\n },\n\n onWindowDestroyed(_windowId: string): void {\n // No per-window state in the stub. Host apps with real backends\n // override to clean up pending notifications / channel registrations.\n },\n };\n}\n","/**\n * theme-service.ts — NIP-5D theme NUB reference service.\n *\n * Handles the single napplet->shell request type from @napplet/nub/theme:\n * - `theme.get` -> `theme.get.result { theme }` (current theme)\n *\n * Exposes a host-facing `publishTheme(theme)` handle that:\n * 1. Replaces the service's internal current theme.\n * 2. Invokes `options.onBroadcast(envelope)` synchronously with a\n * `theme.changed` envelope so the shell adapter (Plan 13-02) can\n * fan-out the push to every registered napplet.\n * 3. Returns the envelope so callers can use it directly if they prefer.\n *\n * The default theme values are centralized here and mirrored in the runtime's\n * fallback path (`packages/runtime/src/runtime.ts`) — runtime does NOT import\n * from @kehto/services because services depends on runtime (one-way only).\n *\n * Host apps replace this via `runtime.registerService('theme', realHandler)`\n * when real CSS injection / storage / dark-mode logic is needed.\n *\n * @example\n * ```ts\n * import { createThemeService } from '@kehto/services';\n *\n * const theme = createThemeService({\n * onBroadcast: (envelope) => broadcastToAllNapplets(envelope),\n * });\n * runtime.registerService('theme', theme.handler);\n *\n * // Later, when the user flips dark/light mode:\n * theme.publishTheme(newTheme);\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from\n// @napplet/core v0.2.0+ (napplet phase-81). Re-exported from\n// @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n Theme,\n ThemeChangedMessage,\n ThemeGetResultMessage,\n} from '@napplet/nub/theme/types';\n\n/** Theme service version — follows semver. */\nconst THEME_SERVICE_VERSION = '1.0.0';\n\n/**\n * Canonical default theme values.\n *\n * Must stay synchronized with the runtime fallback constant at\n * `packages/runtime/src/runtime.ts` (THEME_FALLBACK_DEFAULT). The runtime\n * keeps a local copy instead of importing from @kehto/services to avoid a\n * runtime->services dependency (services depends on runtime; one-way).\n */\nconst DEFAULT_THEME: Theme = {\n colors: {\n background: '#0a0a0a',\n text: '#e0e0e0',\n primary: '#7aa2f7',\n },\n // fonts, background, title intentionally undefined — all optional per\n // @napplet/nub/theme Theme interface.\n};\n\n/**\n * Configuration for `createThemeService`.\n *\n * @example\n * ```ts\n * const theme = createThemeService({\n * initialTheme: { colors: { background: '#fff', text: '#000', primary: '#00f' } },\n * onBroadcast: (envelope) => shellBridge.broadcastToAll(envelope),\n * });\n * ```\n */\nexport interface ThemeServiceOptions {\n /**\n * Override the default theme payload. If omitted, the service starts with\n * the canonical defaults (`#0a0a0a / #e0e0e0 / #7aa2f7`).\n */\n initialTheme?: Theme;\n\n /**\n * Called synchronously from `publishTheme(theme)` with a `theme.changed`\n * envelope. Intended for the shell adapter (Plan 13-02) to fan-out the\n * push to every registered napplet via the runtime's sendToNapplet\n * primitive.\n *\n * Keep this callback shape framework-agnostic — the service does NOT\n * import any shell / browser APIs.\n */\n onBroadcast?: (envelope: ThemeChangedMessage) => void;\n}\n\n/**\n * A theme service bundle — the ServiceHandler that handles `theme.*`\n * envelopes, the host-facing `publishTheme(theme)` handle for theme-change\n * broadcasts, and a `getCurrentTheme()` accessor for host-side reads.\n */\nexport interface ThemeService {\n /** Register this with the runtime via `runtime.registerService('theme', handler)`. */\n handler: ServiceHandler;\n\n /**\n * Publish a theme-change to the shell adapter. Updates the service's\n * internal current theme, invokes `options.onBroadcast` with a\n * `theme.changed` envelope, and returns the envelope.\n *\n * @param theme - The new theme payload\n * @returns A `theme.changed` envelope (same one passed to onBroadcast)\n */\n publishTheme: (theme: Theme) => ThemeChangedMessage;\n\n /** Return the current theme. Equivalent to the payload a napplet's `theme.get` would receive. */\n getCurrentTheme: () => Theme;\n}\n\n/**\n * Create a theme service that handles the NIP-5D `theme.*` NUB.\n *\n * Answers `theme.get` with the current theme (default or\n * `options.initialTheme`). Exposes `publishTheme(theme)` for the host app to\n * broadcast theme changes to every registered napplet — the shell adapter\n * (Plan 13-02) wires `onBroadcast` to `runtime.sendToNapplet` fan-out.\n *\n * @param options - Optional service configuration (see ThemeServiceOptions)\n * @returns A ThemeService bundle ready for `runtime.registerService('theme', service.handler)`\n *\n * @example\n * ```ts\n * import { createThemeService } from '@kehto/services';\n *\n * const theme = createThemeService();\n * runtime.registerService('theme', theme.handler);\n * theme.publishTheme({ colors: { background: '#fff', text: '#000', primary: '#00f' } });\n * ```\n */\nexport function createThemeService(options: ThemeServiceOptions = {}): ThemeService {\n let currentTheme: Theme = options.initialTheme ?? DEFAULT_THEME;\n\n const descriptor: ServiceDescriptor = {\n name: 'theme',\n version: THEME_SERVICE_VERSION,\n description: 'NIP-5D theme NUB reference handler',\n };\n\n const handler: ServiceHandler = {\n descriptor,\n\n handleMessage(\n _windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n const id = (message as NappletMessage & { id?: string }).id ?? '';\n\n if (message.type === 'theme.get') {\n const result: ThemeGetResultMessage = {\n type: 'theme.get.result',\n id,\n theme: currentTheme,\n };\n send(result as NappletMessage);\n return;\n }\n\n send({\n type: `${message.type}.error`,\n id,\n error: `Unknown theme method: ${message.type}`,\n } as NappletMessage);\n },\n\n // Theme service has no per-window state to clean up.\n onWindowDestroyed(_windowId: string): void {\n /* no-op */\n },\n };\n\n function publishTheme(theme: Theme): ThemeChangedMessage {\n currentTheme = theme;\n const envelope: ThemeChangedMessage = { type: 'theme.changed', theme };\n options.onBroadcast?.(envelope);\n return envelope;\n }\n\n function getCurrentTheme(): Theme {\n return currentTheme;\n }\n\n return { handler, publishTheme, getCurrentTheme };\n}\n","/**\n * config-service.ts — NUB-CONFIG reference service (9th NUB domain, v1.7 Phase 39).\n *\n * Shell-side reference implementation for the canonical NUB-CONFIG wire\n * protocol (`@napplet/nub/config`, published at `^0.3.0`). Handles the full\n * 8-message discriminated union: 5 napplet→shell request types + 3\n * shell→napplet result/push types.\n *\n * ──────────────────────────── SCOPE BOUNDARY (CONFIG-04) ─────────────────────────\n * NUB-CONFIG is **shell-managed per-napplet configuration**. Napplets observe\n * values via `config.get` (one-shot) or `config.subscribe` (snapshot + live\n * push). The shell is the **sole writer** — there is intentionally **NO**\n * `config.set` wire message. Napplets cannot mutate configuration values;\n * the shell owns persistence and the update flow.\n *\n * Do NOT use this service as a general key-value store. NUB-STORAGE\n * (`state:read` / `state:write`) remains the general KV surface. Using\n * NUB-CONFIG to store e.g. `{ lastScrollPosition: 420 }` is an anti-pattern\n * (H-07 in PITFALLS.md) — such state belongs in NUB-STORAGE.\n * ──────────────────────────────────────────────────────────────────────────────────\n *\n * Host integration: provide `getValues()` returning the current\n * `ConfigValues` snapshot. Call the returned `publishValues(newValues)`\n * whenever the configuration changes — the service fans the new snapshot\n * out to every napplet that has an active `config.subscribe`.\n *\n * Optional: provide `registerSchema` to accept napplet-declared schemas at\n * runtime (the ref impl does a minimal shape check using the Core Subset\n * validator; use `ajv` in host impls that need strict draft-07 conformance).\n * Provide `openSettings` to open a shell-side UI for the napplet (no\n * response envelope — fire-and-forget UI hook).\n *\n * @example\n * ```ts\n * import { createConfigService } from '@kehto/services';\n *\n * const configFixtures = { theme: 'dark', density: 'compact', recentSearches: [] };\n * const config = createConfigService({\n * getValues: () => ({ ...configFixtures }),\n * });\n * runtime.registerService('config', config.handler);\n *\n * // Later, when shell-side values change:\n * configFixtures.theme = 'light';\n * config.publishValues({ ...configFixtures });\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\n// DRIFT-CORE-06 — Phase 11-deviation: ServiceDescriptor dropped from @napplet/core\n// v0.2.0+ (napplet phase-81). Re-exported from @kehto/runtime (canonical home after Phase 24 DRIFT-01).\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n ConfigGetMessage,\n ConfigRegisterSchemaMessage,\n ConfigOpenSettingsMessage,\n ConfigValues,\n ConfigValuesMessage,\n ConfigRegisterSchemaResultMessage,\n ConfigSchemaErrorCode,\n NappletConfigSchema,\n} from '@napplet/nub/config/types';\n\n/** Config service version — follows semver. */\nconst CONFIG_SERVICE_VERSION = '1.0.0';\n\n/**\n * Shape returned by a successful `registerSchema` result (ok=true) or a\n * rejection (ok=false + code + error). Mirrors the wire envelope fields.\n */\nexport type ConfigSchemaValidation =\n | { ok: true }\n | { ok: false; code: ConfigSchemaErrorCode; error: string };\n\n/**\n * Configuration options for `createConfigService` (options-as-bridge\n * per v1.6 Decision 18).\n *\n * @example\n * ```ts\n * const config = createConfigService({\n * getValues: () => ({ theme: 'dark', density: 'compact' }),\n * openSettings: (windowId, section) => showSettingsPanel(windowId, section),\n * });\n * ```\n */\nexport interface ConfigServiceOptions {\n /**\n * Returns the current configuration values snapshot.\n * Called on every `config.get` and at every `config.subscribe` initial push.\n * Implementations should return a fresh object (not a mutable reference).\n */\n getValues(): ConfigValues;\n\n /**\n * Optional: receive notification when a napplet subscribes to config updates.\n * Fire-and-forget — the service tracks the subscription internally regardless.\n */\n onSubscribe?: (windowId: string) => void;\n\n /**\n * Optional: receive notification when a napplet unsubscribes.\n */\n onUnsubscribe?: (windowId: string) => void;\n\n /**\n * Optional: validate and store a napplet-provided schema.\n *\n * If omitted, the ref impl runs its own Core Subset check (hand-coded\n * validator; 30-50 lines) and returns ok/reject. Hosts that need strict\n * draft-07 conformance should provide an ajv-backed implementation.\n *\n * Return shape mirrors `config.registerSchema.result` wire envelope\n * (minus the `id` — the dispatch layer correlates).\n */\n registerSchema?: (\n windowId: string,\n schema: NappletConfigSchema,\n version: number | undefined,\n ) => ConfigSchemaValidation;\n\n /**\n * Optional: open the shell-side settings UI for this napplet.\n * Fire-and-forget — no response envelope per the wire spec.\n * If omitted, `config.openSettings` is silently dropped (D10 allows\n * the config-demo napplet to function without a settings UI).\n */\n openSettings?: (windowId: string, section: string | undefined) => void;\n}\n\n/**\n * NUB-CONFIG reference service bundle — `handler` to register with the\n * runtime, `publishValues` for the host app to push updates live to all\n * subscribed napplets.\n */\nexport interface ConfigService {\n /** Register this with the runtime via `runtime.registerService('config', handler)`. */\n handler: ServiceHandler;\n\n /**\n * Broadcast a new values snapshot to every napplet with an active\n * `config.subscribe`. Each subscriber receives a `config.values` envelope\n * with no `id` (push form per wire spec — absence of `id` distinguishes\n * push from correlated `config.get` response).\n *\n * @param values - The new configuration snapshot (full object, not a diff)\n */\n publishValues(values: ConfigValues): void;\n}\n\n/**\n * Minimal JSON Schema validator covering the NUB-CONFIG Core Subset:\n * type: object / string / number / boolean / array, required[], default, properties.\n *\n * Explicitly rejects: $ref, pattern, oneOf/anyOf/allOf/not, if/then/else.\n * Returns { ok: true } on shape sanity, otherwise an error code per the\n * canonical ConfigSchemaErrorCode union.\n *\n * Host apps that need strict draft-07 conformance should supply a custom\n * `registerSchema` callback backed by ajv@8.\n */\nfunction validateCoreSubset(schema: unknown): ConfigSchemaValidation {\n if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {\n return { ok: false, code: 'invalid-schema', error: 'schema root must be an object' };\n }\n const s = schema as Record<string, unknown>;\n\n // Reject forbidden keywords (NUB-CONFIG Core Subset limits per spec).\n if ('$ref' in s) {\n return { ok: false, code: 'ref-not-allowed', error: '$ref is not permitted in the Core Subset' };\n }\n if ('pattern' in s) {\n return {\n ok: false,\n code: 'pattern-not-allowed',\n error: 'pattern is not permitted in the Core Subset',\n };\n }\n if ('oneOf' in s || 'anyOf' in s || 'allOf' in s || 'not' in s) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: 'oneOf/anyOf/allOf/not are not permitted in the Core Subset',\n };\n }\n if ('if' in s || 'then' in s || 'else' in s) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: 'if/then/else are not permitted in the Core Subset',\n };\n }\n if (s.type !== 'object') {\n return { ok: false, code: 'invalid-schema', error: 'schema root must have type: \"object\"' };\n }\n\n // Shallow properties check: each declared property must use a supported type.\n const props = s.properties;\n if (props !== undefined && (typeof props !== 'object' || props === null)) {\n return { ok: false, code: 'invalid-schema', error: 'properties must be an object' };\n }\n if (props) {\n for (const [key, val] of Object.entries(props as Record<string, unknown>)) {\n if (typeof val !== 'object' || val === null) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: `property \"${key}\" must be an object schema`,\n };\n }\n const pv = val as Record<string, unknown>;\n const ALLOWED_TYPES = new Set(['string', 'number', 'boolean', 'array', 'object']);\n if (pv.type !== undefined && !ALLOWED_TYPES.has(pv.type as string)) {\n return {\n ok: false,\n code: 'invalid-schema',\n error: `property \"${key}\" must have type: string|number|boolean|array|object`,\n };\n }\n }\n }\n\n return { ok: true };\n}\n\n/**\n * Create a NUB-CONFIG reference service.\n *\n * Shell-writes, napplet-reads. Handles the full `@napplet/nub/config` wire\n * protocol: `config.get` (correlated snapshot), `config.subscribe` /\n * `config.unsubscribe` (live push stream), `config.registerSchema` (optional\n * schema registration + Core Subset validation), `config.openSettings`\n * (optional UI deep-link, fire-and-forget).\n *\n * Returns a `ConfigService` bundle: `{ handler, publishValues }`.\n * Register `handler` with the runtime; call `publishValues(newValues)` from\n * the shell whenever config state changes.\n *\n * @param options - Host-supplied implementation hooks (options-as-bridge,\n * v1.6 Decision 18). `getValues` is required; all other fields are optional.\n * @returns A ConfigService bundle.\n *\n * @see ConfigServiceOptions for the options shape.\n * @see packages/services/src/theme-service.ts for the sibling pattern.\n * @see SCOPE BOUNDARY comment at the top of this file re: NUB-STORAGE separation.\n *\n * @example\n * ```ts\n * import { createConfigService } from '@kehto/services';\n *\n * const config = createConfigService({\n * getValues: () => ({ theme: 'dark', density: 'compact' }),\n * openSettings: (windowId, section) => openSettingsUI(section),\n * });\n * runtime.registerService('config', config.handler);\n *\n * // Push a live update to all subscribers:\n * config.publishValues({ theme: 'light', density: 'compact' });\n * ```\n */\nexport function createConfigService(options: ConfigServiceOptions): ConfigService {\n /**\n * Per-window subscriber set. Maps windowId → the send callback captured at\n * `config.subscribe` time. `publishValues` fans out to every entry.\n */\n const subscribers = new Map<string, (msg: NappletMessage) => void>();\n\n const descriptor: ServiceDescriptor = {\n name: 'config',\n version: CONFIG_SERVICE_VERSION,\n description: 'NUB-CONFIG reference service — shell-writes, napplet-reads configuration',\n };\n\n const handler: ServiceHandler = {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'config.get': {\n const m = message as ConfigGetMessage;\n const reply: ConfigValuesMessage = {\n type: 'config.values',\n id: m.id,\n values: options.getValues(),\n };\n send(reply as NappletMessage);\n return;\n }\n\n case 'config.subscribe': {\n // Capture the send callback so publishValues can fan pushes out.\n subscribers.set(windowId, send);\n // Immediate initial snapshot push — no `id` (push form per wire spec).\n const push: ConfigValuesMessage = {\n type: 'config.values',\n values: options.getValues(),\n };\n send(push as NappletMessage);\n options.onSubscribe?.(windowId);\n return;\n }\n\n case 'config.unsubscribe': {\n subscribers.delete(windowId);\n options.onUnsubscribe?.(windowId);\n return;\n }\n\n case 'config.registerSchema': {\n const m = message as ConfigRegisterSchemaMessage;\n // Delegate to host-supplied validator if present; otherwise use\n // the built-in Core Subset hand-coded validator (D12).\n const validation: ConfigSchemaValidation = options.registerSchema\n ? options.registerSchema(windowId, m.schema, m.version)\n : validateCoreSubset(m.schema);\n\n const result: ConfigRegisterSchemaResultMessage = validation.ok\n ? { type: 'config.registerSchema.result', id: m.id, ok: true }\n : {\n type: 'config.registerSchema.result',\n id: m.id,\n ok: false,\n code: validation.code,\n error: validation.error,\n };\n send(result as NappletMessage);\n return;\n }\n\n case 'config.openSettings': {\n const m = message as ConfigOpenSettingsMessage;\n // Silently dropped if openSettings hook not provided (D10).\n options.openSettings?.(windowId, m.section);\n return;\n }\n\n default:\n // Unknown config.* message — silently ignored per NIP-5D.\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n // A napplet iframe was destroyed — drop any active subscription so we\n // don't retain the stale send callback in the subscribers map.\n subscribers.delete(windowId);\n },\n };\n\n /**\n * Broadcast a new config values snapshot to every subscribed napplet.\n * Each subscriber receives a `config.values` push envelope (no `id` —\n * absence of `id` distinguishes a push from a correlated `config.get`\n * response per the NUB-CONFIG wire spec).\n */\n function publishValues(values: ConfigValues): void {\n const envelope: ConfigValuesMessage = {\n type: 'config.values',\n values,\n };\n for (const send of subscribers.values()) {\n try {\n send(envelope as NappletMessage);\n } catch {\n // Subscriber's send callback threw (e.g., iframe gone without\n // onWindowDestroyed firing yet). Best-effort — drop silently.\n }\n }\n }\n\n return { handler, publishValues };\n}\n","/**\n * resource-service.ts — NUB-RESOURCE reference service (10th NUB domain, v1.7 Phase 40).\n *\n * Shell-side reference implementation for the canonical NUB-RESOURCE wire\n * protocol (`internal-resource.ts` in @kehto/shell/src/types; kehto-internal\n * model per PROJECT.md Decision #31 — diverges from upstream `@napplet/nub/\n * resource` in field names + error vocabulary). Handles the canonical\n * 4-message protocol:\n * Inbound: resource.bytes, resource.cancel\n * Outbound: resource.bytes.result, resource.bytes.error\n *\n * ──────────────────────── SCOPE BOUNDARY (RESOURCE-01) ────────────────────────\n * NUB-RESOURCE is an **authenticated fetch proxy** — read-only, atomic.\n *\n * This service is NOT responsible for:\n * - Streaming / chunked responses (host-app concern)\n * - Response caching / conditional requests (host-app concern)\n * - Upload / POST body construction (NUB-RESOURCE v1.7 is read-only)\n * - Redirect limits, MIME sniffing, SVG rasterization (host-fetch concern)\n * - Private-IP blocking, SSRF mitigation (host-provided-fetch responsibility)\n *\n * These belong to the host-app's `fetch` implementation per D7 and\n * SHELL-RESOURCE-POLICY.md (Phase 40 Plan 40-03). Kehto ships a reference\n * service; production hardening is the host app's concern.\n * ──────────────────────────────────────────────────────────────────────────────\n *\n * Host integration: provide `fetch`, `isOriginGranted`, `getConnectGrants`,\n * and `resolveIdentity`. ALL FOUR are required from day one (H-03 prevention).\n *\n * @example\n * ```ts\n * import { createResourceService } from '@kehto/services';\n *\n * const resourceSvc = createResourceService({\n * fetch: (url, init) => globalThis.fetch(url, init),\n * isOriginGranted: (origin, grants) => grants.includes(origin),\n * getConnectGrants: (dTag, hash) => connectStore.getOrigins(dTag, hash),\n * resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,\n * });\n * runtime.registerService('resource', resourceSvc);\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\n\n/** Resource service version — follows semver. */\nconst RESOURCE_SERVICE_VERSION = '1.0.0';\n\n/**\n * Options for `createResourceService` (options-as-bridge per v1.6 Decision 18).\n *\n * ALL FOUR fields are required. The factory throws at construction if any is\n * missing — H-03 prevention: the grants source (`getConnectGrants`) MUST be\n * wired from day one so there is no window where resource requests bypass the\n * grant check.\n *\n * @see PITFALLS.md:228 (H-03) — grants-source coupling must be present at construction\n */\nexport interface ResourceServiceOptions {\n /**\n * Host-supplied fetch implementation. Receives the URL, a partial init\n * (method, headers, signal), and must return a `Response`-compatible promise.\n *\n * The host's `fetch` is the ONLY place to implement redirect limits, MIME\n * sniffing, SVG rasterization, private-IP / SSRF blocking, etc.\n * This service does NOT filter: it proxies transparently.\n *\n * @param url - The URL from the resource.bytes request\n * @param init - Method, headers (from napplet), and an AbortSignal\n */\n fetch(\n url: string,\n init: { method?: string; headers?: Record<string, string>; signal: AbortSignal }\n ): Promise<Response>;\n\n /**\n * Returns true if `origin` is present in `grants` (the list returned by\n * `getConnectGrants` for the napplet's dTag + aggregateHash).\n *\n * The reference implementation is simply `grants.includes(origin)`. Host apps\n * may provide normalized-origin comparison if needed.\n *\n * @param origin - Parsed origin of the requested URL (scheme + host + port)\n * @param grants - Readonly list from getConnectGrants for this napplet identity\n */\n isOriginGranted(origin: string, grants: readonly string[]): boolean;\n\n /**\n * Returns the list of allowed fetch origins for the given napplet identity.\n * Called on every `resource.bytes` request — must be synchronous and fast.\n *\n * Typically wraps `connectStore.getOrigins(dTag, aggregateHash)` from\n * @kehto/shell.\n *\n * H-03 prevention: REQUIRED from day one — factory throws on construction\n * if omitted.\n *\n * @param dTag - The napplet's d-tag (from session registry)\n * @param aggregateHash - The napplet's aggregate hash (from session registry)\n */\n getConnectGrants(dTag: string, aggregateHash: string): readonly string[];\n\n /**\n * Resolve a windowId to the napplet's identity (dTag + aggregateHash).\n * Returns null if the window is not in the session registry.\n *\n * Typically wraps `sessionRegistry.getEntryByWindowId(windowId)`.\n *\n * @param windowId - The iframe window identifier\n */\n resolveIdentity(windowId: string): { dTag: string; aggregateHash: string } | null;\n}\n\n/**\n * Type alias for the ServiceHandler returned by `createResourceService`.\n * Exported for host apps that need to type-annotate the handler reference.\n */\nexport type ResourceService = ServiceHandler;\n\n/**\n * Convert an ArrayBuffer to base64 string, safe for both browser and Node.\n * Chunked in 0x8000-byte slices to avoid `String.fromCharCode(...largeArray)`\n * stack overflow on large responses.\n */\nfunction arrayBufferToBase64(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf);\n const CHUNK = 0x8000; // 32 KB\n let binary = '';\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n}\n\ninterface ResourceRequestState {\n inFlight: Map<string, { controller: AbortController; windowId: string }>;\n perWindow: Map<string, Set<string>>;\n}\n\nfunction assertResourceOptions(options: ResourceServiceOptions): void {\n if (\n typeof options?.fetch !== 'function' ||\n typeof options?.isOriginGranted !== 'function' ||\n typeof options?.getConnectGrants !== 'function' ||\n typeof options?.resolveIdentity !== 'function'\n ) {\n throw new Error(\n '[RESOURCE-01 / H-03] createResourceService requires {fetch, isOriginGranted, getConnectGrants, resolveIdentity} ' +\n '— all four options are required from day one. ' +\n 'The grants source (getConnectGrants) MUST be wired at construction time to prevent unguarded fetch proxying.',\n );\n }\n}\n\nfunction trackRequest(\n state: ResourceRequestState,\n requestId: string,\n windowId: string,\n controller: AbortController,\n): void {\n state.inFlight.set(requestId, { controller, windowId });\n if (!state.perWindow.has(windowId)) {\n state.perWindow.set(windowId, new Set());\n }\n state.perWindow.get(windowId)!.add(requestId);\n}\n\nfunction untrackRequest(state: ResourceRequestState, requestId: string): void {\n const entry = state.inFlight.get(requestId);\n if (entry) {\n state.inFlight.delete(requestId);\n state.perWindow.get(entry.windowId)?.delete(requestId);\n }\n}\n\nfunction sendResourceError(\n send: (m: NappletMessage) => void,\n requestId: string,\n code: 'denied' | 'invalid-url' | 'canceled' | 'network-error',\n message: string,\n): void {\n send({\n type: 'resource.bytes.error',\n requestId,\n code,\n message,\n } as NappletMessage);\n}\n\nfunction parseResourceUrl(send: (m: NappletMessage) => void, requestId: string, url: string): URL | null {\n try {\n return new URL(url);\n } catch {\n sendResourceError(send, requestId, 'invalid-url', `invalid URL: ${url}`);\n return null;\n }\n}\n\nfunction collectResponseHeaders(response: Response): Record<string, string> {\n const headers: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n headers[key] = value;\n });\n return headers;\n}\n\nasync function handleBytes(\n options: ResourceServiceOptions,\n state: ResourceRequestState,\n windowId: string,\n msg: { requestId: string; url: string; init?: { method?: string; headers?: Readonly<Record<string, string>> } },\n send: (m: NappletMessage) => void,\n): Promise<void> {\n const { requestId, url, init } = msg;\n const identity = options.resolveIdentity(windowId);\n if (!identity) {\n sendResourceError(send, requestId, 'denied', 'napplet identity not resolvable');\n return;\n }\n\n const parsedUrl = parseResourceUrl(send, requestId, url);\n if (!parsedUrl) return;\n\n const origin = parsedUrl.origin;\n const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);\n if (!options.isOriginGranted(origin, grants)) {\n sendResourceError(send, requestId, 'denied', `origin ${origin} not granted`);\n return;\n }\n\n const controller = new AbortController();\n trackRequest(state, requestId, windowId, controller);\n\n try {\n const response = await options.fetch(url, {\n method: init?.method,\n headers: init?.headers ? { ...init.headers } : undefined,\n signal: controller.signal,\n });\n const buffer = await response.arrayBuffer();\n send({\n type: 'resource.bytes.result',\n requestId,\n status: response.status,\n headers: collectResponseHeaders(response),\n bodyBase64: arrayBufferToBase64(buffer),\n } as NappletMessage);\n } catch (err: unknown) {\n const isAbort =\n controller.signal.aborted ||\n (err instanceof Error && (err.name === 'AbortError' || err.name === 'DOMException'));\n sendResourceError(\n send,\n requestId,\n isAbort ? 'canceled' : 'network-error',\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n untrackRequest(state, requestId);\n }\n}\n\nfunction handleCancel(state: ResourceRequestState, requestId: string): void {\n const entry = state.inFlight.get(requestId);\n if (entry) {\n entry.controller.abort();\n }\n}\n\nfunction destroyWindowRequests(state: ResourceRequestState, windowId: string): void {\n const requestIds = state.perWindow.get(windowId);\n if (!requestIds) return;\n for (const requestId of requestIds) {\n const entry = state.inFlight.get(requestId);\n if (entry) {\n entry.controller.abort();\n state.inFlight.delete(requestId);\n }\n }\n state.perWindow.delete(windowId);\n}\n\n/**\n * Create a NUB-RESOURCE reference service.\n *\n * Implements canonical 4-message protocol: `resource.bytes` (napplet → shell\n * fetch request), `resource.cancel` (napplet → shell in-flight cancellation),\n * `resource.bytes.result` (shell → napplet success), `resource.bytes.error`\n * (shell → napplet failure/denial/cancel).\n *\n * On-construction guard (H-03 prevention): all four options are validated at\n * factory call time. If any is missing, the factory throws immediately with a\n * message containing `[RESOURCE-01 / H-03]` so misconfigured shell apps fail\n * loudly at startup rather than silently at first dispatch.\n *\n * Returns a `ServiceHandler` (no `publishValues`-style surface — resource has\n * no shell-initiated push beyond the response/error path).\n *\n * @param options - REQUIRED: fetch, isOriginGranted, getConnectGrants, resolveIdentity\n * @returns ServiceHandler to register via `runtime.registerService('resource', handler)`\n *\n * @example\n * ```ts\n * import { createResourceService } from '@kehto/services';\n *\n * const svc = createResourceService({\n * fetch: (url, init) => globalThis.fetch(url, init),\n * isOriginGranted: (origin, grants) => grants.includes(origin),\n * getConnectGrants: (dTag, hash) => connectStore.getOrigins(dTag, hash),\n * resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,\n * });\n * runtime.registerService('resource', svc);\n * ```\n */\nexport function createResourceService(options: ResourceServiceOptions): ResourceService {\n assertResourceOptions(options);\n const state: ResourceRequestState = {\n inFlight: new Map<string, { controller: AbortController; windowId: string }>(),\n perWindow: new Map<string, Set<string>>(),\n };\n\n const descriptor: ServiceDescriptor = {\n name: 'resource',\n version: RESOURCE_SERVICE_VERSION,\n description:\n 'NUB-RESOURCE reference service — shell-proxied authenticated fetch (RESOURCE-01..06)',\n };\n\n const handler: ServiceHandler = {\n descriptor,\n\n handleMessage(\n windowId: string,\n message: NappletMessage,\n send: (msg: NappletMessage) => void,\n ): void {\n switch (message.type) {\n case 'resource.bytes': {\n const m = message as NappletMessage & {\n requestId: string;\n url: string;\n init?: { method?: string; headers?: Readonly<Record<string, string>> };\n };\n handleBytes(options, state, windowId, m, send).catch(() => { /* errors surface via send() */ });\n return;\n }\n\n case 'resource.cancel': {\n const m = message as NappletMessage & { requestId: string };\n handleCancel(state, m.requestId);\n return;\n }\n\n default:\n // Unknown resource.* message — silently ignored per NIP-5D.\n return;\n }\n },\n\n onWindowDestroyed(windowId: string): void {\n destroyWindowRequests(state, windowId);\n },\n };\n\n return handler;\n}\n","/**\n * outbox-service.ts — NAP-OUTBOX (outbox-aware relay routing) reference service.\n *\n * Shell-side handler for the NAP-OUTBOX wire protocol. It is a pure envelope\n * router: it validates `outbox.*` envelopes, delegates the actual relay\n * discovery / routing / dedup / publish-fanout work to an injected\n * {@link OutboxRouter}, and posts the correlated result / lifecycle messages\n * (echoing the request `id` or `subId`) back to the napplet.\n *\n * The router is injected (options-as-bridge) so this service has no Nostr\n * dependency and is fully unit-testable. A concrete relay-pool-backed router\n * ships alongside as {@link createRelayPoolOutboxRouter}.\n *\n * ──────────────────────────── Responsibilities ────────────────────────────\n * Inbound: outbox.query, outbox.subscribe, outbox.close, outbox.publish,\n * outbox.resolveRelays\n * Outbound: outbox.query.result, outbox.event, outbox.eose, outbox.closed,\n * outbox.publish.result, outbox.resolveRelays.result\n *\n * The shell owns relay discovery, routing, fallback, deduplication, signature\n * validation, signing, and publish fanout policy — all of which live behind\n * the {@link OutboxRouter}. This service only marshals the wire protocol.\n *\n * @example\n * ```ts\n * import { createOutboxService, createRelayPoolOutboxRouter } from '@kehto/services';\n *\n * const router = createRelayPoolOutboxRouter({ relayPool, loadRelayLists, fallbackRelays });\n * runtime.registerService('outbox', createOutboxService({ router }));\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { EventTemplate, NappletMessage, NostrEvent, NostrFilter } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\n\n/** Outbox service version — follows semver. */\nconst OUTBOX_SERVICE_VERSION = '1.0.0';\n\n/**\n * Relay-selection strategy:\n * - `outbox` — query/publish via author write relays (the outbox model)\n * - `inbox` — query/publish via recipient read relays (the inbox model)\n * - `auto` — let the shell choose per its policy and relay intelligence\n */\nexport type OutboxStrategy = 'outbox' | 'inbox' | 'auto';\n\n/** Options for a one-shot outbox query. */\nexport interface OutboxQueryOptions {\n /** Explicit author hints (augment/override authors derived from filters). */\n authors?: string[];\n /** Relay hints; treated as a hint subject to shell validation, not a bypass. */\n relays?: string[];\n /** Relay-selection strategy. */\n strategy?: OutboxStrategy;\n /** Maximum events to collect. */\n limit?: number;\n /** Wall-clock budget for the query, in milliseconds. */\n timeoutMs?: number;\n}\n\n/** Options for a live outbox subscription. */\nexport interface OutboxSubscribeOptions extends OutboxQueryOptions {\n /** Keep the subscription open for real-time events after EOSE. */\n live?: boolean;\n}\n\n/** Options for an outbox publish. */\nexport interface OutboxPublishOptions {\n /** Relay hints; treated as a hint subject to shell validation. */\n relays?: string[];\n /** Recipient authors whose inbox relays should be included for directed events. */\n targetAuthors?: string[];\n /** Relay-selection strategy. */\n strategy?: OutboxStrategy;\n}\n\n/** A read/write target for relay-plan resolution. */\nexport interface OutboxTarget {\n /** Authors to resolve relays for. */\n authors?: string[];\n /** Single pubkey to resolve relays for. */\n pubkey?: string;\n /** Whether the plan is for reading (their write relays) or writing (their read relays). */\n direction?: 'read' | 'write';\n /** Relay-selection strategy. */\n strategy?: OutboxStrategy;\n}\n\n/** The relay plan the shell would use for a target. */\nexport interface OutboxRelayPlan {\n /** Resolved relay URLs. */\n relays: string[];\n /** Where the plan came from. */\n source: 'nip65' | 'cache' | 'policy' | 'fallback';\n /** Authors for which no relay list could be resolved. */\n missingAuthors?: string[];\n}\n\n/** Outcome of an outbox query, as returned by the {@link OutboxRouter}. */\nexport interface OutboxResult {\n /** Deduplicated, signature-validated events. */\n events: NostrEvent[];\n /** Map of event id -> relay URLs where the shell observed the event. */\n relays: Record<string, string[]>;\n /** True when some relay lists or connections failed and results are partial. */\n incomplete?: boolean;\n /** Error reason when the query could not complete. */\n error?: string;\n}\n\n/** Outcome of an outbox publish, as returned by the {@link OutboxRouter}. */\nexport interface OutboxPublishResult {\n /** Whether the publish succeeded on at least the required relays. */\n ok: boolean;\n /** The signed event returned by the shell. */\n event?: NostrEvent;\n /** The published event id. */\n eventId?: string;\n /** Map of relay URL -> per-relay publish success. */\n relays?: Record<string, boolean>;\n /** Error reason when the publish failed. */\n error?: string;\n}\n\n/** Sink an {@link OutboxRouter} streams subscription lifecycle through. */\nexport interface OutboxSubscriptionSink {\n /** Deliver a matching event; `relay` is the relay it was observed on, when known. */\n event(event: NostrEvent, relay?: string): void;\n /** Signal end-of-stored-events. */\n eose(): void;\n /** Signal that the subscription was closed upstream; `reason` is optional. */\n closed(reason?: string): void;\n}\n\n/** Handle to a router-owned subscription. */\nexport interface OutboxRouterSubscription {\n /** Stop the subscription and release its relay connections. */\n close(): void;\n}\n\n/**\n * Abstract outbox router. Implementors own relay discovery (NIP-65 / NIP-66),\n * routing, fallback, deduplication, signature validation, signing, and publish\n * fanout. The service translates wire envelopes into these calls and back.\n */\nexport interface OutboxRouter {\n /** Resolve relays, query them, dedup by id, validate signatures, collect events. */\n query(filters: NostrFilter[], options?: OutboxQueryOptions): Promise<OutboxResult>;\n /** Open a live outbox-aware subscription, streaming through `sink`. */\n subscribe(\n filters: NostrFilter[],\n options: OutboxSubscribeOptions | undefined,\n sink: OutboxSubscriptionSink,\n ): OutboxRouterSubscription;\n /** Sign `template` and fan it out to the relevant write/inbox relays. */\n publish(template: EventTemplate, options?: OutboxPublishOptions): Promise<OutboxPublishResult>;\n /** Return the relay plan the shell would use for a read/write target. */\n resolveRelays(target: OutboxTarget): Promise<OutboxRelayPlan>;\n}\n\n/** Options for {@link createOutboxService}. */\nexport interface OutboxServiceOptions {\n /** The outbox router the shell uses to reach relays. Required. */\n router: OutboxRouter;\n}\n\ntype Send = (msg: NappletMessage) => void;\n\nconst OUTBOX_DESCRIPTOR: ServiceDescriptor = {\n name: 'outbox',\n version: OUTBOX_SERVICE_VERSION,\n description: 'NAP-OUTBOX outbox-aware relay routing — query/subscribe/publish/resolveRelays',\n};\n\n/**\n * Normalize a wire `filters` field (a single NIP-01 filter or an array) into a\n * filter array. Returns `null` when the input is missing or has no usable\n * filter objects.\n */\nfunction normalizeFilters(raw: unknown): NostrFilter[] | null {\n if (Array.isArray(raw)) {\n const filters = raw.filter((f): f is NostrFilter => typeof f === 'object' && f !== null);\n return filters.length > 0 ? filters : null;\n }\n if (typeof raw === 'object' && raw !== null) return [raw as NostrFilter];\n return null;\n}\n\n/**\n * Create the NAP-OUTBOX service handler.\n *\n * @param options - Must provide an {@link OutboxRouter}.\n * @returns A `ServiceHandler` ready for `runtime.registerService('outbox', handler)`.\n * @throws If `options.router` is missing.\n */\nexport function createOutboxService(options: OutboxServiceOptions): ServiceHandler {\n if (!options || typeof options.router !== 'object' || options.router === null) {\n throw new Error('createOutboxService: options.router is required');\n }\n const { router } = options;\n\n // Active subscriptions keyed by `windowId:subId` for lifecycle management.\n const subscriptions = new Map<string, OutboxRouterSubscription>();\n\n function handleQuery(msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; filters?: unknown; options?: OutboxQueryOptions };\n const id = m.id ?? '';\n const filters = normalizeFilters(m.filters);\n if (!filters) {\n send({ type: 'outbox.query.result', id, events: [], relays: {}, error: 'invalid filter' } as NappletMessage);\n return;\n }\n void router\n .query(filters, m.options)\n .then((result) =>\n send({\n type: 'outbox.query.result',\n id,\n events: result.events,\n relays: result.relays,\n ...(result.incomplete === undefined ? {} : { incomplete: result.incomplete }),\n ...(result.error === undefined ? {} : { error: result.error }),\n } as NappletMessage),\n )\n .catch((err) =>\n send({ type: 'outbox.query.result', id, events: [], relays: {}, error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n function handleSubscribe(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { subId?: string; filters?: unknown; options?: OutboxSubscribeOptions };\n const subId = m.subId;\n if (typeof subId !== 'string' || subId.length === 0) return;\n const subKey = `${windowId}:${subId}`;\n\n // Replace any existing subscription for this key.\n subscriptions.get(subKey)?.close();\n subscriptions.delete(subKey);\n\n const filters = normalizeFilters(m.filters);\n if (!filters) {\n send({ type: 'outbox.closed', subId, reason: 'invalid filter' } as NappletMessage);\n return;\n }\n\n const sink: OutboxSubscriptionSink = {\n event: (event, relay) =>\n send({ type: 'outbox.event', subId, event, ...(relay === undefined ? {} : { relay }) } as NappletMessage),\n eose: () => send({ type: 'outbox.eose', subId } as NappletMessage),\n closed: (reason) => {\n subscriptions.delete(subKey);\n send({ type: 'outbox.closed', subId, ...(reason === undefined ? {} : { reason }) } as NappletMessage);\n },\n };\n\n subscriptions.set(subKey, router.subscribe(filters, m.options, sink));\n }\n\n function handleClose(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { subId?: string };\n const subId = m.subId;\n if (typeof subId !== 'string') return;\n const subKey = `${windowId}:${subId}`;\n subscriptions.get(subKey)?.close();\n subscriptions.delete(subKey);\n send({ type: 'outbox.closed', subId } as NappletMessage);\n }\n\n function handlePublish(msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; event?: EventTemplate; options?: OutboxPublishOptions };\n const id = m.id ?? '';\n if (!m.event || typeof m.event !== 'object') {\n send({ type: 'outbox.publish.result', id, ok: false, error: 'invalid filter' } as NappletMessage);\n return;\n }\n void router\n .publish(m.event, m.options)\n .then((result) =>\n send({\n type: 'outbox.publish.result',\n id,\n ok: result.ok,\n ...(result.event === undefined ? {} : { event: result.event }),\n ...(result.eventId === undefined ? {} : { eventId: result.eventId }),\n ...(result.relays === undefined ? {} : { relays: result.relays }),\n ...(result.error === undefined ? {} : { error: result.error }),\n } as NappletMessage),\n )\n .catch((err) =>\n send({ type: 'outbox.publish.result', id, ok: false, error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n function handleResolveRelays(msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; target?: OutboxTarget };\n const id = m.id ?? '';\n if (!m.target || typeof m.target !== 'object') {\n send({ type: 'outbox.resolveRelays.result', id, error: 'invalid filter' } as NappletMessage);\n return;\n }\n void router\n .resolveRelays(m.target)\n .then((plan) => send({ type: 'outbox.resolveRelays.result', id, plan } as NappletMessage))\n .catch((err) =>\n send({ type: 'outbox.resolveRelays.result', id, error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n return {\n descriptor: OUTBOX_DESCRIPTOR,\n handleMessage(windowId: string, message: NappletMessage, send: Send): void {\n switch (message.type) {\n case 'outbox.query':\n handleQuery(message, send);\n return;\n case 'outbox.subscribe':\n handleSubscribe(windowId, message, send);\n return;\n case 'outbox.close':\n handleClose(windowId, message, send);\n return;\n case 'outbox.publish':\n handlePublish(message, send);\n return;\n case 'outbox.resolveRelays':\n handleResolveRelays(message, send);\n return;\n default:\n // Unknown outbox.* action — silently ignored (forward-compatible).\n return;\n }\n },\n onWindowDestroyed(windowId: string): void {\n const prefix = `${windowId}:`;\n for (const [key, sub] of subscriptions) {\n if (key.startsWith(prefix)) {\n sub.close();\n subscriptions.delete(key);\n }\n }\n },\n };\n}\n\nfunction toErrorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return 'outbox request failed';\n}\n","/**\n * relay-pool-outbox-router.ts — concrete {@link OutboxRouter} backed by a relay pool.\n *\n * Implements the outbox-model routing that NAP-OUTBOX centralizes so napplets\n * don't each reinvent it: derive authors, resolve their NIP-65 relays, fan a\n * per-relay subscription out across the plan, deduplicate by event id (while\n * recording every relay an event was observed on), validate signatures, and —\n * for publish — sign the template and fan it out to the relevant write/inbox\n * relays.\n *\n * NIP-65 relay-list *fetching* is the host's concern (it may come from a\n * kind-10002 cache, a NIP-66 indexer via `@kehto/nip/66`, or a live query), so\n * it is injected via {@link RelayPoolOutboxRouterOptions.loadRelayLists}. The\n * relay pool, signer, and signature verifier are injected too — keeping this\n * router browser-agnostic and unit-testable with mocks.\n *\n * Relay-selection model (per the outbox model):\n * - reading an author's events → their **write** relays (where they publish)\n * - writing to reach an author → their **read** relays (their inbox)\n *\n * `strategy` overrides the direction default: `outbox` forces write relays,\n * `inbox` forces read relays, `auto` (default) follows the read/write direction.\n *\n * @example\n * ```ts\n * import { createOutboxService, createRelayPoolOutboxRouter } from '@kehto/services';\n *\n * const router = createRelayPoolOutboxRouter({\n * relayPool: myOutboxPool,\n * loadRelayLists: (pubkeys) => relayListCache.getMany(pubkeys),\n * fallbackRelays: ['wss://relay.damus.io', 'wss://nos.lol'],\n * signEvent: (tmpl) => signer.signEvent(tmpl),\n * verifyEvent: (ev) => verifyEvent(ev),\n * });\n * runtime.registerService('outbox', createOutboxService({ router }));\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { EventTemplate, NostrEvent, NostrFilter } from '@napplet/core';\nimport type {\n OutboxRouter,\n OutboxResult,\n OutboxPublishResult,\n OutboxRelayPlan,\n OutboxQueryOptions,\n OutboxSubscribeOptions,\n OutboxPublishOptions,\n OutboxStrategy,\n OutboxTarget,\n OutboxSubscriptionSink,\n OutboxRouterSubscription,\n} from './outbox-service.js';\n\n// Timer globals available in all JS runtimes.\ndeclare function setTimeout(callback: () => void, ms: number): unknown;\ndeclare function clearTimeout(id: unknown): void;\n\n/** Default per-query wall-clock budget when `options.timeoutMs` is unset. */\nconst DEFAULT_QUERY_TIMEOUT_MS = 4000;\n\n/** A NIP-65 relay list for a single pubkey. */\nexport interface RelayListEntry {\n /** Relays the author reads from (their inbox). */\n read: string[];\n /** Relays the author writes to (where their events land). */\n write: string[];\n}\n\n/**\n * Relay pool contract the router drives. Implementors adapt their pool library\n * (nostr-tools SimplePool, applesauce-relay, etc.). Unlike the lower-level\n * relay NUB pool, both methods take an explicit relay-URL set so the router\n * controls outbox routing and can attribute events to the relay they arrived on.\n */\nexport interface OutboxRelayPool {\n /**\n * Subscribe to `filters` on exactly `relayUrls`. The callback receives each\n * matching event or the literal `'EOSE'` once stored events are exhausted.\n * Returns a handle to cancel the subscription.\n */\n subscribe(\n filters: NostrFilter[],\n relayUrls: string[],\n callback: (item: NostrEvent | 'EOSE') => void,\n ): { unsubscribe(): void };\n /**\n * Publish `event` to `relayUrls`. May return a per-relay success map; a\n * `void`/missing return is treated as optimistic success on every target.\n */\n publish(\n event: NostrEvent,\n relayUrls: string[],\n ): Promise<Record<string, boolean>> | Record<string, boolean> | void;\n /** Whether the relay pool is connected and able to handle requests. */\n isAvailable(): boolean;\n}\n\n/** Options for {@link createRelayPoolOutboxRouter}. */\nexport interface RelayPoolOutboxRouterOptions {\n /** Relay pool the router subscribes/publishes through. Required. */\n relayPool: OutboxRelayPool;\n /**\n * Resolve NIP-65 relay lists for a set of pubkeys. Pubkeys with no known\n * list are simply omitted from the returned map (they become `missingAuthors`).\n */\n loadRelayLists(pubkeys: string[]): Promise<Map<string, RelayListEntry>> | Map<string, RelayListEntry>;\n /** Relays to fall back to when NIP-65 data is absent, stale, or empty. Required. */\n fallbackRelays: string[];\n /**\n * Sign a template before publish (shell-mediated; napplets never sign). When\n * omitted, `publish` resolves with `{ ok: false, error: 'publish denied' }`.\n */\n signEvent?(template: EventTemplate): Promise<NostrEvent>;\n /**\n * Validate an event signature before delivering it to a napplet. May be sync\n * or async. Defaults to accepting every event (host pools often pre-verify).\n */\n verifyEvent?(event: NostrEvent): Promise<boolean> | boolean;\n /**\n * Gate relay URLs (e.g. block private-network hosts). Defaults to allowing\n * only `ws://` / `wss://` URLs — `options.relays` hints pass through this too.\n */\n isRelayAllowed?(url: string): boolean;\n /** Default query timeout when `options.timeoutMs` is unset. Default 4000ms. */\n defaultTimeoutMs?: number;\n}\n\n/** Resolved router dependencies threaded into the module-level helpers. */\ninterface RouterCtx {\n relayPool: OutboxRelayPool;\n loadRelayLists: RelayPoolOutboxRouterOptions['loadRelayLists'];\n fallbackRelays: string[];\n signEvent?: RelayPoolOutboxRouterOptions['signEvent'];\n isRelayAllowed: (url: string) => boolean;\n defaultTimeoutMs: number;\n verify(event: NostrEvent): Promise<boolean>;\n}\n\n/** Default relay gate: only ws(s):// URLs are permitted. */\nfunction defaultRelayAllowed(url: string): boolean {\n return typeof url === 'string' && (url.startsWith('wss://') || url.startsWith('ws://'));\n}\n\n/** Collect a deduplicated author set from filters + explicit option hints. */\nfunction deriveAuthors(filters: NostrFilter[], optionAuthors?: string[]): string[] {\n const authors = new Set<string>();\n for (const filter of filters) {\n for (const author of filter.authors ?? []) authors.add(author);\n }\n for (const author of optionAuthors ?? []) authors.add(author);\n return [...authors];\n}\n\n/**\n * Whether a resolved plan should use authors' write relays (true) or read\n * relays (false), given the read/write direction and an explicit strategy.\n */\nfunction wantsWriteRelays(direction: 'read' | 'write', strategy: OutboxStrategy): boolean {\n if (strategy === 'outbox') return true;\n if (strategy === 'inbox') return false;\n return direction === 'read'; // auto: reading → author write relays\n}\n\n/** Apply the relay gate to a candidate set, deduplicating. */\nfunction allowed(ctx: RouterCtx, urls: Iterable<string>): string[] {\n const out = new Set<string>();\n for (const url of urls) {\n if (ctx.isRelayAllowed(url)) out.add(url);\n }\n return [...out];\n}\n\n/**\n * Resolve the relay plan for a set of pubkeys. Returns the allowed relay set,\n * its provenance, and any pubkeys whose relay list was unavailable.\n */\nasync function resolvePlan(\n ctx: RouterCtx,\n pubkeys: string[],\n direction: 'read' | 'write',\n strategy: OutboxStrategy,\n relayHints?: string[],\n): Promise<OutboxRelayPlan> {\n const useWrite = wantsWriteRelays(direction, strategy);\n const collected = new Set<string>();\n const missingAuthors: string[] = [];\n let sawNip65 = false;\n\n if (pubkeys.length > 0) {\n const lists = await ctx.loadRelayLists(pubkeys);\n for (const pubkey of pubkeys) {\n const entry = lists.get(pubkey);\n const relays = entry ? (useWrite ? entry.write : entry.read) : undefined;\n if (relays && relays.length > 0) {\n sawNip65 = true;\n for (const url of relays) collected.add(url);\n } else {\n missingAuthors.push(pubkey);\n }\n }\n }\n\n // Relay hints from the napplet augment the plan, subject to the relay gate.\n for (const url of relayHints ?? []) collected.add(url);\n\n let relays = allowed(ctx, collected);\n let source: OutboxRelayPlan['source'];\n if (relays.length === 0) {\n relays = allowed(ctx, ctx.fallbackRelays);\n source = 'fallback';\n } else {\n // nip65 if any author list contributed; otherwise only hints did (policy).\n source = sawNip65 ? 'nip65' : 'policy';\n }\n\n const plan: OutboxRelayPlan = { relays, source };\n if (missingAuthors.length > 0) plan.missingAuthors = missingAuthors;\n return plan;\n}\n\n/** Mutable accumulator for a one-shot fan-out collection. */\ninterface Collector {\n seen: Map<string, NostrEvent>;\n relayMap: Map<string, Set<string>>;\n verifications: Promise<void>[];\n}\n\n/** Record that `id` was observed on `relayUrl`. */\nfunction recordRelay(collector: Collector, id: string, relayUrl: string): void {\n let set = collector.relayMap.get(id);\n if (!set) { set = new Set<string>(); collector.relayMap.set(id, set); }\n set.add(relayUrl);\n}\n\n/** Verify a freshly-seen event and admit it (or drop its sightings) once settled. */\nfunction admitEvent(ctx: RouterCtx, collector: Collector, event: NostrEvent): void {\n if (collector.seen.has(event.id)) return;\n collector.verifications.push(\n ctx.verify(event).then((ok) => {\n if (ok && !collector.seen.has(event.id)) collector.seen.set(event.id, event);\n else if (!ok) collector.relayMap.delete(event.id);\n }),\n );\n}\n\n/** Build the final query outcome from a settled collector. */\nfunction buildCollectResult(\n collector: Collector,\n timedOut: boolean,\n): { events: NostrEvent[]; relayMap: Record<string, string[]>; incomplete: boolean } {\n const events = [...collector.seen.values()];\n const relayObj: Record<string, string[]> = {};\n for (const event of events) relayObj[event.id] = [...(collector.relayMap.get(event.id) ?? [])];\n return { events, relayMap: relayObj, incomplete: timedOut };\n}\n\n/**\n * Fan a one-shot query out across `relayUrls` (one subscription per relay so\n * events can be attributed to their source relay), dedup by id, validate\n * signatures, and finalize on all-EOSE or timeout.\n */\nfunction collectFromRelays(\n ctx: RouterCtx,\n filters: NostrFilter[],\n relayUrls: string[],\n timeoutMs: number,\n): Promise<{ events: NostrEvent[]; relayMap: Record<string, string[]>; incomplete: boolean }> {\n return new Promise((resolve) => {\n const collector: Collector = { seen: new Map(), relayMap: new Map(), verifications: [] };\n const handles: { unsubscribe(): void }[] = [];\n let eoseCount = 0;\n let finished = false;\n let timedOut = false;\n\n function finalize(): void {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n for (const handle of handles) {\n try { handle.unsubscribe(); } catch { /* best-effort */ }\n }\n void Promise.all(collector.verifications).then(() => resolve(buildCollectResult(collector, timedOut)));\n }\n\n const timer = setTimeout(() => { timedOut = true; finalize(); }, timeoutMs);\n\n for (const relayUrl of relayUrls) {\n handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {\n if (finished) return;\n if (item === 'EOSE') {\n eoseCount += 1;\n if (eoseCount >= relayUrls.length) finalize();\n return;\n }\n recordRelay(collector, item.id, relayUrl);\n admitEvent(ctx, collector, item);\n }));\n }\n\n if (relayUrls.length === 0) finalize();\n });\n}\n\n/** Thin wrapper so callers read as a single-relay subscribe. */\nfunction relayPoolSubscribe(\n ctx: RouterCtx,\n filters: NostrFilter[],\n relayUrl: string,\n cb: (item: NostrEvent | 'EOSE') => void,\n): { unsubscribe(): void } {\n return ctx.relayPool.subscribe(filters, [relayUrl], cb);\n}\n\nasync function queryImpl(ctx: RouterCtx, filters: NostrFilter[], options?: OutboxQueryOptions): Promise<OutboxResult> {\n if (!ctx.relayPool.isAvailable()) {\n return { events: [], relays: {}, incomplete: true, error: 'relay list unavailable' };\n }\n const strategy = options?.strategy ?? 'auto';\n const authors = deriveAuthors(filters, options?.authors);\n const plan = await resolvePlan(ctx, authors, 'read', strategy, options?.relays);\n if (plan.relays.length === 0) {\n return { events: [], relays: {}, incomplete: true, error: 'relay list unavailable' };\n }\n\n const timeoutMs = options?.timeoutMs ?? ctx.defaultTimeoutMs;\n const collected = await collectFromRelays(ctx, filters, plan.relays, timeoutMs);\n\n const incomplete = collected.incomplete || (plan.missingAuthors?.length ?? 0) > 0;\n let events = collected.events;\n if (options?.limit !== undefined && events.length > options.limit) {\n events = [...events].sort((a, b) => b.created_at - a.created_at).slice(0, options.limit);\n }\n const result: OutboxResult = { events, relays: collected.relayMap };\n if (incomplete) result.incomplete = true;\n return result;\n}\n\n/** Tracks a live/one-shot subscription across its per-relay fan-out. */\ninterface LiveSub {\n handles: { unsubscribe(): void }[];\n seen: Set<string>;\n closed: boolean;\n eoseCount: number;\n relayCount: number;\n eoseSent: boolean;\n}\n\nfunction closeLiveSub(sub: LiveSub): void {\n for (const handle of sub.handles) {\n try { handle.unsubscribe(); } catch { /* best-effort */ }\n }\n sub.handles.length = 0;\n}\n\n/** Wire one relay's subscription into a live subscription's event/eose flow. */\nfunction attachLiveRelay(\n ctx: RouterCtx,\n sub: LiveSub,\n filters: NostrFilter[],\n relayUrl: string,\n live: boolean,\n sink: OutboxSubscriptionSink,\n): void {\n sub.handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {\n if (sub.closed) return;\n if (item === 'EOSE') {\n sub.eoseCount += 1;\n if (!sub.eoseSent && sub.eoseCount >= sub.relayCount) {\n sub.eoseSent = true;\n sink.eose();\n if (!live) { sub.closed = true; closeLiveSub(sub); sink.closed(); }\n }\n return;\n }\n if (sub.seen.has(item.id)) return;\n void ctx.verify(item).then((ok) => {\n if (!ok || sub.closed || sub.seen.has(item.id)) return;\n sub.seen.add(item.id);\n sink.event(item, relayUrl);\n });\n }));\n}\n\nfunction startSubscription(\n ctx: RouterCtx,\n filters: NostrFilter[],\n options: OutboxSubscribeOptions | undefined,\n sink: OutboxSubscriptionSink,\n): OutboxRouterSubscription {\n const live = options?.live ?? true;\n const strategy = options?.strategy ?? 'auto';\n const authors = deriveAuthors(filters, options?.authors);\n const sub: LiveSub = { handles: [], seen: new Set(), closed: false, eoseCount: 0, relayCount: 0, eoseSent: false };\n\n void resolvePlan(ctx, authors, 'read', strategy, options?.relays)\n .then((plan) => {\n if (sub.closed) return;\n if (plan.relays.length === 0) { sink.closed('relay list unavailable'); return; }\n sub.relayCount = plan.relays.length;\n for (const relayUrl of plan.relays) attachLiveRelay(ctx, sub, filters, relayUrl, live, sink);\n })\n .catch((err) => {\n if (!sub.closed) sink.closed(err instanceof Error ? err.message : 'subscribe failed');\n });\n\n return {\n close(): void {\n if (sub.closed) return;\n sub.closed = true;\n closeLiveSub(sub);\n },\n };\n}\n\n/** Resolve the full write/inbox/hint relay set a publish should fan out to. */\nasync function resolvePublishTargets(\n ctx: RouterCtx,\n signed: NostrEvent,\n options?: OutboxPublishOptions,\n): Promise<string[]> {\n const strategy = options?.strategy ?? 'auto';\n const targets = new Set<string>();\n\n // The author's own write relays (outbox model for the user's own event).\n const authorPlan = await resolvePlan(ctx, [signed.pubkey], 'read', strategy === 'inbox' ? 'auto' : 'outbox');\n for (const url of authorPlan.relays) targets.add(url);\n\n // Directed events: include recipients' read relays (their inbox).\n if (options?.targetAuthors && options.targetAuthors.length > 0) {\n const inboxPlan = await resolvePlan(ctx, options.targetAuthors, 'write', 'inbox');\n for (const url of inboxPlan.relays) targets.add(url);\n }\n\n for (const url of options?.relays ?? []) targets.add(url);\n return allowed(ctx, targets);\n}\n\nasync function publishImpl(ctx: RouterCtx, template: EventTemplate, options?: OutboxPublishOptions): Promise<OutboxPublishResult> {\n if (!ctx.signEvent) return { ok: false, error: 'publish denied' };\n if (!ctx.relayPool.isAvailable()) return { ok: false, error: 'relay list unavailable' };\n\n let signed: NostrEvent;\n try {\n signed = await ctx.signEvent(template);\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : 'sign failed' };\n }\n\n const relayUrls = await resolvePublishTargets(ctx, signed, options);\n if (relayUrls.length === 0) return { ok: false, event: signed, eventId: signed.id, error: 'relay list unavailable' };\n\n let relays: Record<string, boolean>;\n try {\n relays = normalizePublishResult(await ctx.relayPool.publish(signed, relayUrls), relayUrls);\n } catch (err) {\n return { ok: false, event: signed, eventId: signed.id, error: err instanceof Error ? err.message : 'publish failed' };\n }\n\n const ok = Object.values(relays).some(Boolean);\n const result: OutboxPublishResult = { ok, event: signed, eventId: signed.id, relays };\n if (!ok) result.error = 'publish denied';\n return result;\n}\n\n/**\n * Create a relay-pool-backed {@link OutboxRouter}.\n *\n * @param options - Relay pool, NIP-65 loader, fallback relays, and optional\n * signer / verifier / relay gate / timeout.\n * @returns An {@link OutboxRouter} for {@link createOutboxService}.\n * @throws If `relayPool`, `loadRelayLists`, or `fallbackRelays` are missing.\n */\nexport function createRelayPoolOutboxRouter(options: RelayPoolOutboxRouterOptions): OutboxRouter {\n if (!options || typeof options.relayPool !== 'object' || options.relayPool === null) {\n throw new Error('createRelayPoolOutboxRouter: options.relayPool is required');\n }\n if (typeof options.loadRelayLists !== 'function') {\n throw new Error('createRelayPoolOutboxRouter: options.loadRelayLists is required');\n }\n if (!Array.isArray(options.fallbackRelays)) {\n throw new Error('createRelayPoolOutboxRouter: options.fallbackRelays is required');\n }\n\n const verifyEvent = options.verifyEvent;\n const ctx: RouterCtx = {\n relayPool: options.relayPool,\n loadRelayLists: options.loadRelayLists,\n fallbackRelays: options.fallbackRelays,\n signEvent: options.signEvent,\n isRelayAllowed: options.isRelayAllowed ?? defaultRelayAllowed,\n defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,\n async verify(event: NostrEvent): Promise<boolean> {\n if (!verifyEvent) return true;\n try {\n return await verifyEvent(event);\n } catch {\n return false;\n }\n },\n };\n\n return {\n query: (filters, queryOptions) => queryImpl(ctx, filters, queryOptions),\n subscribe: (filters, subscribeOptions, sink) => startSubscription(ctx, filters, subscribeOptions, sink),\n publish: (template, publishOptions) => publishImpl(ctx, template, publishOptions),\n resolveRelays: (target: OutboxTarget) => {\n const pubkeys = target.authors ?? (target.pubkey ? [target.pubkey] : []);\n return resolvePlan(ctx, pubkeys, target.direction ?? 'read', target.strategy ?? 'auto');\n },\n };\n}\n\n/** Normalize a pool publish return into a per-relay success map. */\nfunction normalizePublishResult(\n res: Record<string, boolean> | void,\n relayUrls: string[],\n): Record<string, boolean> {\n const out: Record<string, boolean> = {};\n if (res && typeof res === 'object') {\n for (const url of relayUrls) out[url] = res[url] ?? false;\n } else {\n // void return → optimistic success on every targeted relay.\n for (const url of relayUrls) out[url] = true;\n }\n return out;\n}\n","/**\n * upload-service.ts — NAP-UPLOAD (shell-mediated file/blob upload) reference service.\n *\n * Shell-side handler for the NAP-UPLOAD wire protocol. It is a pure envelope\n * router: it validates `upload.*` envelopes, delegates the actual byte transfer\n * (server selection, rail authorization signing, the HTTP upload) to an injected\n * {@link Uploader}, and posts the correlated result / status messages back to the\n * napplet.\n *\n * The uploader is injected (options-as-bridge) so this service has no transport\n * or Nostr dependency and is fully unit-testable. NAP-UPLOAD is deliberately\n * abstract over the backend — the runtime decides *how* it uploads (NIP-96,\n * Blossom, …). A concrete HTTP-backed uploader ships alongside as\n * {@link createHttpUploader}.\n *\n * ──────────────────────────── Responsibilities ────────────────────────────\n * Inbound: upload.upload, upload.status\n * Outbound: upload.upload.result, upload.status.result, upload.status.changed\n *\n * The service owns the `uploadId` (generated per request, scoped to the\n * requesting napplet), tracks the latest {@link UploadStatus} per upload for\n * `upload.status` queries, and cleans up on window teardown. The shell owns\n * consent, policy, server selection, signing, and the HTTP upload — all behind\n * the {@link Uploader}.\n *\n * @example\n * ```ts\n * import { createUploadService, createHttpUploader } from '@kehto/services';\n *\n * const uploader = createHttpUploader({ rails: { nip96: { servers } }, signEvent });\n * runtime.registerService('upload', createUploadService({ uploader }));\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\n\n/** Upload service version — follows semver. */\nconst UPLOAD_SERVICE_VERSION = '1.0.0';\n\n/**\n * Storage rail. `nip96` (NIP-96 HTTP file storage) and `blossom` (Blossom blob\n * storage) are the first concrete backends; the open string keeps the API\n * stable as shells add rails (torrents, usenet, …).\n */\nexport type UploadRail = 'nip96' | 'blossom' | (string & {});\n\n/** Lifecycle state of an upload. */\nexport type UploadState = 'pending' | 'uploading' | 'complete' | 'failed' | 'cancelled';\n\n/** Pixel dimensions of an uploaded image/video. */\nexport interface UploadDimensions {\n width: number;\n height: number;\n}\n\n/**\n * A napplet's upload request. `data` crosses the postMessage boundary by\n * structured clone — shells never require base64 encoding.\n */\nexport interface UploadRequest {\n /** Storage rail; omit to let the shell pick a configured default. */\n rail?: UploadRail;\n /** The bytes to upload. */\n data: ArrayBuffer | Blob;\n /** MIME type; inferred from `data` when omitted. */\n mimeType?: string;\n /** Suggested filename. */\n filename?: string;\n /** Alt text / description for the file event. */\n caption?: string;\n /** Request the server not re-encode the file (NIP-96 `no_transform`). */\n noTransform?: boolean;\n /** Rail-specific or shell-specific extra metadata. */\n metadata?: Record<string, unknown>;\n}\n\n/** A single Nostr tag (NIP-94 / imeta entries are arrays of strings). */\nexport type NostrTag = string[];\n\n/** The result of an upload. */\nexport interface UploadResult {\n /** Whether the upload succeeded (or is progressing) vs failed/cancelled. */\n ok: boolean;\n /** Shell-generated id, scoped to the requesting napplet. */\n uploadId: string;\n /** Current lifecycle state. */\n status: UploadState;\n /** The rail the shell used. */\n rail: UploadRail;\n /** Primary download URL. */\n url?: string;\n /** Mirrors / alternative server URLs. */\n fallbackUrls?: string[];\n /** Hash of the stored blob (NIP-94 `x`). */\n sha256?: string;\n /** Hash before server transforms (NIP-94 `ox`). */\n originalSha256?: string;\n /** Size in bytes. */\n size?: number;\n /** Stored MIME type. */\n mimeType?: string;\n /** Image/video dimensions when known. */\n dimensions?: UploadDimensions;\n /** Blurhash placeholder when known. */\n blurhash?: string;\n /** Ready-to-attach NIP-94 / imeta tags. */\n nip94?: NostrTag[];\n /** Error reason when the upload failed or was cancelled. */\n error?: string;\n}\n\n/** A status snapshot for an upload, including progress counters. */\nexport interface UploadStatus extends UploadResult {\n /** Bytes sent so far (while uploading). */\n bytesSent?: number;\n /** Total bytes to send. */\n bytesTotal?: number;\n /** Unix ms timestamp of this status. */\n updatedAt: number;\n}\n\n/**\n * Context handed to an {@link Uploader} for a single upload. Carries the\n * service-owned `uploadId` and a sink for streaming progress / state changes.\n */\nexport interface UploaderContext {\n /** The service-generated upload id (authoritative; scoped to the napplet). */\n uploadId: string;\n /** The napplet window that requested the upload. */\n windowId: string;\n /**\n * Push a status update (progress, or a transition to complete/failed). The\n * service stamps `uploadId` and `updatedAt` before forwarding to the napplet\n * as `upload.status.changed`, and records it as the latest tracked status.\n */\n onStatus(status: UploadStatus): void;\n}\n\n/**\n * Abstract upload backend. Implementors own server selection, rail\n * authorization signing (NIP-98 for NIP-96, kind 24242 for Blossom), the HTTP\n * upload, and integrity-hash reporting. The service translates wire envelopes\n * into these calls and back. A concrete reference implementation ships as\n * {@link createHttpUploader}.\n */\nexport interface Uploader {\n /** Upload `request.data`, streaming progress through `ctx.onStatus`. */\n upload(request: UploadRequest, ctx: UploaderContext): Promise<UploadResult>;\n /** Optional: resolve the latest status for an upload the service is not tracking. */\n status?(uploadId: string): Promise<UploadStatus | undefined>;\n /** Optional: abort an in-flight upload (called on window teardown). */\n cancel?(uploadId: string): void;\n}\n\n/** Options for {@link createUploadService}. */\nexport interface UploadServiceOptions {\n /** The upload backend the shell uses. Required. */\n uploader: Uploader;\n /** Generate an upload id; defaults to `crypto.randomUUID()`. */\n generateId?: () => string;\n /** Current time in unix ms; defaults to `Date.now()`. */\n now?: () => number;\n}\n\ntype Send = (msg: NappletMessage) => void;\n\nconst UPLOAD_DESCRIPTOR: ServiceDescriptor = {\n name: 'upload',\n version: UPLOAD_SERVICE_VERSION,\n description: 'NAP-UPLOAD shell-mediated file/blob upload — upload/status with progress pushes',\n};\n\n/** Per-upload tracking entry, keyed by `windowId:uploadId`. */\ninterface UploadEntry {\n uploadId: string;\n status?: UploadStatus;\n}\n\ndeclare const crypto: { randomUUID(): string };\n\n/**\n * Create the NAP-UPLOAD service handler.\n *\n * @param options - Must provide an {@link Uploader}.\n * @returns A `ServiceHandler` ready for `runtime.registerService('upload', handler)`.\n * @throws If `options.uploader` is missing.\n */\nexport function createUploadService(options: UploadServiceOptions): ServiceHandler {\n if (!options || typeof options.uploader !== 'object' || options.uploader === null) {\n throw new Error('createUploadService: options.uploader is required');\n }\n const { uploader } = options;\n const generateId = options.generateId ?? (() => crypto.randomUUID());\n const now = options.now ?? (() => Date.now());\n\n // Tracked uploads keyed by `windowId:uploadId` for status lookup + cleanup.\n const entries = new Map<string, UploadEntry>();\n\n function handleUpload(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; request?: UploadRequest };\n const id = m.id ?? '';\n const request = m.request;\n if (!request || typeof request !== 'object' || request.data == null) {\n send({ type: 'upload.upload.result', id, error: 'invalid request' } as NappletMessage);\n return;\n }\n\n const uploadId = generateId();\n const key = `${windowId}:${uploadId}`;\n entries.set(key, { uploadId });\n\n const ctx: UploaderContext = {\n uploadId,\n windowId,\n onStatus: (status) => {\n const stamped: UploadStatus = { ...status, uploadId, updatedAt: status.updatedAt || now() };\n const entry = entries.get(key);\n if (entry) entry.status = stamped;\n send({ type: 'upload.status.changed', status: stamped } as NappletMessage);\n },\n };\n\n void uploader\n .upload(request, ctx)\n .then((result) => {\n const stamped: UploadResult = { ...result, uploadId };\n const entry = entries.get(key);\n if (entry) entry.status = { ...stamped, updatedAt: now() };\n send({ type: 'upload.upload.result', id, result: stamped } as NappletMessage);\n })\n .catch((err) => {\n entries.delete(key);\n send({ type: 'upload.upload.result', id, error: toErrorMessage(err) } as NappletMessage);\n });\n }\n\n function handleStatus(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; uploadId?: string };\n const id = m.id ?? '';\n const uploadId = m.uploadId;\n if (typeof uploadId !== 'string' || uploadId.length === 0) {\n send({ type: 'upload.status.result', id, error: 'invalid uploadId' } as NappletMessage);\n return;\n }\n\n const tracked = entries.get(`${windowId}:${uploadId}`)?.status;\n if (tracked) {\n send({ type: 'upload.status.result', id, status: tracked } as NappletMessage);\n return;\n }\n\n if (uploader.status) {\n void uploader\n .status(uploadId)\n .then((status) =>\n send(\n status\n ? ({ type: 'upload.status.result', id, status } as NappletMessage)\n : ({ type: 'upload.status.result', id, error: 'unknown upload' } as NappletMessage),\n ),\n )\n .catch((err) =>\n send({ type: 'upload.status.result', id, error: toErrorMessage(err) } as NappletMessage),\n );\n return;\n }\n\n send({ type: 'upload.status.result', id, error: 'unknown upload' } as NappletMessage);\n }\n\n return {\n descriptor: UPLOAD_DESCRIPTOR,\n handleMessage(windowId: string, message: NappletMessage, send: Send): void {\n switch (message.type) {\n case 'upload.upload':\n handleUpload(windowId, message, send);\n return;\n case 'upload.status':\n handleStatus(windowId, message, send);\n return;\n default:\n // Unknown upload.* action — silently ignored (forward-compatible).\n return;\n }\n },\n onWindowDestroyed(windowId: string): void {\n const prefix = `${windowId}:`;\n for (const [key, entry] of entries) {\n if (key.startsWith(prefix)) {\n uploader.cancel?.(entry.uploadId);\n entries.delete(key);\n }\n }\n },\n };\n}\n\nfunction toErrorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return 'upload request failed';\n}\n","/**\n * http-uploader.ts — NAP-UPLOAD concrete HTTP-backed {@link Uploader}.\n *\n * The reference upload backend for {@link createUploadService}. Implements two\n * storage rails over HTTP:\n *\n * - **NIP-96** — signs a NIP-98 (kind 27235) HTTP-auth event, POSTs the file as\n * `multipart/form-data`, and maps the returned NIP-94 event tags into an\n * {@link UploadResult}.\n * - **Blossom** — signs a kind 24242 authorization event, PUTs the raw bytes to\n * `<server>/upload`, and maps the returned blob descriptor.\n *\n * Signing (`signEvent`) and transport (`fetch`) are injected so the uploader\n * carries no Nostr or network dependency and is fully unit-testable. The shell\n * holds the signing key and never exposes it to napplets — the uploader only\n * receives a signing callback. Server URLs are shell configuration, not napplet\n * input: a napplet may *hint* a rail, but never a server.\n *\n * The configured server URL is used directly as the upload endpoint (the\n * NIP-96 `api_url` / Blossom base). Hosts that need `.well-known` discovery can\n * resolve it before constructing the uploader.\n *\n * @example\n * ```ts\n * const uploader = createHttpUploader({\n * rails: { nip96: { servers: ['https://nostr.build/api/v2/nip96/upload'] } },\n * signEvent: (tmpl) => signer.signEvent(tmpl),\n * });\n * runtime.registerService('upload', createUploadService({ uploader }));\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { EventTemplate, NostrEvent } from '@napplet/core';\nimport type {\n NostrTag,\n UploadDimensions,\n UploadRail,\n UploadRequest,\n UploadResult,\n Uploader,\n UploaderContext,\n} from './upload-service.js';\n\n/** NIP-98 HTTP-auth event kind. */\nconst KIND_NIP98 = 27235;\n/** Blossom authorization event kind. */\nconst KIND_BLOSSOM_AUTH = 24242;\n/** Blossom auth-event lifetime, in seconds. */\nconst BLOSSOM_AUTH_TTL_S = 3600;\n\n/** Per-rail server configuration. The first server is the primary endpoint. */\nexport interface RailServerConfig {\n /** Ordered server endpoint URLs; index 0 is primary. */\n servers: string[];\n}\n\n/** Storage rails this uploader can serve. */\nexport interface HttpUploaderRails {\n /** NIP-96 HTTP file storage. */\n nip96?: RailServerConfig;\n /** Blossom blob storage. */\n blossom?: RailServerConfig;\n}\n\n/** Signs an event template on the user's behalf (shell holds the key). */\nexport type SignEvent = (template: EventTemplate) => Promise<NostrEvent>;\n\n/** Options for {@link createHttpUploader}. */\nexport interface HttpUploaderOptions {\n /** Configured rails + their servers. */\n rails: HttpUploaderRails;\n /** Rail to use when a request omits one; defaults to the first configured rail. */\n defaultRail?: UploadRail;\n /** Signs NIP-98 / Blossom auth events. Required. */\n signEvent: SignEvent;\n /** Fetch implementation; defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** Hex SHA-256 of the payload bytes; defaults to Web Crypto. */\n digestSha256?: (bytes: Uint8Array) => Promise<string>;\n /** Unix *seconds* clock for event timestamps; defaults to `Date.now()/1000`. */\n now?: () => number;\n}\n\ndeclare const btoa: (data: string) => string;\ndeclare const crypto: { subtle: { digest(alg: string, data: ArrayBuffer): Promise<ArrayBuffer> } };\n\n/**\n * Create the reference HTTP {@link Uploader} (NIP-96 + Blossom rails).\n *\n * @param options - Rails, server config, and the injected `signEvent`.\n * @returns An {@link Uploader} for `createUploadService({ uploader })`.\n * @throws If `options.signEvent` is missing.\n */\nexport function createHttpUploader(options: HttpUploaderOptions): Uploader {\n if (!options || typeof options.signEvent !== 'function') {\n throw new Error('createHttpUploader: options.signEvent is required');\n }\n const rails = options.rails ?? {};\n const signEvent = options.signEvent;\n const fetchFn = options.fetch ?? fetch;\n const digest = options.digestSha256 ?? defaultDigestSha256;\n const nowS = options.now ?? (() => Math.floor(Date.now() / 1000));\n const defaultRail = options.defaultRail ?? firstConfiguredRail(rails);\n\n async function upload(request: UploadRequest, ctx: UploaderContext): Promise<UploadResult> {\n const rail = request.rail ?? defaultRail;\n const config = rail === 'nip96' ? rails.nip96 : rail === 'blossom' ? rails.blossom : undefined;\n if (rail !== 'nip96' && rail !== 'blossom') {\n return failed(ctx.uploadId, rail ?? 'unknown', 'unsupported rail');\n }\n\n const server = config?.servers?.[0];\n if (!server) {\n return failed(ctx.uploadId, rail, 'no server configured');\n }\n\n const bytes = await toBytes(request.data);\n const sha256 = await digest(bytes);\n\n try {\n return rail === 'nip96'\n ? await uploadNip96({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS })\n : await uploadBlossom({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS });\n } catch (err) {\n return failed(ctx.uploadId, rail, toErrorMessage(err), sha256);\n }\n }\n\n return { upload };\n}\n\ninterface RailUploadArgs {\n request: UploadRequest;\n ctx: UploaderContext;\n server: string;\n bytes: Uint8Array;\n sha256: string;\n signEvent: SignEvent;\n fetchFn: typeof fetch;\n nowS: () => number;\n}\n\nasync function uploadNip96(args: RailUploadArgs): Promise<UploadResult> {\n const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;\n\n const auth = await signEvent({\n kind: KIND_NIP98,\n created_at: nowS(),\n content: '',\n tags: [\n ['u', server],\n ['method', 'POST'],\n ['payload', sha256],\n ],\n });\n\n const form = new FormData();\n form.append('file', new Blob([bytesToArrayBuffer(bytes)], { type: request.mimeType }), request.filename ?? 'file');\n if (request.caption !== undefined) form.append('caption', request.caption);\n if (request.mimeType !== undefined) form.append('content_type', request.mimeType);\n if (request.noTransform) form.append('no_transform', 'true');\n\n const res = await fetchFn(server, {\n method: 'POST',\n headers: { Authorization: nostrAuthHeader(auth) },\n body: form,\n });\n\n if (!res.ok) {\n return failed(ctx.uploadId, 'nip96', `server rejected (HTTP ${res.status})`, sha256);\n }\n\n const body = (await res.json()) as Nip96Response;\n if (body.status === 'error') {\n return failed(ctx.uploadId, 'nip96', body.message ?? 'upload failed', sha256);\n }\n\n const tags = body.nip94_event?.tags ?? [];\n return fromNip94Tags(ctx.uploadId, 'nip96', tags, bytes.byteLength, sha256);\n}\n\ninterface Nip96Response {\n status?: 'success' | 'error' | string;\n message?: string;\n nip94_event?: { tags?: string[][] };\n}\n\n/** Map NIP-94 / imeta tags into an {@link UploadResult}. */\nfunction fromNip94Tags(\n uploadId: string,\n rail: UploadRail,\n tags: string[][],\n fallbackSize: number,\n fallbackSha: string,\n): UploadResult {\n const get = (name: string): string | undefined => tags.find((t) => t[0] === name)?.[1];\n const url = get('url');\n const result: UploadResult = {\n ok: Boolean(url),\n uploadId,\n status: url ? 'complete' : 'failed',\n rail,\n sha256: get('x') ?? fallbackSha,\n nip94: tags as NostrTag[],\n };\n if (url) result.url = url;\n const ox = get('ox');\n if (ox) result.originalSha256 = ox;\n const size = get('size');\n result.size = size ? Number(size) : fallbackSize;\n const m = get('m');\n if (m) result.mimeType = m;\n const dim = parseDimensions(get('dim'));\n if (dim) result.dimensions = dim;\n const blurhash = get('blurhash');\n if (blurhash) result.blurhash = blurhash;\n if (!url) result.error = 'server returned no url';\n return result;\n}\n\nasync function uploadBlossom(args: RailUploadArgs): Promise<UploadResult> {\n const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;\n\n const auth = await signEvent({\n kind: KIND_BLOSSOM_AUTH,\n created_at: nowS(),\n content: `Upload ${request.filename ?? 'file'}`,\n tags: [\n ['t', 'upload'],\n ['x', sha256],\n ['expiration', String(nowS() + BLOSSOM_AUTH_TTL_S)],\n ],\n });\n\n const endpoint = `${trimTrailingSlash(server)}/upload`;\n const headers: Record<string, string> = { Authorization: nostrAuthHeader(auth) };\n if (request.mimeType) headers['Content-Type'] = request.mimeType;\n\n const res = await fetchFn(endpoint, {\n method: 'PUT',\n headers,\n body: bytesToArrayBuffer(bytes),\n });\n\n if (!res.ok) {\n return failed(ctx.uploadId, 'blossom', `server rejected (HTTP ${res.status})`, sha256);\n }\n\n const blob = (await res.json()) as BlossomDescriptor;\n if (!blob.url) {\n return failed(ctx.uploadId, 'blossom', 'server returned no url', sha256);\n }\n\n const result: UploadResult = {\n ok: true,\n uploadId: ctx.uploadId,\n status: 'complete',\n rail: 'blossom',\n url: blob.url,\n sha256: blob.sha256 ?? sha256,\n size: blob.size ?? bytes.byteLength,\n };\n if (blob.type) result.mimeType = blob.type;\n return result;\n}\n\ninterface BlossomDescriptor {\n url?: string;\n sha256?: string;\n size?: number;\n type?: string;\n}\n\nfunction failed(uploadId: string, rail: UploadRail, error: string, sha256?: string): UploadResult {\n return { ok: false, uploadId, status: 'failed', rail, error, ...(sha256 ? { sha256 } : {}) };\n}\n\nfunction firstConfiguredRail(rails: HttpUploaderRails): UploadRail | undefined {\n if (rails.nip96?.servers?.length) return 'nip96';\n if (rails.blossom?.servers?.length) return 'blossom';\n return undefined;\n}\n\nfunction nostrAuthHeader(event: NostrEvent): string {\n return `Nostr ${base64Utf8(JSON.stringify(event))}`;\n}\n\nfunction base64Utf8(s: string): string {\n // UTF-8 safe base64 (event content/tags may contain non-ASCII).\n return btoa(String.fromCharCode(...new TextEncoder().encode(s)));\n}\n\nfunction parseDimensions(dim: string | undefined): UploadDimensions | undefined {\n if (!dim) return undefined;\n const m = /^(\\d+)x(\\d+)$/.exec(dim);\n if (!m) return undefined;\n return { width: Number(m[1]), height: Number(m[2]) };\n}\n\nasync function toBytes(data: ArrayBuffer | Blob): Promise<Uint8Array> {\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n return new Uint8Array(await data.arrayBuffer());\n}\n\nfunction bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nfunction trimTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\nasync function defaultDigestSha256(bytes: Uint8Array): Promise<string> {\n const buf = await crypto.subtle.digest('SHA-256', bytesToArrayBuffer(bytes));\n return Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\nfunction toErrorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return 'upload failed';\n}\n","/**\n * cvm-service.ts — NAP-CVM (ContextVM bridge) reference service.\n *\n * Shell-side handler for the NAP-CVM wire protocol. It is a pure envelope\n * router: it validates `cvm.*` envelopes, delegates the actual ContextVM /\n * MCP-over-Nostr work to an injected {@link CvmTransport}, and posts the\n * correlated `*.result` (echoing the request `id`) back to the napplet.\n *\n * The transport is injected (options-as-bridge) so this service has no Nostr\n * dependency and is fully unit-testable. A concrete ContextVM transport ships\n * separately at `@kehto/services/cvm-nostr-transport`.\n *\n * ──────────────────────────── Responsibilities ────────────────────────────\n * Inbound: cvm.discover, cvm.request, cvm.close\n * Outbound: cvm.discover.result, cvm.request.result, cvm.close.result,\n * cvm.event (server-pushed MCP notifications)\n *\n * MCP-level errors are returned inside `request.result.message.error`;\n * transport/shell-policy failures are returned in the envelope `error` field.\n *\n * `cvm.event` is fanned out to every window that holds an active session with\n * the originating server (a window opens a session by issuing a `cvm.request`\n * and closes it via `cvm.close` or window teardown).\n *\n * @example\n * ```ts\n * import { createCvmService } from '@kehto/services';\n * import { createNostrCvmTransport } from '@kehto/services/cvm-nostr-transport';\n *\n * const transport = createNostrCvmTransport({ defaultRelays: ['wss://relay.contextvm.org'] });\n * runtime.registerService('cvm', createCvmService({ transport }));\n * ```\n */\n\nimport type { NappletMessage } from '@napplet/core';\nimport type { ServiceDescriptor, ServiceHandler } from '@kehto/runtime';\nimport type {\n CvmDiscoverQuery,\n CvmRequestOptions,\n CvmServer,\n CvmServerRef,\n McpMessage,\n} from './cvm-types.js';\n\n/** CVM service version — follows semver. */\nconst CVM_SERVICE_VERSION = '1.0.0';\n\n/**\n * Abstract ContextVM transport. Implementors own Nostr relay access, signing,\n * encryption (CEP-4 gift wrap), JSON-RPC correlation, and MCP initialization.\n */\nexport interface CvmTransport {\n /** Resolve public ContextVM server announcements matching the query. */\n discover(query?: CvmDiscoverQuery): Promise<CvmServer[]>;\n /** Send a raw MCP message to a server and resolve with the MCP response. */\n request(server: CvmServerRef, message: McpMessage, options?: CvmRequestOptions): Promise<McpMessage>;\n /** Release any session state held for a server (subscriptions, init cache). */\n close(server: CvmServerRef): Promise<void>;\n /**\n * Subscribe to server-pushed MCP messages not correlated to a single\n * request (e.g. notifications). Returns a handle whose `close()` detaches.\n */\n onEvent(handler: (server: CvmServerRef, message: McpMessage) => void): { close(): void };\n}\n\n/** Options for {@link createCvmService}. */\nexport interface CvmServiceOptions {\n /** The ContextVM transport the shell uses to reach servers. Required. */\n transport: CvmTransport;\n}\n\n/** The created CVM service, exposing the handler plus a disposal hook. */\nexport interface CvmService extends ServiceHandler {\n /** Detach the transport event subscription. Idempotent. */\n dispose(): void;\n}\n\ntype Send = (msg: NappletMessage) => void;\n\nconst CVM_DESCRIPTOR: ServiceDescriptor = {\n name: 'cvm',\n version: CVM_SERVICE_VERSION,\n description: 'NAP-CVM ContextVM bridge — MCP over Nostr',\n};\n\n/**\n * Create the NAP-CVM service handler.\n *\n * @param options - Must provide a {@link CvmTransport}.\n * @returns A {@link CvmService} (a `ServiceHandler` with a `dispose()` hook).\n * @throws If `options.transport` is missing.\n */\nexport function createCvmService(options: CvmServiceOptions): CvmService {\n if (!options || typeof options.transport !== 'object' || options.transport === null) {\n throw new Error('createCvmService: options.transport is required');\n }\n const { transport } = options;\n\n // Per-window send callbacks, captured at request time for cvm.event fan-out.\n const sendByWindow = new Map<string, Send>();\n // serverPubkey -> set of windowIds with an active session.\n const windowsByServer = new Map<string, Set<string>>();\n\n function openSession(windowId: string, server: CvmServerRef, send: Send): void {\n sendByWindow.set(windowId, send);\n let windows = windowsByServer.get(server.pubkey);\n if (!windows) {\n windows = new Set<string>();\n windowsByServer.set(server.pubkey, windows);\n }\n windows.add(windowId);\n }\n\n function closeSession(windowId: string, serverPubkey: string): void {\n const windows = windowsByServer.get(serverPubkey);\n if (windows) {\n windows.delete(windowId);\n if (windows.size === 0) windowsByServer.delete(serverPubkey);\n }\n }\n\n const eventSub = transport.onEvent((server, message) => {\n const windows = windowsByServer.get(server.pubkey);\n if (!windows) return;\n for (const windowId of windows) {\n const send = sendByWindow.get(windowId);\n send?.({ type: 'cvm.event', server, message } as NappletMessage);\n }\n });\n\n function handleDiscover(msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; query?: CvmDiscoverQuery };\n const id = m.id ?? '';\n void transport\n .discover(m.query)\n .then((servers) => send({ type: 'cvm.discover.result', id, servers } as NappletMessage))\n .catch((err) =>\n send({ type: 'cvm.discover.result', id, servers: [], error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n function handleRequest(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & {\n id?: string;\n server?: CvmServerRef;\n message?: McpMessage;\n options?: CvmRequestOptions;\n };\n const id = m.id ?? '';\n if (!m.server || typeof m.server.pubkey !== 'string' || m.server.pubkey.length === 0) {\n send({ type: 'cvm.request.result', id, error: 'server not found' } as NappletMessage);\n return;\n }\n if (!m.message || typeof m.message !== 'object') {\n send({ type: 'cvm.request.result', id, error: 'unsupported method' } as NappletMessage);\n return;\n }\n openSession(windowId, m.server, send);\n void transport\n .request(m.server, m.message, m.options)\n .then((message) => send({ type: 'cvm.request.result', id, message } as NappletMessage))\n .catch((err) =>\n send({ type: 'cvm.request.result', id, error: toErrorMessage(err) } as NappletMessage),\n );\n }\n\n function handleClose(windowId: string, msg: NappletMessage, send: Send): void {\n const m = msg as NappletMessage & { id?: string; server?: CvmServerRef };\n const id = m.id ?? '';\n if (!m.server || typeof m.server.pubkey !== 'string') {\n send({ type: 'cvm.close.result', id, error: 'server not found' } as NappletMessage);\n return;\n }\n closeSession(windowId, m.server.pubkey);\n void transport\n .close(m.server)\n .then(() => send({ type: 'cvm.close.result', id } as NappletMessage))\n .catch((err) => send({ type: 'cvm.close.result', id, error: toErrorMessage(err) } as NappletMessage));\n }\n\n return {\n descriptor: CVM_DESCRIPTOR,\n handleMessage(windowId: string, message: NappletMessage, send: Send): void {\n switch (message.type) {\n case 'cvm.discover':\n handleDiscover(message, send);\n return;\n case 'cvm.request':\n handleRequest(windowId, message, send);\n return;\n case 'cvm.close':\n handleClose(windowId, message, send);\n return;\n default:\n // Unknown cvm.* action — silently ignored (forward-compatible).\n return;\n }\n },\n onWindowDestroyed(windowId: string): void {\n sendByWindow.delete(windowId);\n for (const [pubkey, windows] of windowsByServer) {\n windows.delete(windowId);\n if (windows.size === 0) windowsByServer.delete(pubkey);\n }\n },\n dispose(): void {\n eventSub.close();\n },\n };\n}\n\nfunction toErrorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return 'cvm request failed';\n}\n"],"mappings":"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","names":["notify","result","deliver","relays","generateId","toErrorMessage","toErrorMessage","toErrorMessage"]}
|