@kehto/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1259 @@
1
+ import { NappletMessage, NostrEvent, NostrFilter } from '@napplet/core';
2
+ export { NappletMessage } from '@napplet/core';
3
+ import { Capability } from '@kehto/acl/capabilities';
4
+ export { ALL_CAPABILITIES, Capability } from '@kehto/acl/capabilities';
5
+ import { NubMessage } from '@kehto/acl';
6
+ export { NubMessage, resolveCapabilitiesNub } from '@kehto/acl';
7
+
8
+ /**
9
+ * types.ts — Runtime adapter interfaces and supporting types.
10
+ *
11
+ * RuntimeAdapter is the abstract contract any environment must implement
12
+ * to host napplets. No DOM types, no browser APIs.
13
+ */
14
+
15
+ /**
16
+ * Event emitted on every ACL enforcement check.
17
+ *
18
+ * @param identity - The napplet identity being checked
19
+ * @param capability - The capability being checked
20
+ * @param decision - Whether the check passed or failed
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * hooks.onAclCheck = (event: AclCheckEvent) => {
25
+ * console.log(`${event.decision}: ${event.capability} for ${event.identity.pubkey}`);
26
+ * };
27
+ * ```
28
+ */
29
+ interface AclCheckEvent {
30
+ /** The identity being checked. */
31
+ identity: {
32
+ pubkey: string;
33
+ dTag: string;
34
+ hash: string;
35
+ };
36
+ /** The capability being checked (e.g., 'relay:write', 'state:read'). */
37
+ capability: string;
38
+ /** The enforcement decision. */
39
+ decision: 'allow' | 'deny';
40
+ /** The triggering message, if available. Accepts NIP-01 arrays or NIP-5D NappletMessage envelopes. */
41
+ message?: unknown[] | NappletMessage;
42
+ }
43
+ /**
44
+ * Abstract message sender — the runtime calls this to send messages
45
+ * back to a specific napplet. The transport layer (postMessage, WebSocket,
46
+ * IPC channel, etc.) is the implementor's concern.
47
+ *
48
+ * Accepts both NIP-01 array format (legacy) and NIP-5D NappletMessage envelope format.
49
+ *
50
+ * @param windowId - Target napplet's identifier
51
+ * @param msg - NIP-01 message array (e.g., ['EVENT', subId, event]) or NIP-5D envelope
52
+ */
53
+ type SendToNapplet = (windowId: string, msg: unknown[] | NappletMessage) => void;
54
+ /** Handle returned by relay pool subscriptions. */
55
+ interface RelaySubscriptionHandle {
56
+ unsubscribe(): void;
57
+ }
58
+ /**
59
+ * Abstract relay pool — runtime uses this to subscribe to and publish
60
+ * events on real Nostr relays. Implementor wraps their relay library.
61
+ */
62
+ interface RelayPoolAdapter {
63
+ /**
64
+ * Subscribe to events from relays matching the given filters.
65
+ * The callback receives either 'EOSE' (end of stored events) or a NostrEvent.
66
+ * Returns a handle that can cancel the subscription.
67
+ */
68
+ subscribe(filters: NostrFilter[], callback: (item: NostrEvent | 'EOSE') => void, relayUrls?: string[]): RelaySubscriptionHandle;
69
+ /** Publish an event to relays. */
70
+ publish(event: NostrEvent): void;
71
+ /** Select relay URLs appropriate for the given filters. */
72
+ selectRelayTier(filters: NostrFilter[]): string[];
73
+ /** Track a subscription key for lifecycle management. */
74
+ trackSubscription(subKey: string, cleanup: () => void): void;
75
+ /** Untrack and clean up a subscription. */
76
+ untrackSubscription(subKey: string): void;
77
+ /** Open a scoped relay connection (NIP-29 groups). */
78
+ openScopedRelay(windowId: string, relayUrl: string, subId: string, filters: NostrFilter[], sendToNapplet: SendToNapplet): void;
79
+ /** Close a scoped relay connection. */
80
+ closeScopedRelay(windowId: string): void;
81
+ /** Publish to a scoped relay. Returns false if no active scoped relay. */
82
+ publishToScopedRelay(windowId: string, event: NostrEvent): boolean;
83
+ /** Whether a relay pool is available. */
84
+ isAvailable(): boolean;
85
+ }
86
+ /** Abstract local cache — query and store events. */
87
+ interface CacheAdapter {
88
+ /** Query cached events. Returns matching events. */
89
+ query(filters: NostrFilter[]): Promise<NostrEvent[]>;
90
+ /** Store an event in cache. Best-effort, may silently fail. */
91
+ store(event: NostrEvent): void;
92
+ /** Whether cache is available. */
93
+ isAvailable(): boolean;
94
+ }
95
+ /** NIP-07 compatible signer interface — minimal methods the runtime needs. */
96
+ interface Signer {
97
+ getPublicKey?(): string | Promise<string>;
98
+ signEvent?(event: NostrEvent): Promise<NostrEvent>;
99
+ getRelays?(): Record<string, {
100
+ read: boolean;
101
+ write: boolean;
102
+ }> | Promise<Record<string, {
103
+ read: boolean;
104
+ write: boolean;
105
+ }>>;
106
+ nip04?: {
107
+ encrypt(pubkey: string, plaintext: string): Promise<string>;
108
+ decrypt(pubkey: string, ciphertext: string): Promise<string>;
109
+ };
110
+ nip44?: {
111
+ encrypt(pubkey: string, plaintext: string): Promise<string>;
112
+ decrypt(pubkey: string, ciphertext: string): Promise<string>;
113
+ };
114
+ }
115
+ /** Auth adapter — user identity and signing. */
116
+ interface AuthAdapter {
117
+ /** Get the current user's pubkey, or null if not logged in. */
118
+ getUserPubkey(): string | null;
119
+ /** Get the signer, or null if unavailable. */
120
+ getSigner(): Signer | null;
121
+ }
122
+ /** Config adapter — runtime behavior settings. */
123
+ interface ConfigAdapter {
124
+ /** Get the napp update behavior policy. */
125
+ getNappUpdateBehavior(): 'auto-grant' | 'banner' | 'silent-reprompt';
126
+ }
127
+ /** Hotkey adapter — keyboard shortcut forwarding. */
128
+ interface HotkeyAdapter {
129
+ /** Execute a forwarded hotkey from a napp. */
130
+ executeHotkeyFromForward(event: {
131
+ key: string;
132
+ code: string;
133
+ ctrlKey: boolean;
134
+ altKey: boolean;
135
+ shiftKey: boolean;
136
+ metaKey: boolean;
137
+ }): void;
138
+ }
139
+ /**
140
+ * ACL persistence — runtime calls these to save/load ACL state.
141
+ * Implementor decides storage backend (localStorage, file, DB, etc.).
142
+ */
143
+ interface AclPersistence {
144
+ persist(data: string): void;
145
+ load(): string | null;
146
+ }
147
+ /**
148
+ * Manifest persistence — runtime calls these to save/load manifest cache.
149
+ * Implementor decides storage backend.
150
+ */
151
+ interface ManifestPersistence {
152
+ persist(data: string): void;
153
+ load(): string | null;
154
+ }
155
+ /**
156
+ * Shell secret persistence — runtime calls these to save/load the per-shell secret
157
+ * used for deterministic keypair derivation. The secret is a 32-byte random value
158
+ * generated once on first use.
159
+ */
160
+ interface ShellSecretPersistence {
161
+ /** Get the stored shell secret, or null if not yet generated. */
162
+ get(): Uint8Array | null;
163
+ /** Store the shell secret. */
164
+ set(secret: Uint8Array): void;
165
+ }
166
+ /**
167
+ * GUID persistence — runtime calls these to save/load per-iframe instance GUIDs.
168
+ * GUIDs survive page reloads: same iframe slot gets the same GUID.
169
+ * Implementor decides storage backend and keying strategy
170
+ * (e.g., localStorage keyed by iframe src or slot index).
171
+ */
172
+ interface GuidPersistence {
173
+ /** Get a stored GUID for a window identifier, or null if none exists. */
174
+ get(windowId: string): string | null;
175
+ /** Store a GUID for a window identifier. */
176
+ set(windowId: string, guid: string): void;
177
+ /** Remove a stored GUID. */
178
+ remove(windowId: string): void;
179
+ }
180
+ /**
181
+ * State storage — runtime calls these for napplet-scoped key-value storage.
182
+ * All keys are pre-scoped by the runtime (dTag:hash:userKey).
183
+ */
184
+ interface StatePersistence {
185
+ get(scopedKey: string): string | null;
186
+ set(scopedKey: string, value: string): boolean;
187
+ remove(scopedKey: string): void;
188
+ clear(prefix: string): void;
189
+ keys(prefix: string): string[];
190
+ calculateBytes(prefix: string, excludeKey?: string): number;
191
+ }
192
+ /** Crypto adapter — event verification. */
193
+ interface CryptoAdapter {
194
+ /** Verify a nostr event's Schnorr signature. */
195
+ verifyEvent(event: NostrEvent): Promise<boolean>;
196
+ /** Generate a random UUID string (replaces crypto.randomUUID). */
197
+ randomUUID(): string;
198
+ /** Generate cryptographically secure random bytes. */
199
+ randomBytes(length: number): Uint8Array;
200
+ }
201
+ /**
202
+ * Hash verification adapter — runtime calls this to verify a napplet's
203
+ * declared aggregate hash against its actual file contents.
204
+ * Optional: if not provided, hash verification is skipped (dev mode).
205
+ */
206
+ interface HashVerifierAdapter {
207
+ /**
208
+ * Compute aggregate hash from the napplet's served files.
209
+ * Returns the computed hash, or null if files cannot be fetched.
210
+ *
211
+ * @param nappletUrl - Base URL of the napplet (iframe src)
212
+ * @param manifestFiles - File paths and hashes from the manifest
213
+ * @returns Computed aggregate hash, or null on failure
214
+ */
215
+ computeHash(nappletUrl: string, manifestFiles: Array<{
216
+ path: string;
217
+ hash: string;
218
+ }>): Promise<string | null>;
219
+ }
220
+ /** Window management — create new napplet windows. */
221
+ interface WindowManagerAdapter {
222
+ createWindow(options: {
223
+ title: string;
224
+ class: string;
225
+ iframeSrc?: string;
226
+ }): string | null;
227
+ }
228
+ /** Relay configuration — manage relay tiers. */
229
+ interface RelayConfigAdapter {
230
+ addRelay(tier: string, url: string): void;
231
+ removeRelay(tier: string, url: string): void;
232
+ getRelayConfig(): {
233
+ discovery: string[];
234
+ super: string[];
235
+ outbox: string[];
236
+ };
237
+ getNip66Suggestions(): unknown;
238
+ }
239
+ /** DM adapter — send direct messages (NIP-17 gift-wrap). */
240
+ interface DmAdapter {
241
+ sendDm(recipientPubkey: string, message: string): Promise<{
242
+ success: boolean;
243
+ eventId?: string;
244
+ error?: string;
245
+ }>;
246
+ }
247
+ /**
248
+ * A pending consent request — either for a destructive signing kind
249
+ * or for undeclared service usage.
250
+ *
251
+ * When type is 'destructive-signing' (or omitted for backwards compat):
252
+ * Raised when a signer request arrives for kinds 0, 3, 5, 10002.
253
+ *
254
+ * When type is 'undeclared-service':
255
+ * Raised when a napplet uses a service it did not declare in its manifest.
256
+ * The serviceName field identifies which service was used without declaration.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * // Destructive signing consent (existing behavior)
261
+ * const signingConsent: ConsentRequest = {
262
+ * type: 'destructive-signing',
263
+ * windowId: 'win-1', pubkey: 'abc...', event: signingEvent,
264
+ * resolve: (allowed) => { ... },
265
+ * };
266
+ *
267
+ * // Undeclared service consent (new)
268
+ * const serviceConsent: ConsentRequest = {
269
+ * type: 'undeclared-service',
270
+ * windowId: 'win-1', pubkey: 'abc...', event: serviceEvent,
271
+ * serviceName: 'audio',
272
+ * resolve: (allowed) => { ... },
273
+ * };
274
+ * ```
275
+ */
276
+ interface ConsentRequest {
277
+ /** Consent type discriminator. Defaults to 'destructive-signing' if omitted. */
278
+ type?: 'destructive-signing' | 'undeclared-service';
279
+ windowId: string;
280
+ pubkey: string;
281
+ event: NostrEvent;
282
+ resolve: (allowed: boolean) => void;
283
+ /** Service name for undeclared-service consent. Only present when type is 'undeclared-service'. */
284
+ serviceName?: string;
285
+ }
286
+ /** Consent handler callback type. */
287
+ type ConsentHandler = (request: ConsentRequest) => void;
288
+ /**
289
+ * Metadata describing a service handler. Referenced by ServiceHandler.descriptor
290
+ * and the runtime's internal service registry.
291
+ *
292
+ * Relocated from the former @napplet/core compatibility shim (DRIFT-CORE-06
293
+ * deleted in Phase 24). Content unchanged from the v1.1-era definition; this
294
+ * is the canonical location going forward.
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * const descriptor: ServiceDescriptor = {
299
+ * name: 'audio',
300
+ * version: '1.0.0',
301
+ * description: 'Audio playback and mute control',
302
+ * };
303
+ * ```
304
+ */
305
+ interface ServiceDescriptor {
306
+ /** Service identifier (e.g., 'audio', 'notifications'). */
307
+ name: string;
308
+ /** Semver version of the service. */
309
+ version: string;
310
+ /** Optional human-readable description. */
311
+ description?: string;
312
+ }
313
+ /**
314
+ * Information about an available service, as reported in discovery responses.
315
+ * Mirrors the ServiceDescriptor shape from @napplet/core.
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * const info: ServiceInfo = {
320
+ * name: 'audio',
321
+ * version: '1.0.0',
322
+ * description: 'Audio playback and mute control',
323
+ * };
324
+ * ```
325
+ */
326
+ interface ServiceInfo {
327
+ /** Service identifier (e.g., 'audio', 'notifications'). */
328
+ name: string;
329
+ /** Semver version of the service. */
330
+ version: string;
331
+ /** Optional human-readable description. */
332
+ description?: string;
333
+ }
334
+ /**
335
+ * Result of checking a napplet's declared service requirements against
336
+ * the runtime's registered services.
337
+ *
338
+ * Surfaced via RuntimeAdapter.onCompatibilityIssue when compatible is false.
339
+ * In strict mode, the runtime blocks loading. In permissive mode (default),
340
+ * the runtime loads the napplet and the shell host decides UX.
341
+ *
342
+ * @example
343
+ * ```ts
344
+ * const report: CompatibilityReport = {
345
+ * available: [{ name: 'audio', version: '1.0.0' }],
346
+ * missing: ['notifications'],
347
+ * compatible: false,
348
+ * };
349
+ * ```
350
+ */
351
+ interface CompatibilityReport {
352
+ /** Services that the shell provides (full list from service registry). */
353
+ available: ServiceInfo[];
354
+ /** Service names declared in manifest requires but not registered in the runtime. */
355
+ missing: string[];
356
+ /** True if all required services are available (missing.length === 0). */
357
+ compatible: boolean;
358
+ }
359
+ /**
360
+ * Registry entry mapping a napplet's windowId to its runtime metadata.
361
+ * Created after a successful identity establishment (AUTH handshake or NIP-5D origin registration).
362
+ */
363
+ interface SessionEntry {
364
+ /**
365
+ * @deprecated NIP-5D: AUTH keypair no longer exists. Empty string for NIP-5D sessions.
366
+ * Kept for backward compatibility during legacy support period.
367
+ */
368
+ pubkey: string;
369
+ windowId: string;
370
+ origin: string;
371
+ type: string;
372
+ dTag: string;
373
+ aggregateHash: string;
374
+ registeredAt: number;
375
+ /** Persistent GUID for this iframe instance, assigned by the runtime. Survives page reloads. */
376
+ instanceId: string;
377
+ /**
378
+ * How session identity was established.
379
+ * 'source' = NIP-5D (identity registered at iframe creation via originRegistry).
380
+ * 'auth' = legacy AUTH handshake (pubkey is the derived keypair pubkey).
381
+ */
382
+ identitySource: 'auth' | 'source';
383
+ }
384
+ /** @deprecated Use SessionEntry. Will be removed in v0.9.0. */
385
+ type NappKeyEntry = SessionEntry;
386
+ /**
387
+ * A pending napplet update — raised when a napplet reconnects with a different aggregateHash.
388
+ */
389
+ interface PendingUpdate {
390
+ windowId: string;
391
+ pubkey: string;
392
+ dTag: string;
393
+ oldHash: string;
394
+ newHash: string;
395
+ resolve: (action: 'accept' | 'block') => void;
396
+ }
397
+ /** Callback invoked when a pending update is set or cleared. */
398
+ type PendingUpdateNotifier = (windowId: string) => void;
399
+ /**
400
+ * A cached manifest entry for a verified napplet build.
401
+ * Optionally stores the napplet's declared service requirements from its manifest.
402
+ */
403
+ interface ManifestCacheEntry {
404
+ pubkey: string;
405
+ dTag: string;
406
+ aggregateHash: string;
407
+ verifiedAt: number;
408
+ /** Service names declared in the napplet's manifest requires tags. */
409
+ requires?: string[];
410
+ }
411
+ /**
412
+ * Cached verification result for an aggregate hash.
413
+ * Keyed by manifest event ID — immutable Nostr events mean same ID = same content.
414
+ */
415
+ interface VerificationCacheEntry {
416
+ /** The computed aggregate hash. */
417
+ aggregateHash: string;
418
+ /** Whether the computed hash matched the declared hash. */
419
+ valid: boolean;
420
+ /** Timestamp when verification was performed. */
421
+ verifiedAt: number;
422
+ }
423
+ /** External ACL entry — used in shell commands (shell:acl-get etc.). */
424
+ interface AclEntryExternal {
425
+ pubkey: string;
426
+ capabilities: Capability[];
427
+ blocked: boolean;
428
+ stateQuota?: number;
429
+ }
430
+ /**
431
+ * Handler for service-specific messages from napplets.
432
+ * Services receive NIP-5D NappletMessage envelopes and respond via the `send` callback.
433
+ * The same interface is used for all services regardless of what NUB domain they handle.
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * const audioHandler: ServiceHandler = {
438
+ * descriptor: { name: 'audio', version: '1.0.0' },
439
+ * handleMessage(windowId, message, send) {
440
+ * if (message.type === 'ifc.emit') {
441
+ * // process audio ifc event...
442
+ * send({ type: 'ifc.emit.result', id: (message as any).id });
443
+ * }
444
+ * },
445
+ * };
446
+ * ```
447
+ */
448
+ interface ServiceHandler {
449
+ /** Metadata describing this service. */
450
+ descriptor: ServiceDescriptor;
451
+ /**
452
+ * Handle a NIP-5D envelope from a napplet.
453
+ *
454
+ * @param windowId - The requesting napplet's window identifier
455
+ * @param message - NappletMessage JSON envelope (e.g., { type: 'signer.signEvent', id, event })
456
+ * @param send - Callback to send NappletMessage responses back to the napplet
457
+ */
458
+ handleMessage(windowId: string, message: NappletMessage, send: (msg: NappletMessage) => void): void;
459
+ /**
460
+ * Called when a napplet window is destroyed. Services should clean up
461
+ * any state associated with the window.
462
+ *
463
+ * @param windowId - The destroyed napplet's window identifier
464
+ */
465
+ onWindowDestroyed?(windowId: string): void;
466
+ }
467
+ /**
468
+ * Registry of services available to napplets.
469
+ * Each key is a service name (e.g., 'audio', 'notifications').
470
+ * Napplets discover available services via kind 29010 service discovery events.
471
+ *
472
+ * @example
473
+ * ```ts
474
+ * const services: ServiceRegistry = {
475
+ * audio: audioHandler,
476
+ * notifications: notificationHandler,
477
+ * };
478
+ * ```
479
+ */
480
+ type ServiceRegistry = Record<string, ServiceHandler>;
481
+ /**
482
+ * Optional runtime configuration overrides. When provided via
483
+ * RuntimeAdapter.getConfigOverrides(), the runtime reads these
484
+ * instead of the module-level defaults. All fields are optional —
485
+ * unset fields use the built-in defaults.
486
+ *
487
+ * Intended for demo/debug use only.
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * const overrides: RuntimeConfigOverrides = {
492
+ * replayWindowSeconds: 60,
493
+ * ringBufferSize: 500,
494
+ * };
495
+ * ```
496
+ */
497
+ interface RuntimeConfigOverrides {
498
+ /** Override REPLAY_WINDOW_SECONDS (default: 30). */
499
+ replayWindowSeconds?: number;
500
+ /** Override RING_BUFFER_SIZE (default: 100). */
501
+ ringBufferSize?: number;
502
+ }
503
+ /**
504
+ * All adapters that the runtime requires from the host environment.
505
+ *
506
+ * This is the primary integration point. A browser shell implements these
507
+ * by wrapping postMessage, localStorage, and relay pool libraries.
508
+ * A CLI or server shell could implement them with IPC channels, file
509
+ * storage, and direct WebSocket connections.
510
+ *
511
+ * @example
512
+ * ```ts
513
+ * import { createRuntime, type RuntimeAdapter } from '@kehto/runtime';
514
+ *
515
+ * const hooks: RuntimeAdapter = {
516
+ * sendToNapplet: (wid, msg) => iframeWindows.get(wid)?.postMessage(msg, '*'),
517
+ * relayPool: myRelayPoolAdapter,
518
+ * cache: myCacheAdapter,
519
+ * auth: myAuthAdapter,
520
+ * config: myConfigAdapter,
521
+ * hotkeys: myHotkeyAdapter,
522
+ * crypto: myCryptoAdapter,
523
+ * aclPersistence: myAclPersistenceAdapter,
524
+ * manifestPersistence: myManifestPersistenceAdapter,
525
+ * statePersistence: myStatePersistenceAdapter,
526
+ * windowManager: myWindowManagerAdapter,
527
+ * relayConfig: myRelayConfigAdapter,
528
+ * };
529
+ *
530
+ * const runtime = createRuntime(hooks);
531
+ * ```
532
+ */
533
+ interface RuntimeAdapter {
534
+ /** Send a NIP-01 message to a napplet by windowId. */
535
+ sendToNapplet: SendToNapplet;
536
+ /**
537
+ * Relay pool operations.
538
+ * Optional when a 'relay' or 'relay-pool' service is registered via
539
+ * RuntimeAdapter.services or runtime.registerService(). If neither adapter
540
+ * nor service are provided, relay functionality is unavailable.
541
+ */
542
+ relayPool?: RelayPoolAdapter;
543
+ /**
544
+ * Local event cache (worker relay).
545
+ * Optional when a 'cache' or 'relay' (coordinated) service is registered.
546
+ * If neither adapter nor service are provided, cache functionality is unavailable.
547
+ */
548
+ cache?: CacheAdapter;
549
+ /**
550
+ * Auth state and signing.
551
+ *
552
+ * NIP-5D path: getUserPubkey() provides the shell user's identity (not napplet's).
553
+ * getSigner() is the primary concern — used for proxied signing operations from napplets.
554
+ */
555
+ auth: AuthAdapter;
556
+ /** Runtime configuration. */
557
+ config: ConfigAdapter;
558
+ /** Hotkey dispatch. */
559
+ hotkeys: HotkeyAdapter;
560
+ /** Crypto operations (signature verification, random UUID). */
561
+ crypto: CryptoAdapter;
562
+ /** ACL persistence (save/load ACL state). */
563
+ aclPersistence: AclPersistence;
564
+ /** Manifest cache persistence. */
565
+ manifestPersistence: ManifestPersistence;
566
+ /** Napplet state storage. */
567
+ statePersistence: StatePersistence;
568
+ /** Window management. */
569
+ windowManager: WindowManagerAdapter;
570
+ /** Relay configuration. */
571
+ relayConfig: RelayConfigAdapter;
572
+ /** DM sending (optional). */
573
+ dm?: DmAdapter;
574
+ /**
575
+ * Shell secret persistence (for deterministic keypair derivation).
576
+ * @deprecated NIP-5D: Shell secrets are no longer needed when using the NIP-5D origin-based
577
+ * identity path. Kept for backward compatibility with legacy AUTH sessions.
578
+ */
579
+ shellSecretPersistence?: ShellSecretPersistence;
580
+ /** Hash verification (optional — if absent, hash verification is skipped). */
581
+ hashVerifier?: HashVerifierAdapter;
582
+ /** GUID persistence for iframe instance tracking (optional — if absent, GUIDs are in-memory only). */
583
+ guidPersistence?: GuidPersistence;
584
+ /**
585
+ * Called when aggregate hash verification fails (computed != declared).
586
+ * Host app should display a user-visible warning.
587
+ */
588
+ onHashMismatch?: (dTag: string, claimed: string, computed: string) => void;
589
+ /** Called on every ACL enforcement check (audit). */
590
+ onAclCheck?: (event: AclCheckEvent) => void;
591
+ /** Called when a pending napp update is set or cleared. */
592
+ onPendingUpdate?: PendingUpdateNotifier;
593
+ /**
594
+ * Called when a napplet's required services are not fully available.
595
+ * Receives a CompatibilityReport with available/missing services.
596
+ * In strict mode, the runtime blocks the napplet from loading.
597
+ * In permissive mode (default), the napplet loads and the host decides UX.
598
+ */
599
+ onCompatibilityIssue?: (report: CompatibilityReport) => void;
600
+ /**
601
+ * When true, missing required services block napplet loading.
602
+ * When false or omitted (default), napplets load with a warning.
603
+ */
604
+ strictMode?: boolean;
605
+ /**
606
+ * Optional service extensions. Shell/host registers service handlers here
607
+ * for static initialization. Services can also be added dynamically via
608
+ * runtime.registerService(). Each key is a service name (e.g., 'audio').
609
+ *
610
+ * @example
611
+ * ```ts
612
+ * const hooks: RuntimeAdapter = {
613
+ * // ... required adapters ...
614
+ * services: {
615
+ * audio: myAudioServiceHandler,
616
+ * },
617
+ * };
618
+ * ```
619
+ */
620
+ services?: ServiceRegistry;
621
+ /**
622
+ * Optional runtime behavior overrides — demo/debug use only.
623
+ * Called lazily on each relevant operation (replay check, buffer push),
624
+ * so changes take effect immediately without runtime recreation.
625
+ */
626
+ getConfigOverrides?(): RuntimeConfigOverrides;
627
+ }
628
+
629
+ /**
630
+ * enforce.ts — Single ACL enforcement gate for the NIP-5D runtime.
631
+ *
632
+ * All NIP-5D NappletMessage envelopes pass through createNubEnforceGate()
633
+ * before any handler acts. resolveCapabilitiesNub() (re-exported from
634
+ * @kehto/acl) maps NUB message types to required capabilities. No NIP-01
635
+ * dispatch path remains — v1.4 Phase 24 DRIFT-02 deleted the legacy
636
+ * capability-resolution function along with its dead kind + topic
637
+ * dispatch table.
638
+ */
639
+
640
+ /**
641
+ * Result of an enforcement check.
642
+ *
643
+ * @param allowed - Whether the capability check passed
644
+ * @param capability - The capability that was checked (human-readable string)
645
+ */
646
+ interface EnforceResult {
647
+ allowed: boolean;
648
+ capability: Capability;
649
+ }
650
+ /**
651
+ * Identity lookup function type — resolves a pubkey to its full identity.
652
+ * Provided by sessionRegistry at runtime.
653
+ */
654
+ type IdentityResolver = (pubkey: string) => {
655
+ dTag: string;
656
+ aggregateHash: string;
657
+ } | undefined;
658
+ /**
659
+ * ACL check function type — performs the actual capability check.
660
+ * Provided by @kehto/acl's check() at runtime, or by the legacy aclStore.check().
661
+ */
662
+ type AclChecker = (pubkey: string, dTag: string, aggregateHash: string, capability: Capability) => boolean;
663
+ /**
664
+ * Enforcement gate configuration.
665
+ *
666
+ * @param checkAcl - The ACL check function (wraps @kehto/acl or legacy aclStore)
667
+ * @param resolveIdentity - Maps pubkey to full identity (dTag, aggregateHash)
668
+ * @param onAclCheck - Optional audit callback. Called on every enforce() check
669
+ * with the identity, capability, and decision.
670
+ */
671
+ interface EnforceConfig {
672
+ checkAcl: AclChecker;
673
+ resolveIdentity: IdentityResolver;
674
+ onAclCheck?: (event: AclCheckEvent) => void;
675
+ }
676
+ /**
677
+ * Create an enforcement gate with the given configuration.
678
+ *
679
+ * Returns a function that checks a single capability for a given pubkey.
680
+ * Every call is logged to the audit callback.
681
+ *
682
+ * @param config - Enforcement configuration with ACL checker, identity resolver, and audit hooks
683
+ * @returns An enforce function that checks capabilities and logs decisions
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * const gate = createEnforceGate({
688
+ * checkAcl: aclStore.check,
689
+ * resolveIdentity: (pk) => nappKeyRegistry.getEntry(pk),
690
+ * onAclCheck: hooks.onAclCheck,
691
+ * });
692
+ * const result = gate('abc123...', 'relay:write');
693
+ * // result.allowed === true | false
694
+ * ```
695
+ */
696
+ declare function createEnforceGate(config: EnforceConfig): (pubkey: string, capability: Capability, message?: unknown[]) => EnforceResult;
697
+ /**
698
+ * Enforcement gate configuration for NIP-5D NUB handlers.
699
+ * Uses windowId for identity resolution instead of pubkey (which is '' in NIP-5D sessions).
700
+ *
701
+ * @param checkAcl - The ACL check function
702
+ * @param resolveIdentityByWindowId - Maps windowId to identity (dTag, aggregateHash)
703
+ * @param onAclCheck - Optional audit callback, called on every enforceNub() check
704
+ */
705
+ interface NubEnforceConfig {
706
+ checkAcl: AclChecker;
707
+ resolveIdentityByWindowId: (windowId: string) => {
708
+ dTag: string;
709
+ aggregateHash: string;
710
+ } | undefined;
711
+ onAclCheck?: (event: AclCheckEvent) => void;
712
+ }
713
+ /**
714
+ * Create an enforcement gate for NIP-5D NUB message handlers.
715
+ *
716
+ * Unlike createEnforceGate (which resolves identity by pubkey), this factory
717
+ * resolves identity by windowId — necessary for NIP-5D sessions where pubkey is ''.
718
+ *
719
+ * @param config - NUB enforcement configuration
720
+ * @returns An enforceNub function that resolves identity by windowId
721
+ *
722
+ * @example
723
+ * ```ts
724
+ * const gate = createNubEnforceGate({
725
+ * checkAcl: aclStore.check,
726
+ * resolveIdentityByWindowId: (wid) => sessionRegistry.getEntryByWindowId(wid),
727
+ * onAclCheck: hooks.onAclCheck,
728
+ * });
729
+ * const result = gate('win-1', 'relay:write', { type: 'relay.publish' });
730
+ * // result.allowed === true | false
731
+ * ```
732
+ */
733
+ declare function createNubEnforceGate(config: NubEnforceConfig): (windowId: string, capability: Capability, message?: NubMessage) => EnforceResult;
734
+ /**
735
+ * Format a denial reason string with the standard 'denied:' prefix.
736
+ *
737
+ * @param capability - The denied capability name
738
+ * @returns Formatted denial string, e.g., 'denied: relay:write'
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * formatDenialReason('relay:write')
743
+ * // => 'denied: relay:write'
744
+ * ```
745
+ */
746
+ declare function formatDenialReason(capability: Capability): string;
747
+
748
+ /**
749
+ * SessionRegistry — windowId to verified napplet pubkey bidirectional mapping.
750
+ *
751
+ * After a successful AUTH handshake, the runtime registers the napplet's
752
+ * verified pubkey here. Both mappings are kept in sync.
753
+ *
754
+ * Unlike the shell singleton version, this is a factory that accepts
755
+ * an optional notifier callback instead of using window.dispatchEvent.
756
+ */
757
+
758
+ /**
759
+ * Bidirectional registry mapping windowIds to verified napplet pubkeys.
760
+ * Maintained by the runtime after successful AUTH handshakes.
761
+ *
762
+ * @example
763
+ * ```ts
764
+ * const registry = createSessionRegistry();
765
+ * registry.register('win-1', entry);
766
+ * const pubkey = registry.getPubkey('win-1');
767
+ * ```
768
+ */
769
+ interface SessionRegistry {
770
+ /** Register a napplet entry, mapping windowId to pubkey and vice versa. */
771
+ register(windowId: string, entry: SessionEntry): void;
772
+ /** Unregister a napplet by windowId, removing both mappings. */
773
+ unregister(windowId: string): void;
774
+ /** Get the pubkey associated with a windowId. */
775
+ getPubkey(windowId: string): string | undefined;
776
+ /** Get the full entry for a napplet pubkey. */
777
+ getEntry(pubkey: string): SessionEntry | undefined;
778
+ /** Get the windowId for a napplet pubkey. */
779
+ getWindowId(pubkey: string): string | undefined;
780
+ /** Check if a windowId has a registered napplet. */
781
+ isRegistered(windowId: string): boolean;
782
+ /** Get all registered napplet entries. */
783
+ getAllEntries(): SessionEntry[];
784
+ /** Get the instance GUID for a window. */
785
+ getInstanceId(windowId: string): string | undefined;
786
+ /** Set a pending update for a window (napplet reconnected with different hash). */
787
+ setPendingUpdate(windowId: string, update: PendingUpdate): void;
788
+ /** Get a pending update for a window. */
789
+ getPendingUpdate(windowId: string): PendingUpdate | undefined;
790
+ /** Clear a pending update for a window. */
791
+ clearPendingUpdate(windowId: string): void;
792
+ /**
793
+ * Get the full entry for a napplet by windowId directly.
794
+ * NIP-5D: Required for sessions where pubkey is '' (identity established via originRegistry).
795
+ * Unlike getEntry(pubkey), this works when pubkey is empty.
796
+ */
797
+ getEntryByWindowId(windowId: string): SessionEntry | undefined;
798
+ /** Clear all registrations and pending updates. */
799
+ clear(): void;
800
+ }
801
+ /** @deprecated Use SessionRegistry. Will be removed in v0.9.0. */
802
+ type NappKeyRegistry = SessionRegistry;
803
+ /**
804
+ * Create a new SessionRegistry instance.
805
+ *
806
+ * @param notifier - Optional callback invoked when pending updates change
807
+ * @returns A SessionRegistry instance
808
+ *
809
+ * @example
810
+ * ```ts
811
+ * const registry = createSessionRegistry((windowId) => {
812
+ * console.log('Pending update changed for', windowId);
813
+ * });
814
+ * ```
815
+ */
816
+ declare function createSessionRegistry(notifier?: PendingUpdateNotifier): SessionRegistry;
817
+ /** @deprecated Use createSessionRegistry. Will be removed in v0.9.0. */
818
+ declare const createNappKeyRegistry: typeof createSessionRegistry;
819
+
820
+ /**
821
+ * acl-state.ts — ACL state container with persistence hooks.
822
+ *
823
+ * Wraps @kehto/acl's pure functions with persistence via
824
+ * AclPersistence. No localStorage or DOM references.
825
+ */
826
+
827
+ /**
828
+ * ACL state container — wraps @kehto/acl's pure functions with
829
+ * persistence and a convenient imperative API.
830
+ *
831
+ * @example
832
+ * ```ts
833
+ * const aclState = createAclState(persistence);
834
+ * aclState.load();
835
+ * const allowed = aclState.check(pubkey, dTag, hash, 'relay:read');
836
+ * ```
837
+ */
838
+ interface AclStateContainer {
839
+ check(pubkey: string, dTag: string, aggregateHash: string, capability: Capability): boolean;
840
+ grant(pubkey: string, dTag: string, aggregateHash: string, capability: Capability): void;
841
+ revoke(pubkey: string, dTag: string, aggregateHash: string, capability: Capability): void;
842
+ block(pubkey: string, dTag: string, aggregateHash: string): void;
843
+ unblock(pubkey: string, dTag: string, aggregateHash: string): void;
844
+ isBlocked(pubkey: string, dTag: string, aggregateHash: string): boolean;
845
+ getEntry(pubkey: string, dTag: string, aggregateHash: string): AclEntryExternal | undefined;
846
+ getAllEntries(): AclEntryExternal[];
847
+ getStateQuota(pubkey: string, dTag: string, aggregateHash: string): number;
848
+ persist(): void;
849
+ load(): void;
850
+ clear(): void;
851
+ }
852
+ /**
853
+ * Create an ACL state container backed by @kehto/acl and persisted
854
+ * via the given persistence hooks.
855
+ *
856
+ * @param persistence - Storage backend for ACL state
857
+ * @param defaultPolicy - Default ACL policy for unknown identities
858
+ * @returns An AclStateContainer instance
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * const aclState = createAclState(persistence, 'permissive');
863
+ * aclState.load();
864
+ * ```
865
+ */
866
+ declare function createAclState(persistence: AclPersistence, defaultPolicy?: 'permissive' | 'restrictive'): AclStateContainer;
867
+
868
+ /**
869
+ * manifest-cache.ts — Manifest cache with persistence hooks.
870
+ *
871
+ * Caches NIP-5A manifest data (aggregate hashes) per napplet identity.
872
+ * Delegates storage to ManifestPersistence — no localStorage.
873
+ */
874
+
875
+ /**
876
+ * Cache for verified napplet manifest entries.
877
+ * Used to detect napplet updates (aggregateHash changes) across sessions.
878
+ *
879
+ * @example
880
+ * ```ts
881
+ * const cache = createManifestCache(persistence);
882
+ * cache.load();
883
+ * cache.set({ pubkey: 'abc...', dTag: 'chat', aggregateHash: 'dead', verifiedAt: Date.now() });
884
+ * ```
885
+ */
886
+ interface ManifestCache {
887
+ /** Get a cached manifest entry by pubkey and dTag. */
888
+ get(pubkey: string, dTag: string): ManifestCacheEntry | undefined;
889
+ /** Set (upsert) a manifest cache entry and persist. */
890
+ set(entry: ManifestCacheEntry): void;
891
+ /** Check if a specific hash is cached for a pubkey/dTag combination. */
892
+ has(pubkey: string, dTag: string, hash: string): boolean;
893
+ /** Get the requires list for a cached manifest, or empty array if not found. */
894
+ getRequires(pubkey: string, dTag: string): string[];
895
+ /** Remove a cached entry for a pubkey/dTag and persist. */
896
+ remove(pubkey: string, dTag: string): void;
897
+ /** Load the cache from persistence. */
898
+ load(): void;
899
+ /** Persist the cache to storage. */
900
+ persist(): void;
901
+ /** Clear all cached entries. */
902
+ clear(): void;
903
+ /** Get a cached verification result by manifest event ID. */
904
+ getVerification(eventId: string): VerificationCacheEntry | undefined;
905
+ /** Cache a verification result keyed by manifest event ID. */
906
+ setVerification(eventId: string, result: VerificationCacheEntry): void;
907
+ /** Check if a manifest event ID has been verified. */
908
+ hasVerification(eventId: string): boolean;
909
+ /** Clear all verification cache entries. */
910
+ clearVerifications(): void;
911
+ }
912
+ /**
913
+ * Create a manifest cache backed by the given persistence hooks.
914
+ *
915
+ * @param persistence - Storage backend for manifest data
916
+ * @returns A ManifestCache instance
917
+ *
918
+ * @example
919
+ * ```ts
920
+ * import { createManifestCache } from '@kehto/runtime';
921
+ *
922
+ * const cache = createManifestCache(manifestPersistence);
923
+ * cache.load();
924
+ * cache.set({ pubkey: 'abc...', dTag: 'chat', aggregateHash: 'dead', verifiedAt: Date.now() });
925
+ * cache.has('abc...', 'chat', 'dead'); // true
926
+ * ```
927
+ */
928
+ declare function createManifestCache(persistence: ManifestPersistence): ManifestCache;
929
+
930
+ /**
931
+ * replay.ts — Replay detection module.
932
+ *
933
+ * Tracks seen event IDs and validates timestamps to prevent
934
+ * duplicate event processing and replay attacks.
935
+ */
936
+
937
+ /**
938
+ * Replay detection engine. Tracks seen event IDs and validates timestamps.
939
+ *
940
+ * @example
941
+ * ```ts
942
+ * const detector = createReplayDetector();
943
+ * const reason = detector.check(event);
944
+ * if (reason !== null) { // reject event }
945
+ * ```
946
+ */
947
+ interface ReplayDetector {
948
+ /**
949
+ * Check if an event should be rejected as a replay.
950
+ * Returns null if event is valid, or a string reason if it should be rejected.
951
+ */
952
+ check(event: NostrEvent): string | null;
953
+ /** Clear all tracked event IDs. */
954
+ clear(): void;
955
+ }
956
+ /**
957
+ * Create a replay detector that rejects duplicate events and events
958
+ * with timestamps outside the replay window.
959
+ *
960
+ * @param getReplayWindow - Optional getter for a dynamic replay window override.
961
+ * When provided, its return value is used instead of the module-level constant.
962
+ * Called on every check, so changes take effect immediately.
963
+ * @returns A ReplayDetector instance
964
+ *
965
+ * @example
966
+ * ```ts
967
+ * import { createReplayDetector } from '@kehto/runtime';
968
+ *
969
+ * const detector = createReplayDetector();
970
+ * const reason = detector.check(event);
971
+ * if (reason !== null) {
972
+ * // Reject — duplicate, stale, or future-dated
973
+ * }
974
+ * ```
975
+ */
976
+ declare function createReplayDetector(getReplayWindow?: () => number | undefined): ReplayDetector;
977
+
978
+ /**
979
+ * event-buffer.ts — Ring buffer and subscription delivery engine.
980
+ *
981
+ * Buffers events in a fixed-size ring buffer and delivers matching
982
+ * events to subscribed napplets via the abstract sendToNapplet transport.
983
+ */
984
+
985
+ /** Default ring buffer size. */
986
+ declare const RING_BUFFER_SIZE = 100;
987
+ /** Subscription entry — tracks a subscription for a specific napplet window. */
988
+ interface SubscriptionEntry {
989
+ windowId: string;
990
+ filters: NostrFilter[];
991
+ }
992
+ /**
993
+ * Check if an event matches a single NIP-01 filter.
994
+ * Pure function — no side effects.
995
+ *
996
+ * @param event - The event to check
997
+ * @param filter - The filter to match against
998
+ * @returns True if the event matches the filter
999
+ *
1000
+ * @example
1001
+ * ```ts
1002
+ * import { matchesFilter } from '@kehto/runtime';
1003
+ *
1004
+ * matchesFilter(event, { kinds: [1], authors: ['abc'] });
1005
+ * // true if event.kind === 1 and event.pubkey starts with 'abc'
1006
+ * ```
1007
+ */
1008
+ declare function matchesFilter(event: NostrEvent, filter: NostrFilter): boolean;
1009
+ /**
1010
+ * Check if an event matches any filter in a list.
1011
+ * Returns true for empty filter lists.
1012
+ *
1013
+ * @param event - The event to check
1014
+ * @param filters - The filters to match against
1015
+ * @returns True if the event matches any filter (or if filters is empty)
1016
+ *
1017
+ * @example
1018
+ * ```ts
1019
+ * import { matchesAnyFilter } from '@kehto/runtime';
1020
+ *
1021
+ * matchesAnyFilter(event, [{ kinds: [1] }, { kinds: [7] }]);
1022
+ * // true if event is kind 1 or kind 7
1023
+ * ```
1024
+ */
1025
+ declare function matchesAnyFilter(event: NostrEvent, filters: NostrFilter[]): boolean;
1026
+ /** Event buffer and subscription delivery engine. */
1027
+ interface EventBuffer {
1028
+ /** Add an event to the ring buffer and deliver to matching subscriptions. */
1029
+ bufferAndDeliver(event: NostrEvent, senderId: string | null): void;
1030
+ /** Deliver an event to matching subscriptions without buffering. */
1031
+ deliverToSubscriptions(event: NostrEvent, senderId: string | null): void;
1032
+ /** Get the current subscription map (for REQ handler to register subs). */
1033
+ getSubscriptions(): Map<string, SubscriptionEntry>;
1034
+ /** Get buffered events (for REQ replay). */
1035
+ getBufferedEvents(): readonly NostrEvent[];
1036
+ /** Clear the buffer. */
1037
+ clear(): void;
1038
+ }
1039
+ /**
1040
+ * Create an event buffer with subscription delivery.
1041
+ *
1042
+ * @param sendToNapplet - Transport function to send messages to napplets
1043
+ * @param sessionRegistry - Identity registry for looking up napplet pubkeys
1044
+ * @param enforce - Enforcement function for checking relay:read on recipients
1045
+ * @param subscriptions - Shared subscription map (owned by the runtime)
1046
+ * @param getBufferSize - Optional getter for a dynamic buffer size override.
1047
+ * When provided, its return value is used instead of RING_BUFFER_SIZE.
1048
+ * Called on every bufferAndDeliver, so changes take effect immediately.
1049
+ * @returns An EventBuffer instance
1050
+ *
1051
+ * @example
1052
+ * ```ts
1053
+ * import { createEventBuffer } from '@kehto/runtime';
1054
+ *
1055
+ * const buffer = createEventBuffer(sendToNapplet, sessionRegistry, enforce, subscriptions);
1056
+ * buffer.bufferAndDeliver(event, senderWindowId);
1057
+ * ```
1058
+ */
1059
+ declare function createEventBuffer(sendToNapplet: SendToNapplet, sessionRegistry: SessionRegistry, enforce: (pubkey: string, capability: Capability, message?: unknown[]) => EnforceResult, subscriptions: Map<string, SubscriptionEntry>, getBufferSize?: () => number): EventBuffer;
1060
+
1061
+ /**
1062
+ * runtime.ts — The napplet protocol engine factory.
1063
+ *
1064
+ * createRuntime(hooks) creates the complete protocol engine that handles
1065
+ * NIP-5D NUB domain dispatch, ACL enforcement, subscription lifecycle,
1066
+ * signer proxying, and shell command routing.
1067
+ *
1068
+ * No browser APIs. No DOM. No localStorage. No postMessage.
1069
+ * All I/O is delegated to RuntimeAdapter.
1070
+ */
1071
+
1072
+ /**
1073
+ * The napplet protocol engine — handles NIP-5D NUB domain dispatch,
1074
+ * ACL enforcement, subscription lifecycle.
1075
+ *
1076
+ * @example
1077
+ * ```ts
1078
+ * import { createRuntime } from '@kehto/runtime';
1079
+ *
1080
+ * const runtime = createRuntime(hooks);
1081
+ * runtime.handleMessage('window-1', { type: 'relay.req', id: 'sub1', filters: [{ kinds: [1] }] });
1082
+ * ```
1083
+ */
1084
+ interface Runtime {
1085
+ /**
1086
+ * Handle an incoming NIP-5D NappletMessage envelope from a napplet.
1087
+ * The caller is responsible for identifying the source (windowId).
1088
+ * Legacy NIP-01 arrays are silently dropped (clean break — no dual-mode).
1089
+ *
1090
+ * @param windowId - The identifier of the napplet that sent the message
1091
+ * @param msg - The raw message (NappletMessage envelopes are processed; other types dropped)
1092
+ */
1093
+ handleMessage(windowId: string, msg: unknown): void;
1094
+ /**
1095
+ * Inject a shell-originated event into subscription delivery.
1096
+ *
1097
+ * @param topic - The event topic tag value
1098
+ * @param payload - The event content
1099
+ */
1100
+ injectEvent(topic: string, payload: unknown): void;
1101
+ /** Destroy the runtime, persisting state and clearing all internal state. */
1102
+ destroy(): void;
1103
+ /** Register a handler for consent requests on destructive signing kinds. */
1104
+ registerConsentHandler(handler: ConsentHandler): void;
1105
+ /**
1106
+ * Register a service handler dynamically after runtime creation.
1107
+ * If a handler is already registered for this name, it is replaced.
1108
+ *
1109
+ * @param name - Service name (e.g., 'audio', 'notifications')
1110
+ * @param handler - The service handler implementation
1111
+ */
1112
+ registerService(name: string, handler: ServiceHandler): void;
1113
+ /**
1114
+ * Unregister a service handler by name. No-op if the name is not registered.
1115
+ *
1116
+ * @param name - Service name to remove
1117
+ */
1118
+ unregisterService(name: string): void;
1119
+ /**
1120
+ * Clean up all state associated with a napplet window.
1121
+ * Removes subscriptions, pending state, and notifies service handlers.
1122
+ *
1123
+ * @param windowId - The window to clean up
1124
+ */
1125
+ destroyWindow(windowId: string): void;
1126
+ /** Access the identity registry (for shell adapter to read napplet session state). */
1127
+ readonly sessionRegistry: SessionRegistry;
1128
+ /** Access the ACL state container. */
1129
+ readonly aclState: AclStateContainer;
1130
+ /** Access the manifest cache. */
1131
+ readonly manifestCache: ManifestCache;
1132
+ }
1133
+ /**
1134
+ * Create a runtime instance with dependency injection via hooks.
1135
+ *
1136
+ * @param hooks - Host application provides relay pool, auth, config, etc.
1137
+ * @returns A Runtime instance ready to handle napplet messages
1138
+ *
1139
+ * @example
1140
+ * ```ts
1141
+ * const runtime = createRuntime(hooks);
1142
+ * // On incoming message from napplet:
1143
+ * runtime.handleMessage(windowId, { type: 'relay.req', id: 'sub1', filters: [] });
1144
+ * ```
1145
+ */
1146
+ declare function createRuntime(hooks: RuntimeAdapter): Runtime;
1147
+
1148
+ /**
1149
+ * state-handler.ts — Storage NUB request handler using persistence hooks.
1150
+ *
1151
+ * Handles napplet storage operations (get, set, remove, keys) via the
1152
+ * canonical `@napplet/nub-storage` NIP-5D envelope surface. Delegates
1153
+ * storage to StatePersistence. No localStorage, no legacy NIP-01 dispatch.
1154
+ */
1155
+
1156
+ /**
1157
+ * Handle a NIP-5D NUB storage message from a napplet.
1158
+ * Routes to the canonical four `@napplet/nub-storage` actions:
1159
+ * - `storage.get` → `storage.get.result` `{ value: string | null }`
1160
+ * - `storage.set` → `storage.set.result` `{ ok: boolean }` (canonical only checks `error`)
1161
+ * - `storage.remove` → `storage.remove.result` `{ ok: boolean }`
1162
+ * - `storage.keys` → `storage.keys.result` `{ keys: string[] }`
1163
+ *
1164
+ * `storage.clear` is NOT in the canonical `@napplet/nub-storage` union (it was a
1165
+ * kehto unilateral extension); it now produces a `storage.clear.error` envelope.
1166
+ * Internal lifecycle cleanup still uses the `cleanupNappState()` helper below —
1167
+ * it is not napplet-reachable.
1168
+ *
1169
+ * **Deviation note (Phase 15 to decide):** Set/remove results emit both `ok`
1170
+ * (legacy compat) and an `error` field on failure. Canonical `@napplet/nub-storage`
1171
+ * only specifies the optional `error`; napplets check `!result.error` for success.
1172
+ * Emitting `ok` preserves backward compatibility with existing in-tree callers.
1173
+ * Phase 15 (v1.2 release prep) decides whether to drop `ok` pre-release.
1174
+ *
1175
+ * @param windowId - The window identifier of the requesting napplet
1176
+ * @param msg - The NappletMessage containing the storage request
1177
+ * @param sendToNapplet - Transport function to send responses
1178
+ * @param sessionRegistry - Identity registry for looking up napplet session identity
1179
+ * @param aclState - ACL state for quota checks
1180
+ * @param statePersistence - State storage backend
1181
+ *
1182
+ * @example
1183
+ * ```ts
1184
+ * import { handleStorageNub } from '@kehto/runtime';
1185
+ *
1186
+ * handleStorageNub(windowId, { type: 'storage.get', id: 'q1', key: 'draft' },
1187
+ * sendToNapplet, sessionRegistry, aclState, statePersistence);
1188
+ * ```
1189
+ */
1190
+ declare function handleStorageNub(windowId: string, msg: NappletMessage, sendToNapplet: SendToNapplet, sessionRegistry: SessionRegistry, aclState: AclStateContainer, statePersistence: StatePersistence): void;
1191
+ /**
1192
+ * Remove all state entries for a napplet identity.
1193
+ * Clears both new-format and legacy-format keys for completeness.
1194
+ * Used during napplet cleanup when a window is closed.
1195
+ *
1196
+ * @param pubkey - The napplet's pubkey (needed for legacy key cleanup)
1197
+ * @param dTag - The napplet's dTag
1198
+ * @param aggregateHash - The napplet's build hash
1199
+ * @param statePersistence - State storage backend
1200
+ *
1201
+ * @example
1202
+ * ```ts
1203
+ * import { cleanupNappState } from '@kehto/runtime';
1204
+ *
1205
+ * cleanupNappState(pubkey, dTag, aggregateHash, statePersistence);
1206
+ * ```
1207
+ */
1208
+ declare function cleanupNappState(pubkey: string, dTag: string, aggregateHash: string, statePersistence: StatePersistence): void;
1209
+
1210
+ /**
1211
+ * service-dispatch.ts — NIP-5D envelope routing for registered services.
1212
+ *
1213
+ * Routes NappletMessage envelopes to the correct ServiceHandler based on the
1214
+ * message type domain prefix. NUB-domain services (signer.*, relay.*, storage.*)
1215
+ * are routed by the domain part of message.type. IFC-routed services (audio,
1216
+ * notifications) receive ifc.emit messages and are routed by topic prefix.
1217
+ */
1218
+
1219
+ /**
1220
+ * Route a NappletMessage envelope to the matching service handler.
1221
+ *
1222
+ * NUB-domain services are routed by the domain prefix of message.type
1223
+ * (e.g., 'signer.signEvent' -> 'signer' service).
1224
+ *
1225
+ * IFC-routed services receive ifc.emit messages and are routed by the
1226
+ * topic prefix before ':' (e.g., topic 'audio:play' -> 'audio' service).
1227
+ *
1228
+ * Returns true if a service handled the message, false otherwise.
1229
+ *
1230
+ * @param windowId - The napplet window that sent the message
1231
+ * @param message - The NappletMessage envelope to route
1232
+ * @param services - The service registry to look up handlers
1233
+ * @param sendToNapplet - Callback to send messages back to the napplet
1234
+ * @returns true if a service handled the message, false if no matching service
1235
+ *
1236
+ * @example
1237
+ * ```ts
1238
+ * const handled = routeServiceMessage(windowId, msg, services, sendToNapplet);
1239
+ * if (!handled) { // fallback handling }
1240
+ * ```
1241
+ */
1242
+ declare function routeServiceMessage(windowId: string, message: NappletMessage, services: ServiceRegistry, sendToNapplet: SendToNapplet): boolean;
1243
+ /**
1244
+ * Notify all registered service handlers that a napplet window was destroyed.
1245
+ * Calls onWindowDestroyed() on every handler that implements it.
1246
+ * Errors in individual handlers are caught and silently ignored to prevent
1247
+ * one service's cleanup failure from blocking others.
1248
+ *
1249
+ * @param windowId - The destroyed napplet's window identifier
1250
+ * @param services - The service registry containing all handlers
1251
+ *
1252
+ * @example
1253
+ * ```ts
1254
+ * notifyServiceWindowDestroyed('window-1', services);
1255
+ * ```
1256
+ */
1257
+ declare function notifyServiceWindowDestroyed(windowId: string, services: ServiceRegistry): void;
1258
+
1259
+ export { type AclCheckEvent, type AclChecker, type AclEntryExternal, type AclPersistence, type AclStateContainer, type AuthAdapter, type CacheAdapter, type CompatibilityReport, type ConfigAdapter, type ConsentHandler, type ConsentRequest, type CryptoAdapter, type DmAdapter, type EnforceConfig, type EnforceResult, type EventBuffer, type GuidPersistence, type HashVerifierAdapter, type HotkeyAdapter, type IdentityResolver, type ManifestCache, type ManifestCacheEntry, type ManifestPersistence, type NappKeyEntry, type NappKeyRegistry, type NubEnforceConfig, type PendingUpdate, type PendingUpdateNotifier, RING_BUFFER_SIZE, type RelayConfigAdapter, type RelayPoolAdapter, type RelaySubscriptionHandle, type ReplayDetector, type Runtime, type RuntimeAdapter, type RuntimeConfigOverrides, type SendToNapplet, type ServiceDescriptor, type ServiceHandler, type ServiceInfo, type ServiceRegistry, type SessionEntry, type SessionRegistry, type ShellSecretPersistence, type Signer, type StatePersistence, type SubscriptionEntry, type VerificationCacheEntry, type WindowManagerAdapter, cleanupNappState, createAclState, createEnforceGate, createEventBuffer, createManifestCache, createNappKeyRegistry, createNubEnforceGate, createReplayDetector, createRuntime, createSessionRegistry, formatDenialReason, handleStorageNub, matchesAnyFilter, matchesFilter, notifyServiceWindowDestroyed, routeServiceMessage };