@kehto/services 0.2.0 → 0.5.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/README.md +320 -18
- package/dist/cvm-nostr-transport.d.ts +97 -0
- package/dist/cvm-nostr-transport.js +286 -0
- package/dist/cvm-nostr-transport.js.map +1 -0
- package/dist/cvm-service-CXcOVQ0m.d.ts +207 -0
- package/dist/index.d.ts +801 -76
- package/dist/index.js +1329 -236
- package/dist/index.js.map +1 -1
- package/package.json +19 -22
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { ServiceHandler, Signer } from '@kehto/runtime';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { NostrEvent, NappletMessage, NostrFilter } from '@napplet/core';
|
|
3
|
+
import { MediaMetadata, MediaAction } from '@napplet/nub/media/types';
|
|
4
|
+
export { MediaAction } from '@napplet/nub/media/types';
|
|
5
|
+
import { NotifySendMessage } from '@napplet/nub/notify/types';
|
|
6
|
+
import { Theme, ThemeChangedMessage } from '@napplet/nub/theme/types';
|
|
7
|
+
import { ConfigSchemaErrorCode, ConfigValues, NappletConfigSchema } from '@napplet/nub/config/types';
|
|
8
|
+
export { a as CvmCloseMessage, b as CvmCloseResultMessage, c as CvmDiscoverMessage, d as CvmDiscoverQuery, e as CvmDiscoverResultMessage, f as CvmEventMessage, g as CvmInboundMessage, h as CvmOutboundMessage, i as CvmRequestMessage, j as CvmRequestOptions, k as CvmRequestResultMessage, l as CvmServer, m as CvmServerRef, n as CvmService, o as CvmServiceOptions, C as CvmTransport, p as CvmTransportError, M as McpContentBlock, q as McpMessage, r as McpTool, s as McpToolResult, t as createCvmService } from './cvm-service-CXcOVQ0m.js';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* @kehto/services — Shared types for reference service implementations.
|
|
@@ -138,15 +142,6 @@ interface NotificationServiceOptions {
|
|
|
138
142
|
*/
|
|
139
143
|
declare function createAudioService(options?: AudioServiceOptions): ServiceHandler;
|
|
140
144
|
|
|
141
|
-
/**
|
|
142
|
-
* notification-service.ts — Notification state registry as a ServiceHandler.
|
|
143
|
-
*
|
|
144
|
-
* Tracks notifications created by napplet windows. Shell hosts wire this
|
|
145
|
-
* into the runtime via registerService('notifications', createNotificationService(opts)).
|
|
146
|
-
* The shell host decides presentation (toast, badge, OS notification, etc.)
|
|
147
|
-
* through the onChange callback. Browser-agnostic — no DOM, no window.
|
|
148
|
-
*/
|
|
149
|
-
|
|
150
145
|
/**
|
|
151
146
|
* Create a notification service handler.
|
|
152
147
|
*
|
|
@@ -186,17 +181,53 @@ declare function createNotificationService(options?: NotificationServiceOptions)
|
|
|
186
181
|
* - signer.nip04.encrypt/decrypt -> DELETED
|
|
187
182
|
* - signer.nip44.encrypt/decrypt -> DELETED (shell encrypts internally
|
|
188
183
|
* inside relay.publishEncrypted)
|
|
184
|
+
* - identity.decrypt -> ADDED in v1.8 as a shell-mediated decrypt request
|
|
185
|
+
* with class gate + typed error union.
|
|
189
186
|
*
|
|
190
187
|
* See REQUIREMENTS.md DEPS-03 (Phase 15 changelog).
|
|
191
188
|
*
|
|
192
|
-
* Handles
|
|
189
|
+
* Handles 10 identity.* request types from @napplet/nub/identity. getPublicKey
|
|
193
190
|
* and getRelays return real values sourced from hooks.auth.getSigner(); the
|
|
194
191
|
* remaining 7 (getProfile/getFollows/getList/getZaps/getMutes/getBlocked/
|
|
195
|
-
* getBadges) are stub-level
|
|
196
|
-
*
|
|
197
|
-
* runtime.registerService('identity', realHandler).
|
|
192
|
+
* getBadges) are stub-level; decrypt delegates to a host-supplied bridge.
|
|
193
|
+
* Host apps plug real backends via runtime.registerService('identity', realHandler).
|
|
198
194
|
*/
|
|
199
195
|
|
|
196
|
+
type IdentityDecryptErrorCode = 'class-forbidden' | 'signer-denied' | 'signer-unavailable' | 'decrypt-failed' | 'malformed-wrap' | 'impersonation' | 'unsupported-encryption' | 'policy-denied';
|
|
197
|
+
interface Rumor {
|
|
198
|
+
id: string;
|
|
199
|
+
pubkey: string;
|
|
200
|
+
created_at: number;
|
|
201
|
+
kind: number;
|
|
202
|
+
tags: string[][];
|
|
203
|
+
content: string;
|
|
204
|
+
}
|
|
205
|
+
interface IdentityDecryptMessage extends NappletMessage {
|
|
206
|
+
type: 'identity.decrypt';
|
|
207
|
+
id: string;
|
|
208
|
+
event: NostrEvent;
|
|
209
|
+
}
|
|
210
|
+
interface IdentityDecryptResultMessage extends NappletMessage {
|
|
211
|
+
type: 'identity.decrypt.result';
|
|
212
|
+
id: string;
|
|
213
|
+
rumor: Rumor;
|
|
214
|
+
sender: string;
|
|
215
|
+
}
|
|
216
|
+
interface IdentityDecryptErrorMessage extends NappletMessage {
|
|
217
|
+
type: 'identity.decrypt.error';
|
|
218
|
+
id: string;
|
|
219
|
+
error: IdentityDecryptErrorCode;
|
|
220
|
+
}
|
|
221
|
+
interface GiftWrapDecryptResult {
|
|
222
|
+
seal: NostrEvent;
|
|
223
|
+
rumor: Rumor;
|
|
224
|
+
}
|
|
225
|
+
interface HostDecryptBridge {
|
|
226
|
+
nip04Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;
|
|
227
|
+
nip44Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;
|
|
228
|
+
unwrapGiftWrap(wrap: NostrEvent): Promise<GiftWrapDecryptResult>;
|
|
229
|
+
}
|
|
230
|
+
type VerifyEvent = (event: NostrEvent) => boolean | Promise<boolean>;
|
|
200
231
|
/**
|
|
201
232
|
* Options for creating the identity service.
|
|
202
233
|
*
|
|
@@ -215,14 +246,27 @@ interface IdentityServiceOptions {
|
|
|
215
246
|
* availability can change dynamically.
|
|
216
247
|
*/
|
|
217
248
|
getSigner: () => Signer | null;
|
|
249
|
+
/**
|
|
250
|
+
* Return the host decrypt bridge. Called only after outer event signature
|
|
251
|
+
* verification and encryption-mode detection succeed. Null means decrypt is
|
|
252
|
+
* unavailable while the rest of the identity service remains usable.
|
|
253
|
+
*/
|
|
254
|
+
getDecryptor?: () => HostDecryptBridge | null;
|
|
255
|
+
/**
|
|
256
|
+
* Verify a received event before any decrypt attempt. Host shells should
|
|
257
|
+
* wire this to their canonical Nostr event verifier; tests and old hosts
|
|
258
|
+
* default to true for backward compatibility with the 9 read-only actions.
|
|
259
|
+
*/
|
|
260
|
+
verifyEvent?: VerifyEvent;
|
|
218
261
|
}
|
|
219
262
|
/**
|
|
220
263
|
* Create an identity service that handles NIP-5D identity.* envelope messages.
|
|
221
264
|
*
|
|
222
|
-
* Supports all
|
|
265
|
+
* Supports all 10 identity.* request types from @napplet/nub/identity. The two
|
|
223
266
|
* read-only nostr-info queries (getPublicKey, getRelays) resolve through the
|
|
224
267
|
* caller-supplied signer; the remaining 7 return default/empty payloads with
|
|
225
268
|
* spec-correct envelope shapes so napplets always receive a result envelope.
|
|
269
|
+
* identity.decrypt delegates to the host decrypt bridge.
|
|
226
270
|
*
|
|
227
271
|
* @param options - Identity service configuration (getSigner)
|
|
228
272
|
* @returns A ServiceHandler ready for runtime.registerService('identity', handler)
|
|
@@ -328,15 +372,6 @@ interface RelayPoolServiceOptions {
|
|
|
328
372
|
*/
|
|
329
373
|
declare function createRelayPoolService(options: RelayPoolServiceOptions): ServiceHandler;
|
|
330
374
|
|
|
331
|
-
/**
|
|
332
|
-
* cache-service.ts — Local event cache as a ServiceHandler.
|
|
333
|
-
*
|
|
334
|
-
* Wraps an existing cache implementation (query, store, isAvailable)
|
|
335
|
-
* as a ServiceHandler that receives relay NUB envelope messages. Cache
|
|
336
|
-
* subscriptions are one-shot queries — relay.subscribe triggers a query
|
|
337
|
-
* and immediate EOSE, unlike relay pool subscriptions which stay open.
|
|
338
|
-
*/
|
|
339
|
-
|
|
340
375
|
/**
|
|
341
376
|
* Options for creating a cache service.
|
|
342
377
|
*
|
|
@@ -370,6 +405,31 @@ interface CacheServiceOptions {
|
|
|
370
405
|
*/
|
|
371
406
|
isAvailable(): boolean;
|
|
372
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* @kehto/services cross-package naming-parity alias for {@link CacheServiceOptions}.
|
|
410
|
+
*
|
|
411
|
+
* `HostCacheBridge` matches the v1.4 `HostKeysBridge` / `HostMediaBridge`
|
|
412
|
+
* convention — it is a pure type alias for `CacheServiceOptions`, NOT a new
|
|
413
|
+
* type. Existing consumers of `CacheServiceOptions` continue to work
|
|
414
|
+
* unchanged; new consumers may prefer `HostCacheBridge` for consistency
|
|
415
|
+
* with the other Host*Bridge names in `@kehto/services`.
|
|
416
|
+
*
|
|
417
|
+
* Anti-feature note (PITFALLS.md M-02): `CacheServiceOptions` MUST remain
|
|
418
|
+
* the primary export. This alias is additive; do not rename or delete
|
|
419
|
+
* `CacheServiceOptions` when other Host*Bridge names eventually
|
|
420
|
+
* stabilize.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```ts
|
|
424
|
+
* import type { HostCacheBridge } from '@kehto/services';
|
|
425
|
+
* const cache: HostCacheBridge = {
|
|
426
|
+
* query: (filters) => myIndexedDB.query(filters),
|
|
427
|
+
* store: (event) => myIndexedDB.store(event),
|
|
428
|
+
* isAvailable: () => true,
|
|
429
|
+
* };
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
type HostCacheBridge = CacheServiceOptions;
|
|
373
433
|
/**
|
|
374
434
|
* Create a cache service that wraps an existing cache implementation
|
|
375
435
|
* as a ServiceHandler.
|
|
@@ -395,17 +455,6 @@ interface CacheServiceOptions {
|
|
|
395
455
|
*/
|
|
396
456
|
declare function createCacheService(options: CacheServiceOptions): ServiceHandler;
|
|
397
457
|
|
|
398
|
-
/**
|
|
399
|
-
* coordinated-relay.ts — Composite relay + cache ServiceHandler.
|
|
400
|
-
*
|
|
401
|
-
* Combines relay pool and cache into a single service that handles
|
|
402
|
-
* relay.subscribe by querying both sources, deduplicating events by ID,
|
|
403
|
-
* and sending a unified EOSE after both sources complete.
|
|
404
|
-
*
|
|
405
|
-
* This is a convenience helper for shell implementors. Those who need
|
|
406
|
-
* custom coordination can write their own composite service.
|
|
407
|
-
*/
|
|
408
|
-
|
|
409
458
|
/**
|
|
410
459
|
* Options for creating a coordinated relay service.
|
|
411
460
|
*
|
|
@@ -468,27 +517,126 @@ interface CoordinatedRelayOptions {
|
|
|
468
517
|
declare function createCoordinatedRelay(options: CoordinatedRelayOptions): ServiceHandler;
|
|
469
518
|
|
|
470
519
|
/**
|
|
471
|
-
* keys-service.ts — NIP-5D keys NUB reference
|
|
520
|
+
* keys-service.ts — NIP-5D keys NUB reference document-level listener implementation.
|
|
472
521
|
*
|
|
473
|
-
* Handles the 3 napplet -> shell request types from @napplet/nub
|
|
522
|
+
* Handles the 3 napplet -> shell request types from @napplet/nub/keys:
|
|
474
523
|
* - keys.forward -> invokes options.onForward (hotkey passthrough, fire-and-forget)
|
|
475
|
-
* - keys.registerAction ->
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
524
|
+
* - keys.registerAction -> parses action.defaultKey into a chord spec, stores the
|
|
525
|
+
* subscription in an in-memory registry keyed by actionId,
|
|
526
|
+
* tracks windowId ownership so onWindowDestroyed can auto-
|
|
527
|
+
* unsubscribe, and echoes { actionId, binding } as .result
|
|
528
|
+
* - keys.unregisterAction -> removes the subscription; fire-and-forget (no envelope)
|
|
529
|
+
*
|
|
530
|
+
* Real listener: on service construction the handler attaches a single
|
|
531
|
+
* `keydown` listener to `options.listenerTarget` (default: `document`). Each
|
|
532
|
+
* keydown is matched against the chord-subscription registry; matches invoke
|
|
533
|
+
* `options.onForward` with the DOM-shape payload AND push a canonical
|
|
534
|
+
* `keys.action` envelope back to the owning napplet via the per-window `send`
|
|
535
|
+
* callback captured at `keys.registerAction` time. Subscriptions persist
|
|
536
|
+
* across messages; `onWindowDestroyed(windowId)` drops all subscriptions owned
|
|
537
|
+
* by the destroyed window as well as its cached `send` handle.
|
|
538
|
+
*
|
|
539
|
+
* On each document keydown matching a registered action, the service
|
|
540
|
+
* additionally emits a `keys.action` envelope to the action's owning napplet
|
|
541
|
+
* via the per-window `send` callback — this is the canonical @napplet/nub/keys
|
|
542
|
+
* surface the SDK's `keys.onAction(...)` helper consumes. The shape is a
|
|
543
|
+
* superset of `KeysActionMessage`: `{ type, actionId, chord }` where `chord`
|
|
544
|
+
* is the parsed `{ ctrl, alt, shift, meta, key }` struct (extension field;
|
|
545
|
+
* base shape unchanged, downstream SDKs that only read `{ type, actionId }`
|
|
546
|
+
* ignore `chord` silently).
|
|
547
|
+
*
|
|
548
|
+
* Field-name translation: @napplet/nub/keys uses the compact
|
|
482
549
|
* { ctrl, alt, shift, meta } form on the wire; the shell's HotkeyHooks
|
|
483
550
|
* (packages/shell/src/types.ts) expects the DOM-compatible
|
|
484
551
|
* { ctrlKey, altKey, shiftKey, metaKey } form. This service performs the
|
|
485
552
|
* translation so callers of `onForward` see the DOM shape.
|
|
486
553
|
*
|
|
487
|
-
* Shell -> napplet push envelopes
|
|
488
|
-
*
|
|
489
|
-
*
|
|
554
|
+
* Shell -> napplet push envelopes `keys.bindings` remain the shell-side keys
|
|
555
|
+
* forwarder's responsibility (DRIFT-SHELL-06, tracked under Plan 12-11 /
|
|
556
|
+
* future phase); `keys.action` is emitted here per Plan 26-01.
|
|
490
557
|
*/
|
|
491
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Minimal structural subset of the DOM `KeyboardEvent` exposed to
|
|
561
|
+
* `HostKeysBridge` subscribe callbacks. DOM `KeyboardEvent` satisfies this
|
|
562
|
+
* structurally with no adapter needed. OS-bridge impls (Electron, Tauri —
|
|
563
|
+
* out of v1.4 scope) synthesize this from native key events.
|
|
564
|
+
*/
|
|
565
|
+
interface HostKeyEvent {
|
|
566
|
+
key: string;
|
|
567
|
+
code: string;
|
|
568
|
+
ctrlKey: boolean;
|
|
569
|
+
altKey: boolean;
|
|
570
|
+
shiftKey: boolean;
|
|
571
|
+
metaKey: boolean;
|
|
572
|
+
/** True for OS autorepeat; the service filters these by default. */
|
|
573
|
+
repeat?: boolean;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Host-bridge contract for pluggable keyboard backends.
|
|
577
|
+
*
|
|
578
|
+
* The browser reference implementation (the default {@link createKeysService}
|
|
579
|
+
* behaviour when `hostBridge` is omitted) registers a `document`-level keydown
|
|
580
|
+
* listener and satisfies this interface structurally — it exposes
|
|
581
|
+
* `subscribe(chord, callback) => unsubscribe` semantics but omits the two
|
|
582
|
+
* OS-level optional fields (browsers cannot register global hotkeys without
|
|
583
|
+
* privileged APIs).
|
|
584
|
+
*
|
|
585
|
+
* Host apps (Electron, Tauri) implement this interface in their own code and
|
|
586
|
+
* pass it via `createKeysService({ hostBridge: myBridge })` — the service
|
|
587
|
+
* then delegates subscription lifecycle to the bridge and remains browser-free.
|
|
588
|
+
*
|
|
589
|
+
* Reference implementations for Electron / Tauri are explicitly out of v1.4
|
|
590
|
+
* scope and live in host-app examples / follow-up milestones (see
|
|
591
|
+
* REQUIREMENTS.md "Future Requirements").
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```ts
|
|
595
|
+
* // Host-app pseudocode (Electron main-process relay):
|
|
596
|
+
* const electronBridge: HostKeysBridge = {
|
|
597
|
+
* subscribe(chord, cb) {
|
|
598
|
+
* const handle = globalShortcut.register(chord, () => cb({ key: '', code: '', ctrlKey: false, altKey: false, shiftKey: false, metaKey: false }));
|
|
599
|
+
* return () => globalShortcut.unregister(chord);
|
|
600
|
+
* },
|
|
601
|
+
* registerGlobalHotkey: (chord) => globalShortcut.register(chord, () => {}),
|
|
602
|
+
* onGlobalHotkey: (cb) => globalHotkeyBridge.on('global-hotkey', (_, chord) => cb(chord)),
|
|
603
|
+
* };
|
|
604
|
+
*
|
|
605
|
+
* const keys = createKeysService({ hostBridge: electronBridge });
|
|
606
|
+
* runtime.registerService('keys', keys);
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
interface HostKeysBridge {
|
|
610
|
+
/**
|
|
611
|
+
* Subscribe a callback to a chord. Returns an unsubscribe handle.
|
|
612
|
+
*
|
|
613
|
+
* Implementations MUST:
|
|
614
|
+
* - invoke `callback` exactly once per matching chord event (implementations
|
|
615
|
+
* are responsible for any OS-autorepeat filtering)
|
|
616
|
+
* - invoke `callback` synchronously during the event delivery
|
|
617
|
+
* - accept the string chord format documented by @napplet/nub/keys
|
|
618
|
+
* (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)
|
|
619
|
+
*/
|
|
620
|
+
subscribe(chord: string, callback: (event: KeyboardEvent | HostKeyEvent) => void): () => void;
|
|
621
|
+
/**
|
|
622
|
+
* Optional: register an OS-level global hotkey (works even when the host
|
|
623
|
+
* window is not focused). Returns true on success, false if the chord
|
|
624
|
+
* cannot be registered (e.g. already claimed by another app).
|
|
625
|
+
*
|
|
626
|
+
* Omitted by the browser reference implementation — browsers cannot
|
|
627
|
+
* register OS-level global hotkeys without privileged APIs. Electron
|
|
628
|
+
* (`globalShortcut`) and Tauri (`GlobalShortcut`) provide this.
|
|
629
|
+
*/
|
|
630
|
+
registerGlobalHotkey?(chord: string): boolean;
|
|
631
|
+
/**
|
|
632
|
+
* Optional: subscribe to OS-level global hotkey events (regardless of
|
|
633
|
+
* focus). Returns an unsubscribe handle.
|
|
634
|
+
*
|
|
635
|
+
* Omitted by the browser reference implementation. See
|
|
636
|
+
* {@link HostKeysBridge.registerGlobalHotkey}.
|
|
637
|
+
*/
|
|
638
|
+
onGlobalHotkey?(callback: (chord: string) => void): () => void;
|
|
639
|
+
}
|
|
492
640
|
/**
|
|
493
641
|
* Options for creating a keys service via createKeysService().
|
|
494
642
|
*
|
|
@@ -504,7 +652,8 @@ declare function createCoordinatedRelay(options: CoordinatedRelayOptions): Servi
|
|
|
504
652
|
*/
|
|
505
653
|
interface KeysServiceOptions {
|
|
506
654
|
/**
|
|
507
|
-
* Called on keys.forward
|
|
655
|
+
* Called on `keys.forward` (napplet-forwarded chord) AND on document keydown
|
|
656
|
+
* matching a registered action. Receives the DOM-style field names
|
|
508
657
|
* (ctrlKey/altKey/shiftKey/metaKey) to match the shell's HotkeyHooks
|
|
509
658
|
* contract. The service translates from the wire shape
|
|
510
659
|
* ({ ctrl, alt, shift, meta }) before invoking this callback.
|
|
@@ -517,17 +666,62 @@ interface KeysServiceOptions {
|
|
|
517
666
|
shiftKey: boolean;
|
|
518
667
|
metaKey: boolean;
|
|
519
668
|
}) => void;
|
|
669
|
+
/**
|
|
670
|
+
* EventTarget to attach the default keydown listener to. Defaults to the
|
|
671
|
+
* global `document` when running in a DOM environment, else an isolated
|
|
672
|
+
* `new EventTarget()` (SSR / Node-test safe). Passing a fresh
|
|
673
|
+
* `new EventTarget()` is useful for unit tests. Mirrors the pattern used
|
|
674
|
+
* by `@kehto/shell`'s `createKeysForwarder`.
|
|
675
|
+
*
|
|
676
|
+
* Ignored when `hostBridge` is provided — the bridge owns subscription
|
|
677
|
+
* lifecycle and no document listener is attached.
|
|
678
|
+
*/
|
|
679
|
+
listenerTarget?: EventTarget;
|
|
680
|
+
/**
|
|
681
|
+
* Optional pluggable backend for chord subscription. When provided, the
|
|
682
|
+
* service delegates `keys.registerAction` → `bridge.subscribe(chord, cb)`
|
|
683
|
+
* and stores the returned unsubscribe handle keyed on `actionId`. The
|
|
684
|
+
* default document-listener path is NOT attached when `hostBridge` is
|
|
685
|
+
* provided — the bridge is authoritative. See {@link HostKeysBridge}.
|
|
686
|
+
*/
|
|
687
|
+
hostBridge?: HostKeysBridge;
|
|
688
|
+
/**
|
|
689
|
+
* Optional set of shell-reserved chords. Strings in the `@napplet/nub/keys`
|
|
690
|
+
* wire format (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`). When a napplet sends
|
|
691
|
+
* `keys.forward` with a chord matching this set — or when a document
|
|
692
|
+
* keydown matches a reserved chord — the service invokes `onForward`
|
|
693
|
+
* (or the `hostBridge`-registered handler) but suppresses the
|
|
694
|
+
* `keys.action` push to any napplet that registered the same chord via
|
|
695
|
+
* `keys.registerAction`. Precedence: reserved > registered. The shell
|
|
696
|
+
* WANTS the forward — that is why it reserved the chord.
|
|
697
|
+
*
|
|
698
|
+
* Normalized once at service construction via the same chord parser used
|
|
699
|
+
* for `action.defaultKey` — `'Ctrl+K'` / `'Control+k'` / `'ctrl+K'` all
|
|
700
|
+
* match. Static; no runtime mutation. For dynamic reservation see the
|
|
701
|
+
* deferred `HostKeysBridge.reserveAbsolute(chords)` extension.
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```ts
|
|
705
|
+
* const keys = createKeysService({
|
|
706
|
+
* reservedChords: ['Ctrl+Alt+T', 'Super+Space'],
|
|
707
|
+
* onForward: (event) => wm.dispatch(event),
|
|
708
|
+
* });
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
reservedChords?: ReadonlyArray<string>;
|
|
520
712
|
}
|
|
521
713
|
/**
|
|
522
714
|
* Create a keys service handler.
|
|
523
715
|
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
716
|
+
* Attaches a single `keydown` listener to `options.listenerTarget`
|
|
717
|
+
* (default `document`). Matching chord subscriptions invoke `options.onForward`
|
|
718
|
+
* with a DOM-shape payload AND push a `keys.action` envelope back to the
|
|
719
|
+
* owning napplet via the per-window `send` callback captured at
|
|
720
|
+
* `keys.registerAction` time. Returns a `ServiceHandler` augmented with a
|
|
721
|
+
* `destroy()` method that detaches the listener and clears all registries.
|
|
528
722
|
*
|
|
529
|
-
* @param options - Optional configuration (onForward callback)
|
|
530
|
-
* @returns A ServiceHandler to register with the runtime
|
|
723
|
+
* @param options - Optional configuration (onForward callback, listenerTarget)
|
|
724
|
+
* @returns A ServiceHandler (with `destroy()`) to register with the runtime
|
|
531
725
|
*
|
|
532
726
|
* @example
|
|
533
727
|
* ```ts
|
|
@@ -538,32 +732,229 @@ interface KeysServiceOptions {
|
|
|
538
732
|
* });
|
|
539
733
|
*
|
|
540
734
|
* runtime.registerService('keys', keys);
|
|
735
|
+
* // Later, on shell teardown:
|
|
736
|
+
* keys.destroy();
|
|
541
737
|
* ```
|
|
542
738
|
*/
|
|
543
|
-
declare function createKeysService(options?: KeysServiceOptions): ServiceHandler
|
|
739
|
+
declare function createKeysService(options?: KeysServiceOptions): ServiceHandler & {
|
|
740
|
+
destroy(): void;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
/** Minimal subset of navigator.mediaSession the browser bridge depends on. Makes the bridge
|
|
744
|
+
* Node/test-safe: unit tests pass a MockMediaSession via mediaSessionTarget.
|
|
745
|
+
* The handler parameter uses `details?` (optional) so both the real DOM impl
|
|
746
|
+
* (which always passes an object) and test mocks that omit details both satisfy
|
|
747
|
+
* this type structurally. */
|
|
748
|
+
type MediaSessionTarget = {
|
|
749
|
+
metadata: MediaMetadataLike | null;
|
|
750
|
+
playbackState: 'none' | 'playing' | 'paused';
|
|
751
|
+
setActionHandler(action: string, handler: ((details?: {
|
|
752
|
+
action?: string;
|
|
753
|
+
seekTime?: number;
|
|
754
|
+
}) => void) | null): void;
|
|
755
|
+
};
|
|
756
|
+
/** Structural subset of the DOM MediaMetadata class — assignable from a plain object
|
|
757
|
+
* with title/artist/album/artwork fields. The browser impl uses `new MediaMetadata({...})`;
|
|
758
|
+
* tests can pass a plain object. */
|
|
759
|
+
type MediaMetadataLike = {
|
|
760
|
+
title?: string;
|
|
761
|
+
artist?: string;
|
|
762
|
+
album?: string;
|
|
763
|
+
artwork?: unknown;
|
|
764
|
+
};
|
|
765
|
+
/**
|
|
766
|
+
* Reference browser implementation of {@link HostMediaBridge}.
|
|
767
|
+
*
|
|
768
|
+
* Mirrors metadata to `navigator.mediaSession.metadata` (via the DOM
|
|
769
|
+
* `MediaMetadata` constructor when available; plain-object fallback in test
|
|
770
|
+
* envs). Mirrors playback state to `navigator.mediaSession.playbackState`
|
|
771
|
+
* with the canonical mapping: 'playing' maps to 'playing', 'paused' maps to
|
|
772
|
+
* 'paused', 'buffering' maps to 'paused', 'stopped' maps to 'none'. Installs
|
|
773
|
+
* `setActionHandler` callbacks for play/pause/nexttrack/previoustrack/seekto
|
|
774
|
+
* that fan into the onAction subscriber with the mapped `MediaAction` literal
|
|
775
|
+
* (and `value` from `details.seekTime` for seekto). When `setActiveSession` is
|
|
776
|
+
* called with a non-null `actions` parameter, only the declared actions get
|
|
777
|
+
* active handlers — the remaining are cleared (matching the capabilities
|
|
778
|
+
* narrowing behavior of Plan 27-01's inline implementation).
|
|
779
|
+
*
|
|
780
|
+
* Installs a silent-audio prime (4 kHz silent WAV data URL) when the first
|
|
781
|
+
* session becomes active (setActiveSession called with a non-null sessionId) —
|
|
782
|
+
* browsers refuse to render OS media controls without a playing audio element.
|
|
783
|
+
* Removes the element when destroySession brings the active session count to
|
|
784
|
+
* zero.
|
|
785
|
+
*
|
|
786
|
+
* @param opts.mediaSessionTarget - Override navigator.mediaSession (tests).
|
|
787
|
+
* @param opts.documentTarget - Override document (tests; pass null to disable silent-audio prime).
|
|
788
|
+
*/
|
|
789
|
+
declare function createBrowserMediaBridge(opts?: {
|
|
790
|
+
mediaSessionTarget?: MediaSessionTarget;
|
|
791
|
+
documentTarget?: Document | null;
|
|
792
|
+
}): HostMediaBridge;
|
|
544
793
|
|
|
545
794
|
/**
|
|
546
|
-
* media-service.ts — NIP-5D media NUB reference service (
|
|
795
|
+
* media-service.ts — NIP-5D media NUB reference service (navigator.mediaSession
|
|
796
|
+
* reference implementation).
|
|
547
797
|
*
|
|
548
|
-
* Handles
|
|
798
|
+
* Handles the napplet-owned subset of @napplet/nub/media:
|
|
549
799
|
* media.session.create (result), media.session.update, media.session.destroy,
|
|
550
800
|
* media.state, media.capabilities.
|
|
551
801
|
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
802
|
+
* HostMediaBridge contract: {@link HostMediaBridge} defines the pluggable backend
|
|
803
|
+
* contract for metadata/state mirroring + action routing. The browser reference
|
|
804
|
+
* implementation is {@link createBrowserMediaBridge} (mirrors to navigator.mediaSession
|
|
805
|
+
* with setActionHandler matrix). When no hostBridge option is passed, createMediaService
|
|
806
|
+
* internally uses createBrowserMediaBridge as the default — behavior is identical
|
|
807
|
+
* to the Plan 27-01 single-path implementation.
|
|
808
|
+
*
|
|
809
|
+
* navigator.mediaSession mirroring: on session.create the browser bridge mirrors the
|
|
810
|
+
* napplet-supplied metadata to navigator.mediaSession.metadata via new MediaMetadata()
|
|
811
|
+
* and installs setActionHandler callbacks for the 5 OS transport actions
|
|
812
|
+
* (play / pause / nexttrack / previoustrack / seekto). Each callback emits a canonical
|
|
813
|
+
* media.command envelope to the owning napplet — that is the @napplet/nub/media
|
|
814
|
+
* MediaCommandMessage shape consumed by the SDK's mediaOnCommand() helper.
|
|
815
|
+
*
|
|
816
|
+
* NAP-MEDIA now distinguishes napplet-owned playback from shell-owned
|
|
817
|
+
* playback. This reference backend supports `owner: "napplet"` because it
|
|
818
|
+
* mirrors a napplet's own media element to navigator.mediaSession. It rejects
|
|
819
|
+
* `owner: "shell"` until a host bridge provides policy-checked source fetching
|
|
820
|
+
* and playback.
|
|
821
|
+
*
|
|
822
|
+
* media.command push: when the OS user clicks a media control (hardware key, lock-screen
|
|
823
|
+
* transport, OS media overlay), the bridge's onAction callback fires. The service looks
|
|
824
|
+
* up the active session's owning napplet, invokes the per-window send callback captured
|
|
825
|
+
* at session.create time, and delivers:
|
|
826
|
+
* { type: 'media.command', sessionId, action, value? }
|
|
827
|
+
* where action is the nub-media MediaAction literal (play|pause|next|prev|seek) and
|
|
828
|
+
* value is the seekTime (seconds) for seek.
|
|
829
|
+
*
|
|
830
|
+
* Silent-audio prime: the browser bridge creates a hidden <audio> element with a
|
|
831
|
+
* 4 kHz silent WAV data URL and plays it when the first session becomes active
|
|
832
|
+
* (setActiveSession with a non-null sessionId). Without a playing audio element in
|
|
833
|
+
* the host page, most browsers refuse to render OS media controls. The element is
|
|
834
|
+
* cleaned up when the last session is destroyed (via bridge.destroySession).
|
|
835
|
+
*
|
|
836
|
+
* Multi-session registry with last-active-wins semantics: every session.create is
|
|
837
|
+
* tracked in a Map<sessionId, SessionEntry>; any media.state report promotes that
|
|
838
|
+
* session to active. The active session's metadata and playback state are reflected
|
|
839
|
+
* via bridge.setMetadata / bridge.setPlaybackState. When the active session is
|
|
840
|
+
* destroyed, the next-most-recently-touched session is promoted automatically.
|
|
554
841
|
*
|
|
555
842
|
* Note: this is SEPARATE from packages/services/src/audio-service.ts, which
|
|
556
843
|
* is the legacy ifc-topic-based audio source registry (audio:* topic events
|
|
557
|
-
* over ifc.emit). media-service is the canonical @napplet/nub
|
|
844
|
+
* over ifc.emit). media-service is the canonical @napplet/nub/media NIP-5D
|
|
558
845
|
* path and they coexist — audio-service continues to track audio sources for
|
|
559
846
|
* shell UI, while media-service handles the NUB protocol envelope surface.
|
|
560
847
|
*
|
|
561
|
-
* Shell -> Napplet push types (media.command
|
|
562
|
-
*
|
|
848
|
+
* Shell -> Napplet push types (media.command) are emitted here when
|
|
849
|
+
* bridge.onAction callbacks fire — this is the canonical Phase 27 real backend.
|
|
563
850
|
*/
|
|
564
851
|
|
|
852
|
+
type MediaPlaybackOwner = 'shell' | 'napplet';
|
|
853
|
+
interface MediaSourceRef {
|
|
854
|
+
url?: string;
|
|
855
|
+
blossomHash?: string;
|
|
856
|
+
nostr?: {
|
|
857
|
+
eventId?: string;
|
|
858
|
+
address?: string;
|
|
859
|
+
relays?: string[];
|
|
860
|
+
};
|
|
861
|
+
mimeType?: string;
|
|
862
|
+
}
|
|
863
|
+
interface MediaSessionCreateOptions {
|
|
864
|
+
owner: MediaPlaybackOwner;
|
|
865
|
+
sessionId?: string;
|
|
866
|
+
source?: MediaSourceRef;
|
|
867
|
+
metadata?: MediaMetadata;
|
|
868
|
+
capabilities?: MediaAction[];
|
|
869
|
+
autoplay?: boolean;
|
|
870
|
+
live?: boolean;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Host-bridge contract for pluggable media backends.
|
|
874
|
+
*
|
|
875
|
+
* The browser reference implementation ({@link createBrowserMediaBridge}) mirrors
|
|
876
|
+
* napplet-reported metadata/state to `navigator.mediaSession` and installs
|
|
877
|
+
* `setActionHandler` callbacks that fan into the bridge's onAction subscribers.
|
|
878
|
+
* It satisfies this interface with all 5 fields implemented (setActiveSession
|
|
879
|
+
* switches the active session and optionally re-applies action-handler narrowing;
|
|
880
|
+
* destroySession tears down the silent-audio prime on last-session teardown).
|
|
881
|
+
*
|
|
882
|
+
* Host apps (Electron, Tauri) implement this interface in their own code and
|
|
883
|
+
* pass it via `createMediaService({ hostBridge: myBridge })` — the service
|
|
884
|
+
* then delegates metadata/state mirroring + action routing to the bridge and
|
|
885
|
+
* remains browser-free. Session-ownership bookkeeping (which windowId owns
|
|
886
|
+
* which sessionId, which send callback routes media.command back to which
|
|
887
|
+
* napplet) stays in the service layer — that's wire-protocol concern, not a
|
|
888
|
+
* bridge concern.
|
|
889
|
+
*
|
|
890
|
+
* Reference implementations for Electron / Tauri are explicitly out of v1.4
|
|
891
|
+
* scope and live in host-app examples / follow-up milestones (see
|
|
892
|
+
* REQUIREMENTS.md "Future Requirements").
|
|
893
|
+
*
|
|
894
|
+
* @example
|
|
895
|
+
* ```ts
|
|
896
|
+
* // Host-app pseudocode (Electron main-process relay):
|
|
897
|
+
* const electronBridge: HostMediaBridge = {
|
|
898
|
+
* setMetadata(sessionId, md) { mediaBridge.sendMetadata({ sessionId, md }); },
|
|
899
|
+
* setPlaybackState(sessionId, state) { mediaBridge.sendPlaybackState({ sessionId, state }); },
|
|
900
|
+
* onAction(cb) {
|
|
901
|
+
* const handler = (_: unknown, msg: { sessionId: string; action: MediaAction; value?: number }) =>
|
|
902
|
+
* cb(msg.sessionId, msg.action, msg.value);
|
|
903
|
+
* mediaBridge.onAction(handler);
|
|
904
|
+
* return () => mediaBridge.offAction(handler);
|
|
905
|
+
* },
|
|
906
|
+
* };
|
|
907
|
+
* const media = createMediaService({ hostBridge: electronBridge });
|
|
908
|
+
* runtime.registerService('media', media);
|
|
909
|
+
* ```
|
|
910
|
+
*/
|
|
911
|
+
interface HostMediaBridge {
|
|
912
|
+
/**
|
|
913
|
+
* Set the metadata displayed on the OS transport surface for a session.
|
|
914
|
+
* Called on session.create (with initial metadata) and on session.update
|
|
915
|
+
* (with merged metadata) whenever the session is the active session.
|
|
916
|
+
* Implementations MUST be idempotent.
|
|
917
|
+
*/
|
|
918
|
+
setMetadata(sessionId: string, metadata: MediaMetadata): void;
|
|
919
|
+
/**
|
|
920
|
+
* Set the playback state for a session. Called on media.state reports
|
|
921
|
+
* whenever the session is the active session. State strings match
|
|
922
|
+
* nub-media MediaState.status exactly. Implementations MUST be idempotent.
|
|
923
|
+
*/
|
|
924
|
+
setPlaybackState(sessionId: string, state: 'playing' | 'paused' | 'stopped' | 'buffering'): void;
|
|
925
|
+
/**
|
|
926
|
+
* Subscribe to OS-level action events (user clicks play/pause/seek/next/prev
|
|
927
|
+
* on the transport surface). Returns an unsubscribe handle.
|
|
928
|
+
*
|
|
929
|
+
* The callback receives `(sessionId, action, value?)`. `sessionId` is the
|
|
930
|
+
* bridge's currently-active session (the browser impl tracks this internally
|
|
931
|
+
* via setActionHandler-at-fire-time; native impls track via setActiveSession).
|
|
932
|
+
* `value` is populated for `action === 'seek'` (seek target in seconds) and
|
|
933
|
+
* for `action === 'volume'` (0.0-1.0). The service dispatches the resulting
|
|
934
|
+
* `media.command` envelope to the owning napplet of that session.
|
|
935
|
+
*/
|
|
936
|
+
onAction(callback: (sessionId: string, action: MediaAction, value?: number) => void): () => void;
|
|
937
|
+
/**
|
|
938
|
+
* Optional: notify the bridge that the active session has changed. The
|
|
939
|
+
* browser reference impl uses this to switch which session's metadata/state
|
|
940
|
+
* is mirrored to the singleton navigator.mediaSession and to install (or
|
|
941
|
+
* clear) action handlers for the session's declared capabilities.
|
|
942
|
+
*
|
|
943
|
+
* The optional `actions` parameter carries the session's declared capability
|
|
944
|
+
* set so the bridge can narrow which OS transport buttons are active. When
|
|
945
|
+
* omitted, the bridge applies its default set. Native OS bridges that track
|
|
946
|
+
* active-session state internally may omit this field entirely.
|
|
947
|
+
*/
|
|
948
|
+
setActiveSession?(sessionId: string | null, actions?: readonly MediaAction[]): void;
|
|
949
|
+
/**
|
|
950
|
+
* Optional: tear down per-session resources. The browser reference impl
|
|
951
|
+
* uses this to remove the silent-audio prime element when the last session
|
|
952
|
+
* is destroyed. Bridges that need no per-session teardown may omit this field.
|
|
953
|
+
*/
|
|
954
|
+
destroySession?(sessionId: string): void;
|
|
955
|
+
}
|
|
565
956
|
/**
|
|
566
|
-
* Optional host callbacks for the
|
|
957
|
+
* Optional host callbacks for the media service.
|
|
567
958
|
*
|
|
568
959
|
* Host shells can hook session creation and state updates to drive
|
|
569
960
|
* their own UI (e.g., now-playing banner) without replacing the
|
|
@@ -594,12 +985,32 @@ interface MediaServiceOptions {
|
|
|
594
985
|
onSessionUpdate?: (windowId: string, sessionId: string, metadata: unknown) => void;
|
|
595
986
|
/** Called when a napplet declares capabilities for a session. */
|
|
596
987
|
onCapabilities?: (windowId: string, sessionId: string, actions: unknown) => void;
|
|
988
|
+
/**
|
|
989
|
+
* MediaSession target override (used by default browser bridge only).
|
|
990
|
+
* Defaults to `navigator.mediaSession` when running in a browser. Pass a
|
|
991
|
+
* MockMediaSession in unit tests. Ignored when `hostBridge` is provided.
|
|
992
|
+
*/
|
|
993
|
+
mediaSessionTarget?: MediaSessionTarget;
|
|
994
|
+
/**
|
|
995
|
+
* DOM document override (used by default browser bridge only).
|
|
996
|
+
* Defaults to `document` when available. Set to null to disable the silent-audio
|
|
997
|
+
* prime entirely — useful in unit tests. Ignored when `hostBridge` is provided.
|
|
998
|
+
*/
|
|
999
|
+
documentTarget?: Document | null;
|
|
1000
|
+
/**
|
|
1001
|
+
* Optional pluggable backend for metadata/state mirroring + OS action handling.
|
|
1002
|
+
* When provided, the service delegates setMetadata / setPlaybackState / onAction
|
|
1003
|
+
* to the bridge and skips navigator.mediaSession entirely. When omitted, the
|
|
1004
|
+
* service internally uses {@link createBrowserMediaBridge} as the default.
|
|
1005
|
+
* See {@link HostMediaBridge}.
|
|
1006
|
+
*/
|
|
1007
|
+
hostBridge?: HostMediaBridge;
|
|
597
1008
|
}
|
|
598
1009
|
/**
|
|
599
|
-
* Create a
|
|
1010
|
+
* Create a media NUB service handler with navigator.mediaSession integration.
|
|
600
1011
|
*
|
|
601
1012
|
* Implements the 5 napplet->shell media.* request types defined in
|
|
602
|
-
* `@napplet/nub
|
|
1013
|
+
* `@napplet/nub/media`. Only `media.session.create` produces a reply
|
|
603
1014
|
* envelope (`media.session.create.result`) — the remaining four
|
|
604
1015
|
* (`session.update`, `session.destroy`, `state`, `capabilities`) are
|
|
605
1016
|
* fire-and-forget per the NUB spec.
|
|
@@ -607,8 +1018,10 @@ interface MediaServiceOptions {
|
|
|
607
1018
|
* Unknown `media.*` actions produce a `<type>.error` envelope so
|
|
608
1019
|
* napplets are never left hanging on a malformed request.
|
|
609
1020
|
*
|
|
610
|
-
* @param options - Optional host callbacks for session lifecycle + state
|
|
611
|
-
*
|
|
1021
|
+
* @param options - Optional host callbacks for session lifecycle + state, plus
|
|
1022
|
+
* mediaSessionTarget and documentTarget for test injection, and an optional
|
|
1023
|
+
* hostBridge for native-OS media backend delegation.
|
|
1024
|
+
* @returns A ServiceHandler (with `destroy()`) to register with the runtime
|
|
612
1025
|
*
|
|
613
1026
|
* @example
|
|
614
1027
|
* ```ts
|
|
@@ -616,14 +1029,18 @@ interface MediaServiceOptions {
|
|
|
616
1029
|
*
|
|
617
1030
|
* const media = createMediaService();
|
|
618
1031
|
* runtime.registerService('media', media);
|
|
1032
|
+
* // Later, on shell teardown:
|
|
1033
|
+
* media.destroy();
|
|
619
1034
|
* ```
|
|
620
1035
|
*/
|
|
621
|
-
declare function createMediaService(options?: MediaServiceOptions): ServiceHandler
|
|
1036
|
+
declare function createMediaService(options?: MediaServiceOptions): ServiceHandler & {
|
|
1037
|
+
destroy(): void;
|
|
1038
|
+
};
|
|
622
1039
|
|
|
623
1040
|
/**
|
|
624
1041
|
* notify-service.ts — NIP-5D notify NUB reference service (stub-level).
|
|
625
1042
|
*
|
|
626
|
-
* Handles the 5 napplet -> shell request types from `@napplet/nub
|
|
1043
|
+
* Handles the 5 napplet -> shell request types from `@napplet/nub/notify`:
|
|
627
1044
|
* - `notify.send` -> `notify.send.result` (shell-assigned id)
|
|
628
1045
|
* - `notify.dismiss` -> fire-and-forget
|
|
629
1046
|
* - `notify.badge` -> fire-and-forget
|
|
@@ -638,9 +1055,9 @@ declare function createMediaService(options?: MediaServiceOptions): ServiceHandl
|
|
|
638
1055
|
* `packages/services/src/notification-service.ts`, which is the legacy
|
|
639
1056
|
* ifc-topic-based notification registry (operates on `ifc.emit` topics
|
|
640
1057
|
* under `notifications:*`). `notify-service.ts` is the canonical
|
|
641
|
-
* `@napplet/nub
|
|
1058
|
+
* `@napplet/nub/notify` NIP-5D path and lives alongside the legacy module.
|
|
642
1059
|
* If the host app registers this service via
|
|
643
|
-
* `runtime.registerService('notify', ...)`, @napplet/nub
|
|
1060
|
+
* `runtime.registerService('notify', ...)`, @napplet/nub/notify messages
|
|
644
1061
|
* land here; the legacy ifc-emit topic path remains untouched.
|
|
645
1062
|
*
|
|
646
1063
|
* Shell -> napplet push messages (`notify.action`, `notify.clicked`,
|
|
@@ -684,7 +1101,7 @@ interface NotifyServiceOptions {
|
|
|
684
1101
|
/**
|
|
685
1102
|
* Create a stub-level notify service handler.
|
|
686
1103
|
*
|
|
687
|
-
* Answers the 5 napplet->shell request types from `@napplet/nub
|
|
1104
|
+
* Answers the 5 napplet->shell request types from `@napplet/nub/notify`.
|
|
688
1105
|
* Does NOT implement a real backend (no DOM Notification API, no channel
|
|
689
1106
|
* registry, no permission prompt). Host apps replace this via
|
|
690
1107
|
* `runtime.registerService('notify', realHandler)` when a real backend is
|
|
@@ -706,7 +1123,7 @@ declare function createNotifyService(options?: NotifyServiceOptions): ServiceHan
|
|
|
706
1123
|
/**
|
|
707
1124
|
* theme-service.ts — NIP-5D theme NUB reference service.
|
|
708
1125
|
*
|
|
709
|
-
* Handles the single napplet->shell request type from @napplet/nub
|
|
1126
|
+
* Handles the single napplet->shell request type from @napplet/nub/theme:
|
|
710
1127
|
* - `theme.get` -> `theme.get.result { theme }` (current theme)
|
|
711
1128
|
*
|
|
712
1129
|
* Exposes a host-facing `publishTheme(theme)` handle that:
|
|
@@ -807,4 +1224,312 @@ interface ThemeService {
|
|
|
807
1224
|
*/
|
|
808
1225
|
declare function createThemeService(options?: ThemeServiceOptions): ThemeService;
|
|
809
1226
|
|
|
810
|
-
|
|
1227
|
+
/**
|
|
1228
|
+
* config-service.ts — NUB-CONFIG reference service (9th NUB domain, v1.7 Phase 39).
|
|
1229
|
+
*
|
|
1230
|
+
* Shell-side reference implementation for the canonical NUB-CONFIG wire
|
|
1231
|
+
* protocol (`@napplet/nub/config`, published at `^0.3.0`). Handles the full
|
|
1232
|
+
* 8-message discriminated union: 5 napplet→shell request types + 3
|
|
1233
|
+
* shell→napplet result/push types.
|
|
1234
|
+
*
|
|
1235
|
+
* ──────────────────────────── SCOPE BOUNDARY (CONFIG-04) ─────────────────────────
|
|
1236
|
+
* NUB-CONFIG is **shell-managed per-napplet configuration**. Napplets observe
|
|
1237
|
+
* values via `config.get` (one-shot) or `config.subscribe` (snapshot + live
|
|
1238
|
+
* push). The shell is the **sole writer** — there is intentionally **NO**
|
|
1239
|
+
* `config.set` wire message. Napplets cannot mutate configuration values;
|
|
1240
|
+
* the shell owns persistence and the update flow.
|
|
1241
|
+
*
|
|
1242
|
+
* Do NOT use this service as a general key-value store. NUB-STORAGE
|
|
1243
|
+
* (`state:read` / `state:write`) remains the general KV surface. Using
|
|
1244
|
+
* NUB-CONFIG to store e.g. `{ lastScrollPosition: 420 }` is an anti-pattern
|
|
1245
|
+
* (H-07 in PITFALLS.md) — such state belongs in NUB-STORAGE.
|
|
1246
|
+
* ──────────────────────────────────────────────────────────────────────────────────
|
|
1247
|
+
*
|
|
1248
|
+
* Host integration: provide `getValues()` returning the current
|
|
1249
|
+
* `ConfigValues` snapshot. Call the returned `publishValues(newValues)`
|
|
1250
|
+
* whenever the configuration changes — the service fans the new snapshot
|
|
1251
|
+
* out to every napplet that has an active `config.subscribe`.
|
|
1252
|
+
*
|
|
1253
|
+
* Optional: provide `registerSchema` to accept napplet-declared schemas at
|
|
1254
|
+
* runtime (the ref impl does a minimal shape check using the Core Subset
|
|
1255
|
+
* validator; use `ajv` in host impls that need strict draft-07 conformance).
|
|
1256
|
+
* Provide `openSettings` to open a shell-side UI for the napplet (no
|
|
1257
|
+
* response envelope — fire-and-forget UI hook).
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* ```ts
|
|
1261
|
+
* import { createConfigService } from '@kehto/services';
|
|
1262
|
+
*
|
|
1263
|
+
* const configFixtures = { theme: 'dark', density: 'compact', recentSearches: [] };
|
|
1264
|
+
* const config = createConfigService({
|
|
1265
|
+
* getValues: () => ({ ...configFixtures }),
|
|
1266
|
+
* });
|
|
1267
|
+
* runtime.registerService('config', config.handler);
|
|
1268
|
+
*
|
|
1269
|
+
* // Later, when shell-side values change:
|
|
1270
|
+
* configFixtures.theme = 'light';
|
|
1271
|
+
* config.publishValues({ ...configFixtures });
|
|
1272
|
+
* ```
|
|
1273
|
+
*/
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Shape returned by a successful `registerSchema` result (ok=true) or a
|
|
1277
|
+
* rejection (ok=false + code + error). Mirrors the wire envelope fields.
|
|
1278
|
+
*/
|
|
1279
|
+
type ConfigSchemaValidation = {
|
|
1280
|
+
ok: true;
|
|
1281
|
+
} | {
|
|
1282
|
+
ok: false;
|
|
1283
|
+
code: ConfigSchemaErrorCode;
|
|
1284
|
+
error: string;
|
|
1285
|
+
};
|
|
1286
|
+
/**
|
|
1287
|
+
* Configuration options for `createConfigService` (options-as-bridge
|
|
1288
|
+
* per v1.6 Decision 18).
|
|
1289
|
+
*
|
|
1290
|
+
* @example
|
|
1291
|
+
* ```ts
|
|
1292
|
+
* const config = createConfigService({
|
|
1293
|
+
* getValues: () => ({ theme: 'dark', density: 'compact' }),
|
|
1294
|
+
* openSettings: (windowId, section) => showSettingsPanel(windowId, section),
|
|
1295
|
+
* });
|
|
1296
|
+
* ```
|
|
1297
|
+
*/
|
|
1298
|
+
interface ConfigServiceOptions {
|
|
1299
|
+
/**
|
|
1300
|
+
* Returns the current configuration values snapshot.
|
|
1301
|
+
* Called on every `config.get` and at every `config.subscribe` initial push.
|
|
1302
|
+
* Implementations should return a fresh object (not a mutable reference).
|
|
1303
|
+
*/
|
|
1304
|
+
getValues(): ConfigValues;
|
|
1305
|
+
/**
|
|
1306
|
+
* Optional: receive notification when a napplet subscribes to config updates.
|
|
1307
|
+
* Fire-and-forget — the service tracks the subscription internally regardless.
|
|
1308
|
+
*/
|
|
1309
|
+
onSubscribe?: (windowId: string) => void;
|
|
1310
|
+
/**
|
|
1311
|
+
* Optional: receive notification when a napplet unsubscribes.
|
|
1312
|
+
*/
|
|
1313
|
+
onUnsubscribe?: (windowId: string) => void;
|
|
1314
|
+
/**
|
|
1315
|
+
* Optional: validate and store a napplet-provided schema.
|
|
1316
|
+
*
|
|
1317
|
+
* If omitted, the ref impl runs its own Core Subset check (hand-coded
|
|
1318
|
+
* validator; 30-50 lines) and returns ok/reject. Hosts that need strict
|
|
1319
|
+
* draft-07 conformance should provide an ajv-backed implementation.
|
|
1320
|
+
*
|
|
1321
|
+
* Return shape mirrors `config.registerSchema.result` wire envelope
|
|
1322
|
+
* (minus the `id` — the dispatch layer correlates).
|
|
1323
|
+
*/
|
|
1324
|
+
registerSchema?: (windowId: string, schema: NappletConfigSchema, version: number | undefined) => ConfigSchemaValidation;
|
|
1325
|
+
/**
|
|
1326
|
+
* Optional: open the shell-side settings UI for this napplet.
|
|
1327
|
+
* Fire-and-forget — no response envelope per the wire spec.
|
|
1328
|
+
* If omitted, `config.openSettings` is silently dropped (D10 allows
|
|
1329
|
+
* the config-demo napplet to function without a settings UI).
|
|
1330
|
+
*/
|
|
1331
|
+
openSettings?: (windowId: string, section: string | undefined) => void;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* NUB-CONFIG reference service bundle — `handler` to register with the
|
|
1335
|
+
* runtime, `publishValues` for the host app to push updates live to all
|
|
1336
|
+
* subscribed napplets.
|
|
1337
|
+
*/
|
|
1338
|
+
interface ConfigService {
|
|
1339
|
+
/** Register this with the runtime via `runtime.registerService('config', handler)`. */
|
|
1340
|
+
handler: ServiceHandler;
|
|
1341
|
+
/**
|
|
1342
|
+
* Broadcast a new values snapshot to every napplet with an active
|
|
1343
|
+
* `config.subscribe`. Each subscriber receives a `config.values` envelope
|
|
1344
|
+
* with no `id` (push form per wire spec — absence of `id` distinguishes
|
|
1345
|
+
* push from correlated `config.get` response).
|
|
1346
|
+
*
|
|
1347
|
+
* @param values - The new configuration snapshot (full object, not a diff)
|
|
1348
|
+
*/
|
|
1349
|
+
publishValues(values: ConfigValues): void;
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Create a NUB-CONFIG reference service.
|
|
1353
|
+
*
|
|
1354
|
+
* Shell-writes, napplet-reads. Handles the full `@napplet/nub/config` wire
|
|
1355
|
+
* protocol: `config.get` (correlated snapshot), `config.subscribe` /
|
|
1356
|
+
* `config.unsubscribe` (live push stream), `config.registerSchema` (optional
|
|
1357
|
+
* schema registration + Core Subset validation), `config.openSettings`
|
|
1358
|
+
* (optional UI deep-link, fire-and-forget).
|
|
1359
|
+
*
|
|
1360
|
+
* Returns a `ConfigService` bundle: `{ handler, publishValues }`.
|
|
1361
|
+
* Register `handler` with the runtime; call `publishValues(newValues)` from
|
|
1362
|
+
* the shell whenever config state changes.
|
|
1363
|
+
*
|
|
1364
|
+
* @param options - Host-supplied implementation hooks (options-as-bridge,
|
|
1365
|
+
* v1.6 Decision 18). `getValues` is required; all other fields are optional.
|
|
1366
|
+
* @returns A ConfigService bundle.
|
|
1367
|
+
*
|
|
1368
|
+
* @see ConfigServiceOptions for the options shape.
|
|
1369
|
+
* @see packages/services/src/theme-service.ts for the sibling pattern.
|
|
1370
|
+
* @see SCOPE BOUNDARY comment at the top of this file re: NUB-STORAGE separation.
|
|
1371
|
+
*
|
|
1372
|
+
* @example
|
|
1373
|
+
* ```ts
|
|
1374
|
+
* import { createConfigService } from '@kehto/services';
|
|
1375
|
+
*
|
|
1376
|
+
* const config = createConfigService({
|
|
1377
|
+
* getValues: () => ({ theme: 'dark', density: 'compact' }),
|
|
1378
|
+
* openSettings: (windowId, section) => openSettingsUI(section),
|
|
1379
|
+
* });
|
|
1380
|
+
* runtime.registerService('config', config.handler);
|
|
1381
|
+
*
|
|
1382
|
+
* // Push a live update to all subscribers:
|
|
1383
|
+
* config.publishValues({ theme: 'light', density: 'compact' });
|
|
1384
|
+
* ```
|
|
1385
|
+
*/
|
|
1386
|
+
declare function createConfigService(options: ConfigServiceOptions): ConfigService;
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* resource-service.ts — NUB-RESOURCE reference service (10th NUB domain, v1.7 Phase 40).
|
|
1390
|
+
*
|
|
1391
|
+
* Shell-side reference implementation for the canonical NUB-RESOURCE wire
|
|
1392
|
+
* protocol (`internal-resource.ts` in @kehto/shell/src/types; kehto-internal
|
|
1393
|
+
* model per PROJECT.md Decision #31 — diverges from upstream `@napplet/nub/
|
|
1394
|
+
* resource` in field names + error vocabulary). Handles the canonical
|
|
1395
|
+
* 4-message protocol:
|
|
1396
|
+
* Inbound: resource.bytes, resource.cancel
|
|
1397
|
+
* Outbound: resource.bytes.result, resource.bytes.error
|
|
1398
|
+
*
|
|
1399
|
+
* ──────────────────────── SCOPE BOUNDARY (RESOURCE-01) ────────────────────────
|
|
1400
|
+
* NUB-RESOURCE is an **authenticated fetch proxy** — read-only, atomic.
|
|
1401
|
+
*
|
|
1402
|
+
* This service is NOT responsible for:
|
|
1403
|
+
* - Streaming / chunked responses (host-app concern)
|
|
1404
|
+
* - Response caching / conditional requests (host-app concern)
|
|
1405
|
+
* - Upload / POST body construction (NUB-RESOURCE v1.7 is read-only)
|
|
1406
|
+
* - Redirect limits, MIME sniffing, SVG rasterization (host-fetch concern)
|
|
1407
|
+
* - Private-IP blocking, SSRF mitigation (host-provided-fetch responsibility)
|
|
1408
|
+
*
|
|
1409
|
+
* These belong to the host-app's `fetch` implementation per D7 and
|
|
1410
|
+
* SHELL-RESOURCE-POLICY.md (Phase 40 Plan 40-03). Kehto ships a reference
|
|
1411
|
+
* service; production hardening is the host app's concern.
|
|
1412
|
+
* ──────────────────────────────────────────────────────────────────────────────
|
|
1413
|
+
*
|
|
1414
|
+
* Host integration: provide `fetch`, `isOriginGranted`, `getConnectGrants`,
|
|
1415
|
+
* and `resolveIdentity`. ALL FOUR are required from day one (H-03 prevention).
|
|
1416
|
+
*
|
|
1417
|
+
* @example
|
|
1418
|
+
* ```ts
|
|
1419
|
+
* import { createResourceService } from '@kehto/services';
|
|
1420
|
+
*
|
|
1421
|
+
* const resourceSvc = createResourceService({
|
|
1422
|
+
* fetch: (url, init) => globalThis.fetch(url, init),
|
|
1423
|
+
* isOriginGranted: (origin, grants) => grants.includes(origin),
|
|
1424
|
+
* getConnectGrants: (dTag, hash) => connectStore.getOrigins(dTag, hash),
|
|
1425
|
+
* resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,
|
|
1426
|
+
* });
|
|
1427
|
+
* runtime.registerService('resource', resourceSvc);
|
|
1428
|
+
* ```
|
|
1429
|
+
*/
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Options for `createResourceService` (options-as-bridge per v1.6 Decision 18).
|
|
1433
|
+
*
|
|
1434
|
+
* ALL FOUR fields are required. The factory throws at construction if any is
|
|
1435
|
+
* missing — H-03 prevention: the grants source (`getConnectGrants`) MUST be
|
|
1436
|
+
* wired from day one so there is no window where resource requests bypass the
|
|
1437
|
+
* grant check.
|
|
1438
|
+
*
|
|
1439
|
+
* @see PITFALLS.md:228 (H-03) — grants-source coupling must be present at construction
|
|
1440
|
+
*/
|
|
1441
|
+
interface ResourceServiceOptions {
|
|
1442
|
+
/**
|
|
1443
|
+
* Host-supplied fetch implementation. Receives the URL, a partial init
|
|
1444
|
+
* (method, headers, signal), and must return a `Response`-compatible promise.
|
|
1445
|
+
*
|
|
1446
|
+
* The host's `fetch` is the ONLY place to implement redirect limits, MIME
|
|
1447
|
+
* sniffing, SVG rasterization, private-IP / SSRF blocking, etc.
|
|
1448
|
+
* This service does NOT filter: it proxies transparently.
|
|
1449
|
+
*
|
|
1450
|
+
* @param url - The URL from the resource.bytes request
|
|
1451
|
+
* @param init - Method, headers (from napplet), and an AbortSignal
|
|
1452
|
+
*/
|
|
1453
|
+
fetch(url: string, init: {
|
|
1454
|
+
method?: string;
|
|
1455
|
+
headers?: Record<string, string>;
|
|
1456
|
+
signal: AbortSignal;
|
|
1457
|
+
}): Promise<Response>;
|
|
1458
|
+
/**
|
|
1459
|
+
* Returns true if `origin` is present in `grants` (the list returned by
|
|
1460
|
+
* `getConnectGrants` for the napplet's dTag + aggregateHash).
|
|
1461
|
+
*
|
|
1462
|
+
* The reference implementation is simply `grants.includes(origin)`. Host apps
|
|
1463
|
+
* may provide normalized-origin comparison if needed.
|
|
1464
|
+
*
|
|
1465
|
+
* @param origin - Parsed origin of the requested URL (scheme + host + port)
|
|
1466
|
+
* @param grants - Readonly list from getConnectGrants for this napplet identity
|
|
1467
|
+
*/
|
|
1468
|
+
isOriginGranted(origin: string, grants: readonly string[]): boolean;
|
|
1469
|
+
/**
|
|
1470
|
+
* Returns the list of allowed fetch origins for the given napplet identity.
|
|
1471
|
+
* Called on every `resource.bytes` request — must be synchronous and fast.
|
|
1472
|
+
*
|
|
1473
|
+
* Typically wraps `connectStore.getOrigins(dTag, aggregateHash)` from
|
|
1474
|
+
* @kehto/shell.
|
|
1475
|
+
*
|
|
1476
|
+
* H-03 prevention: REQUIRED from day one — factory throws on construction
|
|
1477
|
+
* if omitted.
|
|
1478
|
+
*
|
|
1479
|
+
* @param dTag - The napplet's d-tag (from session registry)
|
|
1480
|
+
* @param aggregateHash - The napplet's aggregate hash (from session registry)
|
|
1481
|
+
*/
|
|
1482
|
+
getConnectGrants(dTag: string, aggregateHash: string): readonly string[];
|
|
1483
|
+
/**
|
|
1484
|
+
* Resolve a windowId to the napplet's identity (dTag + aggregateHash).
|
|
1485
|
+
* Returns null if the window is not in the session registry.
|
|
1486
|
+
*
|
|
1487
|
+
* Typically wraps `sessionRegistry.getEntryByWindowId(windowId)`.
|
|
1488
|
+
*
|
|
1489
|
+
* @param windowId - The iframe window identifier
|
|
1490
|
+
*/
|
|
1491
|
+
resolveIdentity(windowId: string): {
|
|
1492
|
+
dTag: string;
|
|
1493
|
+
aggregateHash: string;
|
|
1494
|
+
} | null;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Type alias for the ServiceHandler returned by `createResourceService`.
|
|
1498
|
+
* Exported for host apps that need to type-annotate the handler reference.
|
|
1499
|
+
*/
|
|
1500
|
+
type ResourceService = ServiceHandler;
|
|
1501
|
+
/**
|
|
1502
|
+
* Create a NUB-RESOURCE reference service.
|
|
1503
|
+
*
|
|
1504
|
+
* Implements canonical 4-message protocol: `resource.bytes` (napplet → shell
|
|
1505
|
+
* fetch request), `resource.cancel` (napplet → shell in-flight cancellation),
|
|
1506
|
+
* `resource.bytes.result` (shell → napplet success), `resource.bytes.error`
|
|
1507
|
+
* (shell → napplet failure/denial/cancel).
|
|
1508
|
+
*
|
|
1509
|
+
* On-construction guard (H-03 prevention): all four options are validated at
|
|
1510
|
+
* factory call time. If any is missing, the factory throws immediately with a
|
|
1511
|
+
* message containing `[RESOURCE-01 / H-03]` so misconfigured shell apps fail
|
|
1512
|
+
* loudly at startup rather than silently at first dispatch.
|
|
1513
|
+
*
|
|
1514
|
+
* Returns a `ServiceHandler` (no `publishValues`-style surface — resource has
|
|
1515
|
+
* no shell-initiated push beyond the response/error path).
|
|
1516
|
+
*
|
|
1517
|
+
* @param options - REQUIRED: fetch, isOriginGranted, getConnectGrants, resolveIdentity
|
|
1518
|
+
* @returns ServiceHandler to register via `runtime.registerService('resource', handler)`
|
|
1519
|
+
*
|
|
1520
|
+
* @example
|
|
1521
|
+
* ```ts
|
|
1522
|
+
* import { createResourceService } from '@kehto/services';
|
|
1523
|
+
*
|
|
1524
|
+
* const svc = createResourceService({
|
|
1525
|
+
* fetch: (url, init) => globalThis.fetch(url, init),
|
|
1526
|
+
* isOriginGranted: (origin, grants) => grants.includes(origin),
|
|
1527
|
+
* getConnectGrants: (dTag, hash) => connectStore.getOrigins(dTag, hash),
|
|
1528
|
+
* resolveIdentity: (windowId) => sessionRegistry.getEntryByWindowId(windowId) ?? null,
|
|
1529
|
+
* });
|
|
1530
|
+
* runtime.registerService('resource', svc);
|
|
1531
|
+
* ```
|
|
1532
|
+
*/
|
|
1533
|
+
declare function createResourceService(options: ResourceServiceOptions): ResourceService;
|
|
1534
|
+
|
|
1535
|
+
export { type AudioServiceOptions, type AudioSource, type CacheServiceOptions, type ConfigSchemaValidation, type ConfigService, type ConfigServiceOptions, type CoordinatedRelayOptions, type GiftWrapDecryptResult, type HostCacheBridge, type HostDecryptBridge, type HostKeyEvent, type HostKeysBridge, type HostMediaBridge, type IdentityDecryptErrorCode, type IdentityDecryptErrorMessage, type IdentityDecryptMessage, type IdentityDecryptResultMessage, type IdentityServiceOptions, type KeysServiceOptions, type MediaMetadataLike, type MediaPlaybackOwner, type MediaServiceOptions, type MediaSessionCreateOptions, type MediaSessionTarget, type MediaSourceRef, type Notification, type NotificationServiceOptions, type NotifyServiceOptions, type RelayPoolServiceOptions, type ResourceService, type ResourceServiceOptions, type Rumor, type ThemeService, type ThemeServiceOptions, type VerifyEvent, createAudioService, createBrowserMediaBridge, createCacheService, createConfigService, createCoordinatedRelay, createIdentityService, createKeysService, createMediaService, createNotificationService, createNotifyService, createRelayPoolService, createResourceService, createThemeService };
|