@kehto/services 0.16.5 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,71 +1,3 @@
1
- // src/audio-service.ts
2
- var AUDIO_SERVICE_VERSION = "1.0.0";
3
- function createAudioService(options) {
4
- const sources = /* @__PURE__ */ new Map();
5
- const onChange = options?.onChange;
6
- function notify2() {
7
- onChange?.(new Map(sources));
8
- }
9
- const descriptor = {
10
- name: "audio",
11
- version: AUDIO_SERVICE_VERSION,
12
- description: "Audio source registry \u2014 tracks active audio sources per napplet window"
13
- };
14
- return {
15
- descriptor,
16
- handleMessage(windowId, message, send) {
17
- if (message.type !== "inc.emit") return;
18
- const incMessage = message;
19
- const topic = typeof incMessage.topic === "string" ? incMessage.topic : void 0;
20
- if (!topic?.startsWith("audio:")) return;
21
- const action = topic.slice(6);
22
- const payload = incMessage.payload && typeof incMessage.payload === "object" ? incMessage.payload : {};
23
- switch (action) {
24
- case "register": {
25
- const nappletClass = typeof payload.nappletClass === "string" ? payload.nappletClass : "";
26
- const title = typeof payload.title === "string" ? payload.title : "";
27
- sources.set(windowId, { windowId, nappletClass, title, muted: false });
28
- notify2();
29
- break;
30
- }
31
- case "unregister": {
32
- if (sources.delete(windowId)) {
33
- notify2();
34
- }
35
- break;
36
- }
37
- case "state-changed": {
38
- const source = sources.get(windowId);
39
- if (!source) return;
40
- if (typeof payload.title === "string") {
41
- source.title = payload.title;
42
- }
43
- notify2();
44
- break;
45
- }
46
- case "mute": {
47
- const targetWindowId = typeof payload.windowId === "string" ? payload.windowId : windowId;
48
- const muted = payload.muted === true;
49
- const source = sources.get(targetWindowId);
50
- if (source) {
51
- source.muted = muted;
52
- notify2();
53
- }
54
- send({ type: "inc.event", topic: "napplet:audio-muted", payload: { muted } });
55
- break;
56
- }
57
- default:
58
- break;
59
- }
60
- },
61
- onWindowDestroyed(windowId) {
62
- if (sources.delete(windowId)) {
63
- notify2();
64
- }
65
- }
66
- };
67
- }
68
-
69
1
  // src/notification-service.ts
70
2
  var NOTIFICATION_SERVICE_VERSION = "1.0.0";
71
3
  var DEFAULT_MAX_PER_WINDOW = 100;
@@ -97,14 +29,11 @@ function enforceLimit(store, list) {
97
29
  list.shift();
98
30
  }
99
31
  }
100
- function findById(store, id) {
101
- for (const [windowId, list] of store.notifications) {
102
- const index = list.findIndex((n) => n.id === id);
103
- if (index !== -1) {
104
- return [windowId, list[index], index];
105
- }
106
- }
107
- return void 0;
32
+ function findById(store, windowId, id) {
33
+ const list = store.notifications.get(windowId);
34
+ const index = list?.findIndex((notification) => notification.id === id) ?? -1;
35
+ if (!list || index === -1) return void 0;
36
+ return [list[index], index];
108
37
  }
109
38
  function createNotification(store, windowId, title, body) {
110
39
  const notification = {
@@ -121,20 +50,20 @@ function createNotification(store, windowId, title, body) {
121
50
  notify(store);
122
51
  return notification;
123
52
  }
124
- function dismissNotification(store, id) {
125
- const found = findById(store, id);
53
+ function dismissNotification(store, windowId, id) {
54
+ const found = findById(store, windowId, id);
126
55
  if (!found) return;
127
- const [foundWindowId, , index] = found;
128
- const list = store.notifications.get(foundWindowId);
56
+ const [, index] = found;
57
+ const list = store.notifications.get(windowId);
129
58
  if (!list) return;
130
59
  list.splice(index, 1);
131
- if (list.length === 0) store.notifications.delete(foundWindowId);
60
+ if (list.length === 0) store.notifications.delete(windowId);
132
61
  notify(store);
133
62
  }
134
- function markNotificationRead(store, id) {
135
- const found = findById(store, id);
63
+ function markNotificationRead(store, windowId, id) {
64
+ const found = findById(store, windowId, id);
136
65
  if (!found) return;
137
- const [, notification] = found;
66
+ const [notification] = found;
138
67
  if (!notification.read) {
139
68
  notification.read = true;
140
69
  notify(store);
@@ -151,12 +80,12 @@ function handleNotifyEnvelope(store, windowId, action, msg, send) {
151
80
  }
152
81
  case "dismiss": {
153
82
  const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
154
- if (notifId) dismissNotification(store, notifId);
83
+ if (notifId) dismissNotification(store, windowId, notifId);
155
84
  break;
156
85
  }
157
86
  case "read": {
158
87
  const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
159
- if (notifId) markNotificationRead(store, notifId);
88
+ if (notifId) markNotificationRead(store, windowId, notifId);
160
89
  break;
161
90
  }
162
91
  case "list": {
@@ -168,34 +97,6 @@ function handleNotifyEnvelope(store, windowId, action, msg, send) {
168
97
  break;
169
98
  }
170
99
  }
171
- function handleIncNotification(store, windowId, action, payload, send) {
172
- switch (action) {
173
- case "create": {
174
- const title = typeof payload.title === "string" ? payload.title : "";
175
- const body = typeof payload.body === "string" ? payload.body : "";
176
- const notification = createNotification(store, windowId, title, body);
177
- send({ type: "inc.event", topic: "notifications:created", payload: { id: notification.id } });
178
- break;
179
- }
180
- case "dismiss": {
181
- const id = typeof payload.id === "string" ? payload.id : "";
182
- if (id) dismissNotification(store, id);
183
- break;
184
- }
185
- case "read": {
186
- const id = typeof payload.id === "string" ? payload.id : "";
187
- if (id) markNotificationRead(store, id);
188
- break;
189
- }
190
- case "list": {
191
- const windowNotifs = store.notifications.get(windowId) ?? [];
192
- send({ type: "inc.event", topic: "notifications:listed", payload: { notifications: windowNotifs } });
193
- break;
194
- }
195
- default:
196
- break;
197
- }
198
- }
199
100
  function createNotificationService(options) {
200
101
  const store = {
201
102
  notifications: /* @__PURE__ */ new Map(),
@@ -203,23 +104,21 @@ function createNotificationService(options) {
203
104
  maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW
204
105
  };
205
106
  const descriptor = {
206
- name: "notifications",
107
+ name: "notify",
207
108
  version: NOTIFICATION_SERVICE_VERSION,
208
109
  description: "Notification state registry \u2014 tracks notifications per napplet window"
209
110
  };
210
111
  return {
211
112
  descriptor,
212
113
  handleMessage(windowId, message, send) {
213
- const msg = message;
214
- if (message.type.startsWith("notify.")) {
215
- handleNotifyEnvelope(store, windowId, message.type.slice(7), msg, send);
216
- return;
217
- }
218
- if (message.type !== "inc.emit") return;
219
- const topic = msg.topic;
220
- if (!topic?.startsWith("notifications:")) return;
221
- const payload = msg.payload ?? {};
222
- handleIncNotification(store, windowId, topic.slice(14), payload, send);
114
+ if (!message.type.startsWith("notify.")) return;
115
+ handleNotifyEnvelope(
116
+ store,
117
+ windowId,
118
+ message.type.slice(7),
119
+ message,
120
+ send
121
+ );
223
122
  },
224
123
  onWindowDestroyed(windowId) {
225
124
  if (store.notifications.delete(windowId)) {
@@ -231,16 +130,16 @@ function createNotificationService(options) {
231
130
 
232
131
  // src/identity-service.ts
233
132
  var IDENTITY_SERVICE_VERSION = "1.0.0";
234
- function sendProviderError(send, result2, fallback, err2) {
133
+ function sendProviderError(send, result2, fallback, _err) {
235
134
  send({
236
135
  ...result2,
237
- error: err2?.message ?? fallback
136
+ error: fallback
238
137
  });
239
138
  }
240
139
  async function getCurrentPubkey(options) {
241
- const currentSigner = options.getSigner();
242
- if (!currentSigner?.getPublicKey) return "";
243
140
  try {
141
+ const currentSigner = options.getSigner();
142
+ if (!currentSigner?.getPublicKey) return "";
244
143
  return await currentSigner.getPublicKey() ?? "";
245
144
  } catch {
246
145
  return "";
@@ -253,46 +152,29 @@ function sendOptionalProviderResult(options, send, fallbackResult, errorFallback
253
152
  }
254
153
  Promise.resolve(getCurrentPubkey(options)).then((pubkey) => buildResult(pubkey)).then((result2) => send(result2)).catch((err2) => sendProviderError(send, fallbackResult, errorFallback, err2));
255
154
  }
256
- function sendIdentityError(send, id, typeBase, error) {
257
- send({ type: `${typeBase}.error`, id, error });
258
- }
259
- function sendSignerError(send, id, typeBase, fallback, err2) {
260
- sendIdentityError(send, id, typeBase, err2?.message ?? fallback);
261
- }
262
155
  function handleGetPublicKey(options, id, send) {
263
- const signer = options.getSigner();
264
- if (!signer) {
265
- const result2 = {
266
- type: "identity.getPublicKey.result",
267
- id,
268
- pubkey: ""
269
- };
270
- send(result2);
271
- return;
272
- }
273
- Promise.resolve(signer.getPublicKey?.()).then((pubkey) => {
156
+ Promise.resolve().then(() => options.getSigner()).then(async (signer) => signer?.getPublicKey ? signer.getPublicKey() : "").catch(() => "").then((pubkey) => {
274
157
  const result2 = {
275
158
  type: "identity.getPublicKey.result",
276
159
  id,
277
160
  pubkey: pubkey ?? ""
278
161
  };
279
162
  send(result2);
280
- }).catch((err2) => sendSignerError(send, id, "identity.getPublicKey", "getPublicKey failed", err2));
163
+ });
281
164
  }
282
165
  function handleGetRelays(options, id, send) {
283
- const signer = options.getSigner();
284
- if (!signer) {
285
- sendIdentityError(send, id, "identity.getRelays", "no signer configured");
286
- return;
287
- }
288
- Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => {
289
- const result2 = {
290
- type: "identity.getRelays.result",
291
- id,
292
- relays
166
+ const fallbackResult = {
167
+ type: "identity.getRelays.result",
168
+ id,
169
+ relays: {}
170
+ };
171
+ Promise.resolve().then(() => options.getSigner()).then(async (signer) => {
172
+ if (!signer?.getRelays) return fallbackResult;
173
+ return {
174
+ ...fallbackResult,
175
+ relays: await signer.getRelays() ?? {}
293
176
  };
294
- send(result2);
295
- }).catch((err2) => sendSignerError(send, id, "identity.getRelays", "getRelays failed", err2));
177
+ }).catch(() => ({ ...fallbackResult, error: "identity.getRelays failed" })).then(send);
296
178
  }
297
179
  function handleReadProvider(options, id, message, send) {
298
180
  switch (message.type) {
@@ -301,11 +183,11 @@ function handleReadProvider(options, id, message, send) {
301
183
  options,
302
184
  send,
303
185
  { type: "identity.getProfile.result", id, profile: null },
304
- "getProfile failed",
186
+ "identity.getProfile failed",
305
187
  options.getProfile ? async (pubkey) => ({
306
188
  type: "identity.getProfile.result",
307
189
  id,
308
- profile: await options.getProfile(pubkey, message)
190
+ profile: await options.getProfile(pubkey, message) ?? null
309
191
  }) : void 0
310
192
  );
311
193
  return true;
@@ -314,11 +196,11 @@ function handleReadProvider(options, id, message, send) {
314
196
  options,
315
197
  send,
316
198
  { type: "identity.getFollows.result", id, pubkeys: [] },
317
- "getFollows failed",
199
+ "identity.getFollows failed",
318
200
  options.getFollows ? async (pubkey) => ({
319
201
  type: "identity.getFollows.result",
320
202
  id,
321
- pubkeys: await options.getFollows(pubkey, message)
203
+ pubkeys: await options.getFollows(pubkey, message) ?? []
322
204
  }) : void 0
323
205
  );
324
206
  return true;
@@ -330,11 +212,11 @@ function handleReadProvider(options, id, message, send) {
330
212
  options,
331
213
  send,
332
214
  { type: "identity.getZaps.result", id, zaps: [] },
333
- "getZaps failed",
215
+ "identity.getZaps failed",
334
216
  options.getZaps ? async (pubkey) => ({
335
217
  type: "identity.getZaps.result",
336
218
  id,
337
- zaps: await options.getZaps(pubkey, message)
219
+ zaps: await options.getZaps(pubkey, message) ?? []
338
220
  }) : void 0
339
221
  );
340
222
  return true;
@@ -343,11 +225,11 @@ function handleReadProvider(options, id, message, send) {
343
225
  options,
344
226
  send,
345
227
  { type: "identity.getMutes.result", id, pubkeys: [] },
346
- "getMutes failed",
228
+ "identity.getMutes failed",
347
229
  options.getMutes ? async (pubkey) => ({
348
230
  type: "identity.getMutes.result",
349
231
  id,
350
- pubkeys: await options.getMutes(pubkey, message)
232
+ pubkeys: await options.getMutes(pubkey, message) ?? []
351
233
  }) : void 0
352
234
  );
353
235
  return true;
@@ -356,11 +238,11 @@ function handleReadProvider(options, id, message, send) {
356
238
  options,
357
239
  send,
358
240
  { type: "identity.getBlocked.result", id, pubkeys: [] },
359
- "getBlocked failed",
241
+ "identity.getBlocked failed",
360
242
  options.getBlocked ? async (pubkey) => ({
361
243
  type: "identity.getBlocked.result",
362
244
  id,
363
- pubkeys: await options.getBlocked(pubkey, message)
245
+ pubkeys: await options.getBlocked(pubkey, message) ?? []
364
246
  }) : void 0
365
247
  );
366
248
  return true;
@@ -369,11 +251,11 @@ function handleReadProvider(options, id, message, send) {
369
251
  options,
370
252
  send,
371
253
  { type: "identity.getBadges.result", id, badges: [] },
372
- "getBadges failed",
254
+ "identity.getBadges failed",
373
255
  options.getBadges ? async (pubkey) => ({
374
256
  type: "identity.getBadges.result",
375
257
  id,
376
- badges: await options.getBadges(pubkey, message)
258
+ badges: await options.getBadges(pubkey, message) ?? []
377
259
  }) : void 0
378
260
  );
379
261
  return true;
@@ -386,11 +268,11 @@ function handleGetList(options, id, message, send) {
386
268
  options,
387
269
  send,
388
270
  { type: "identity.getList.result", id, entries: [] },
389
- "getList failed",
271
+ "identity.getList failed",
390
272
  options.getList ? async (pubkey) => ({
391
273
  type: "identity.getList.result",
392
274
  id,
393
- entries: await options.getList(message.listType, pubkey, message)
275
+ entries: await options.getList(message.listType, pubkey, message) ?? []
394
276
  }) : void 0
395
277
  );
396
278
  }
@@ -404,9 +286,7 @@ function handleIdentityServiceMessage(options, message, send) {
404
286
  handleGetRelays(options, id, send);
405
287
  return;
406
288
  default:
407
- if (!handleReadProvider(options, id, message, send)) {
408
- sendIdentityError(send, id, message.type, `Unknown identity method: ${message.type}`);
409
- }
289
+ handleReadProvider(options, id, message, send);
410
290
  }
411
291
  }
412
292
  function createIdentityService(options) {
@@ -430,6 +310,37 @@ import { createRelayEventResultWithHints } from "@kehto/runtime";
430
310
  var EOSE_FALLBACK_MS = 15e3;
431
311
  function createRelayPoolService(options) {
432
312
  const tracked = /* @__PURE__ */ new Map();
313
+ function publishEvent(relayMessage, resultType, send) {
314
+ const id = typeof relayMessage.id === "string" ? relayMessage.id : "";
315
+ const event = relayMessage.event;
316
+ if (!event || typeof event !== "object") {
317
+ send({ type: resultType, id, ok: false, error: "invalid event" });
318
+ return;
319
+ }
320
+ if (!options.isAvailable()) {
321
+ send({ type: resultType, id, ok: false, error: "no relay pool available" });
322
+ return;
323
+ }
324
+ void (async () => {
325
+ try {
326
+ await options.publish(event);
327
+ send({
328
+ type: resultType,
329
+ id,
330
+ ok: true,
331
+ event,
332
+ eventId: event.id
333
+ });
334
+ } catch (error) {
335
+ send({
336
+ type: resultType,
337
+ id,
338
+ ok: false,
339
+ error: error instanceof Error && error.message ? error.message : "publish failed"
340
+ });
341
+ }
342
+ })();
343
+ }
433
344
  return {
434
345
  descriptor: {
435
346
  name: "relay-pool",
@@ -489,17 +400,11 @@ function createRelayPoolService(options) {
489
400
  return;
490
401
  }
491
402
  if (message.type === "relay.publish") {
492
- const event = relayMessage.event;
493
- if (event && typeof event === "object" && options.isAvailable()) {
494
- options.publish(event);
495
- }
403
+ publishEvent(relayMessage, "relay.publish.result", send);
496
404
  return;
497
405
  }
498
406
  if (message.type === "relay.publishEncrypted") {
499
- const event = relayMessage.event;
500
- if (event && typeof event === "object" && options.isAvailable()) {
501
- options.publish(event);
502
- }
407
+ publishEvent(relayMessage, "relay.publishEncrypted.result", send);
503
408
  return;
504
409
  }
505
410
  },
@@ -651,6 +556,48 @@ function isPlainObject(value) {
651
556
  // src/coordinated-relay.ts
652
557
  import { createRelayEventResult as createRelayEventResult2, createRelayEventResultWithHints as createRelayEventResultWithHints2 } from "@kehto/runtime";
653
558
  var DEFAULT_EOSE_TIMEOUT_MS = 15e3;
559
+ function publishCoordinatedEvent(options, relayMessage, send) {
560
+ const id = typeof relayMessage.id === "string" ? relayMessage.id : "";
561
+ const event = relayMessage.event;
562
+ if (!event || typeof event !== "object") {
563
+ send({ type: "relay.publish.result", id, ok: false, error: "invalid event" });
564
+ return;
565
+ }
566
+ if (!options.relayPool.isAvailable()) {
567
+ send({
568
+ type: "relay.publish.result",
569
+ id,
570
+ ok: false,
571
+ error: "no relay pool available"
572
+ });
573
+ return;
574
+ }
575
+ void (async () => {
576
+ try {
577
+ await options.relayPool.publish(event);
578
+ if (options.cache.isAvailable()) {
579
+ try {
580
+ options.cache.store(event);
581
+ } catch {
582
+ }
583
+ }
584
+ send({
585
+ type: "relay.publish.result",
586
+ id,
587
+ ok: true,
588
+ event,
589
+ eventId: event.id
590
+ });
591
+ } catch (error) {
592
+ send({
593
+ type: "relay.publish.result",
594
+ id,
595
+ ok: false,
596
+ error: error instanceof Error && error.message ? error.message : "publish failed"
597
+ });
598
+ }
599
+ })();
600
+ }
654
601
  function createCoordinatedRelay(options) {
655
602
  const timeoutMs = options.eoseTimeoutMs ?? DEFAULT_EOSE_TIMEOUT_MS;
656
603
  const subs = /* @__PURE__ */ new Map();
@@ -758,17 +705,7 @@ function createCoordinatedRelay(options) {
758
705
  return;
759
706
  }
760
707
  if (message.type === "relay.publish") {
761
- const event = relayMessage.event;
762
- if (!event || typeof event !== "object") return;
763
- if (options.relayPool.isAvailable()) {
764
- options.relayPool.publish(event);
765
- }
766
- if (options.cache.isAvailable()) {
767
- try {
768
- options.cache.store(event);
769
- } catch {
770
- }
771
- }
708
+ publishCoordinatedEvent(options, relayMessage, send);
772
709
  return;
773
710
  }
774
711
  },
@@ -1628,8 +1565,15 @@ var DEFAULT_THEME = {
1628
1565
  // fonts, background, title intentionally undefined — all optional per
1629
1566
  // @napplet/nap/theme Theme interface.
1630
1567
  };
1568
+ function isCompleteTheme(theme) {
1569
+ const colors = theme?.colors;
1570
+ return typeof colors?.background === "string" && typeof colors.text === "string" && typeof colors.primary === "string";
1571
+ }
1572
+ function normalizeTheme(theme) {
1573
+ return isCompleteTheme(theme) ? theme : DEFAULT_THEME;
1574
+ }
1631
1575
  function createThemeService(options = {}) {
1632
- let currentTheme = options.initialTheme ?? DEFAULT_THEME;
1576
+ let currentTheme = normalizeTheme(options.initialTheme);
1633
1577
  const descriptor = {
1634
1578
  name: "theme",
1635
1579
  version: THEME_SERVICE_VERSION,
@@ -1648,19 +1592,15 @@ function createThemeService(options = {}) {
1648
1592
  send(result2);
1649
1593
  return;
1650
1594
  }
1651
- send({
1652
- type: `${message.type}.error`,
1653
- id,
1654
- error: `Unknown theme method: ${message.type}`
1655
- });
1595
+ return;
1656
1596
  },
1657
1597
  // Theme service has no per-window state to clean up.
1658
1598
  onWindowDestroyed(_windowId) {
1659
1599
  }
1660
1600
  };
1661
1601
  function publishTheme(theme) {
1662
- currentTheme = theme;
1663
- const envelope = { type: "theme.changed", theme };
1602
+ currentTheme = normalizeTheme(theme);
1603
+ const envelope = { type: "theme.changed", theme: currentTheme };
1664
1604
  options.onBroadcast?.(envelope);
1665
1605
  return envelope;
1666
1606
  }
@@ -3226,42 +3166,125 @@ function toErrorMessage4(err2) {
3226
3166
  if (typeof err2 === "string") return err2;
3227
3167
  return "intent request failed";
3228
3168
  }
3169
+ function hasOwn(value, key) {
3170
+ return Object.prototype.hasOwnProperty.call(value, key);
3171
+ }
3172
+ function validateIntentRequest(value) {
3173
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3174
+ return { error: "invalid convention" };
3175
+ }
3176
+ const request = value;
3177
+ if (typeof request.archetype !== "string" || request.archetype.length === 0 || typeof request.action !== "string" || request.action.length === 0 || typeof request.convention !== "string" || request.convention.length === 0) {
3178
+ return { error: "invalid convention" };
3179
+ }
3180
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(request.convention);
3181
+ if (!match || match[1] !== request.archetype || match[2] !== request.action) {
3182
+ return { error: "invalid convention" };
3183
+ }
3184
+ const allowedKeys = ["archetype", "action", "convention", "payload", "handler", "behavior"];
3185
+ if (Object.keys(request).some((key) => !allowedKeys.includes(key))) {
3186
+ return { error: "invoke rejected" };
3187
+ }
3188
+ if (hasOwn(request, "handler") && request.handler !== void 0 && (typeof request.handler !== "string" || request.handler.length === 0)) {
3189
+ return { error: "invoke rejected" };
3190
+ }
3191
+ let behavior;
3192
+ if (hasOwn(request, "behavior") && request.behavior !== void 0) {
3193
+ if (typeof request.behavior !== "object" || request.behavior === null || Array.isArray(request.behavior)) {
3194
+ return { error: "invoke rejected" };
3195
+ }
3196
+ const record = request.behavior;
3197
+ if (Object.keys(record).some((key) => key !== "focus" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
3198
+ return { error: "invoke rejected" };
3199
+ }
3200
+ behavior = {
3201
+ ...record.focus === void 0 ? {} : { focus: record.focus },
3202
+ ...record.reuse === void 0 ? {} : { reuse: record.reuse }
3203
+ };
3204
+ }
3205
+ return {
3206
+ request: {
3207
+ archetype: request.archetype,
3208
+ action: request.action,
3209
+ convention: request.convention,
3210
+ ...hasOwn(request, "payload") ? { payload: request.payload } : {},
3211
+ ...typeof request.handler === "string" ? { handler: request.handler } : {},
3212
+ ...behavior === void 0 ? {} : { behavior }
3213
+ }
3214
+ };
3215
+ }
3216
+ function settleResolverCall(call, send, resultType, id, onValue) {
3217
+ let pending;
3218
+ try {
3219
+ pending = Promise.resolve(call());
3220
+ } catch (error) {
3221
+ send({ type: resultType, id, error: toErrorMessage4(error) });
3222
+ return;
3223
+ }
3224
+ pending.then((value) => send(onValue(value))).catch((error) => send({ type: resultType, id, error: toErrorMessage4(error) }));
3225
+ }
3226
+ function rejectInvoke(send, id) {
3227
+ send({
3228
+ type: "intent.invoke.result",
3229
+ id,
3230
+ result: { ok: false, error: "invoke rejected" }
3231
+ });
3232
+ }
3229
3233
  function createIntentService(options) {
3230
3234
  if (!options || typeof options.resolver !== "object" || options.resolver === null) {
3231
3235
  throw new Error("createIntentService: options.resolver is required");
3232
3236
  }
3233
3237
  const { resolver } = options;
3234
- const windows = /* @__PURE__ */ new Map();
3235
- resolver.onChanged?.((availability) => {
3236
- for (const send of windows.values()) {
3237
- send({ type: "intent.changed", availability });
3238
- }
3239
- });
3240
- function settle6(call, send, resultType, id, onValue) {
3241
- let pending;
3242
- try {
3243
- pending = Promise.resolve(call());
3244
- } catch (err2) {
3245
- send({ type: resultType, id, error: toErrorMessage4(err2) });
3246
- return;
3247
- }
3248
- pending.then((value) => send(onValue(value))).catch((err2) => send({ type: resultType, id, error: toErrorMessage4(err2) }));
3249
- }
3238
+ let runtimeContext;
3239
+ let unsubscribeChanged;
3250
3240
  function handleInvoke(windowId, msg, send) {
3251
3241
  const m = msg;
3252
3242
  const id = m.id ?? "";
3253
- const request = m.request;
3254
- if (!request || typeof request !== "object" || typeof request.archetype !== "string" || request.archetype.length === 0) {
3255
- send({ type: "intent.invoke.result", id, error: "invalid request" });
3243
+ const validation = validateIntentRequest(m.request);
3244
+ if ("error" in validation) {
3245
+ send({
3246
+ type: "intent.invoke.result",
3247
+ id,
3248
+ result: { ok: false, error: validation.error }
3249
+ });
3256
3250
  return;
3257
3251
  }
3258
- settle6(
3259
- () => resolver.invoke(request, { windowId }),
3260
- send,
3261
- "intent.invoke.result",
3262
- id,
3263
- (result2) => ({ type: "intent.invoke.result", id, result: result2 })
3264
- );
3252
+ const sender = runtimeContext?.resolveDTag(windowId);
3253
+ if (!sender) {
3254
+ rejectInvoke(send, id);
3255
+ return;
3256
+ }
3257
+ let pending;
3258
+ try {
3259
+ pending = Promise.resolve(resolver.invoke(validation.request, { sender }));
3260
+ } catch {
3261
+ rejectInvoke(send, id);
3262
+ return;
3263
+ }
3264
+ void pending.then(
3265
+ (outcome) => {
3266
+ if (outcome.result.ok && (!("retained" in outcome) || typeof outcome.retained.start !== "function")) {
3267
+ rejectInvoke(send, id);
3268
+ return;
3269
+ }
3270
+ send({
3271
+ type: "intent.invoke.result",
3272
+ id,
3273
+ result: outcome.result
3274
+ });
3275
+ if (!outcome.result.ok || !("retained" in outcome)) return;
3276
+ try {
3277
+ const started = outcome.retained.start();
3278
+ void Promise.resolve(started).catch(() => {
3279
+ });
3280
+ } catch {
3281
+ }
3282
+ },
3283
+ () => {
3284
+ rejectInvoke(send, id);
3285
+ }
3286
+ ).catch(() => {
3287
+ });
3265
3288
  }
3266
3289
  function handleAvailable(msg, send) {
3267
3290
  const m = msg;
@@ -3271,7 +3294,7 @@ function createIntentService(options) {
3271
3294
  return;
3272
3295
  }
3273
3296
  const archetype = m.archetype;
3274
- settle6(
3297
+ settleResolverCall(
3275
3298
  () => resolver.available(archetype),
3276
3299
  send,
3277
3300
  "intent.available.result",
@@ -3282,7 +3305,7 @@ function createIntentService(options) {
3282
3305
  function handleHandlers(msg, send) {
3283
3306
  const m = msg;
3284
3307
  const id = m.id ?? "";
3285
- settle6(
3308
+ settleResolverCall(
3286
3309
  () => resolver.handlers(),
3287
3310
  send,
3288
3311
  "intent.handlers.result",
@@ -3292,8 +3315,31 @@ function createIntentService(options) {
3292
3315
  }
3293
3316
  return {
3294
3317
  descriptor: INTENT_DESCRIPTOR,
3318
+ onRegistered(context) {
3319
+ try {
3320
+ unsubscribeChanged?.();
3321
+ } catch {
3322
+ }
3323
+ runtimeContext = context;
3324
+ unsubscribeChanged = resolver.onChanged?.((availability) => {
3325
+ const current = runtimeContext;
3326
+ if (!current) return;
3327
+ const message = { type: "intent.changed", availability };
3328
+ for (const windowId of current.listWindowIds()) {
3329
+ current.sendToEligibleNapplet(windowId, message);
3330
+ }
3331
+ });
3332
+ },
3333
+ onUnregistered() {
3334
+ const unsubscribe = unsubscribeChanged;
3335
+ unsubscribeChanged = void 0;
3336
+ runtimeContext = void 0;
3337
+ try {
3338
+ unsubscribe?.();
3339
+ } catch {
3340
+ }
3341
+ },
3295
3342
  handleMessage(windowId, message, send) {
3296
- windows.set(windowId, send);
3297
3343
  switch (message.type) {
3298
3344
  case "intent.invoke":
3299
3345
  handleInvoke(windowId, message, send);
@@ -3307,9 +3353,6 @@ function createIntentService(options) {
3307
3353
  default:
3308
3354
  return;
3309
3355
  }
3310
- },
3311
- onWindowDestroyed(windowId) {
3312
- windows.delete(windowId);
3313
3356
  }
3314
3357
  };
3315
3358
  }
@@ -3323,91 +3366,126 @@ function candidatesFor(catalog, archetype, defaultHandler) {
3323
3366
  candidates.push({
3324
3367
  dTag: entry.dTag,
3325
3368
  ...entry.title === void 0 ? {} : { title: entry.title },
3326
- actions: support.actions,
3327
- protocols: support.protocols,
3369
+ actions: [...support.actions],
3370
+ conventions: [...support.conventions],
3371
+ contracts: support.contracts.map((contract) => ({
3372
+ convention: contract.convention,
3373
+ ...contract.eventKinds === void 0 ? {} : { eventKinds: [...contract.eventKinds] }
3374
+ })),
3328
3375
  ...entry.dTag === defaultHandler ? { isDefault: true } : {}
3329
3376
  });
3330
3377
  }
3331
3378
  return candidates;
3332
3379
  }
3333
- function fail(archetype, action, error) {
3334
- return { ok: false, archetype, action, handled: false, error };
3380
+ function reject(error) {
3381
+ return { ok: false, error };
3335
3382
  }
3336
3383
  function createCatalogIntentResolver(options) {
3337
3384
  if (!options || typeof options.loadCatalog !== "function") {
3338
3385
  throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
3339
3386
  }
3340
- if (!options.windows || typeof options.windows.open !== "function") {
3341
- throw new Error("createCatalogIntentResolver: options.windows is required");
3387
+ if (!options.targets || typeof options.targets.retain !== "function") {
3388
+ throw new Error("createCatalogIntentResolver: options.targets is required");
3342
3389
  }
3343
- const { loadCatalog, windows, getDefaultHandler, chooseHandler, defaultProtocol } = options;
3390
+ const {
3391
+ loadCatalog,
3392
+ targets,
3393
+ getDefaultHandler,
3394
+ chooseHandler,
3395
+ authorizeExplicitHandler
3396
+ } = options;
3344
3397
  const listeners = /* @__PURE__ */ new Set();
3345
3398
  async function availabilityFor(archetype) {
3346
3399
  const catalog = await loadCatalog();
3347
- const def = getDefaultHandler?.(archetype);
3348
- const candidates = candidatesFor(catalog, archetype, def);
3400
+ const defaultHandler = getDefaultHandler?.(archetype);
3401
+ const candidates = candidatesFor(catalog, archetype, defaultHandler);
3349
3402
  return {
3350
3403
  archetype,
3351
3404
  available: candidates.length > 0,
3352
3405
  candidates,
3353
- hasDefault: def !== void 0 && candidates.some((c) => c.dTag === def)
3406
+ hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
3354
3407
  };
3355
3408
  }
3356
- async function pickHandler(archetype, candidates, preference, callerWindowId) {
3409
+ async function chooseCompatible(request, candidates, sender) {
3410
+ if (!chooseHandler) {
3411
+ return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
3412
+ }
3413
+ const picked = await chooseHandler(request.archetype, candidates, sender);
3414
+ if (picked === void 0) return { error: "user cancelled" };
3415
+ const candidate = candidates.find((item) => item.dTag === picked);
3416
+ return candidate ? { candidate } : { error: "invoke rejected" };
3417
+ }
3418
+ async function pickHandler(request, compatible, sender) {
3419
+ const preference = request.handler;
3357
3420
  if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
3358
- return preference;
3421
+ const candidate = compatible.find((item) => item.dTag === preference);
3422
+ if (!candidate || !authorizeExplicitHandler) return { error: "invoke rejected" };
3423
+ let authorized = false;
3424
+ try {
3425
+ authorized = await authorizeExplicitHandler(sender, preference, request, candidate);
3426
+ } catch {
3427
+ return { error: "invoke rejected" };
3428
+ }
3429
+ return authorized ? { candidate } : { error: "invoke rejected" };
3359
3430
  }
3360
3431
  if (preference === "choose") {
3361
- const picked = await chooseHandler?.(archetype, candidates, callerWindowId);
3362
- return picked ?? null;
3432
+ return chooseCompatible(request, compatible, sender);
3363
3433
  }
3364
- const def = getDefaultHandler?.(archetype);
3365
- if (def !== void 0 && candidates.some((c) => c.dTag === def)) return def;
3366
- if (candidates.length === 1) return candidates[0].dTag;
3367
- if (chooseHandler) {
3368
- const picked = await chooseHandler(archetype, candidates, callerWindowId);
3369
- return picked ?? null;
3434
+ const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
3435
+ if (preference === "default") {
3436
+ return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
3370
3437
  }
3371
- return candidates[0].dTag;
3438
+ if (defaultCandidate) return { candidate: defaultCandidate };
3439
+ if (compatible.length === 1) return { candidate: compatible[0] };
3440
+ return chooseCompatible(request, compatible, sender);
3372
3441
  }
3373
3442
  async function invoke(request, context) {
3374
- const archetype = request.archetype;
3375
- const action = request.action ?? "open";
3376
3443
  const catalog = await loadCatalog();
3377
- const candidates = candidatesFor(catalog, archetype, getDefaultHandler?.(archetype));
3378
- if (candidates.length === 0) return fail(archetype, action, "no handler");
3379
- const pickedDTag = await pickHandler(archetype, candidates, request.handler, context.windowId);
3380
- if (pickedDTag === null) return fail(archetype, action, "user cancelled");
3381
- const handler = candidates.find((c) => c.dTag === pickedDTag);
3382
- if (!handler) return fail(archetype, action, "no handler");
3383
- if (!handler.actions.includes(action)) return fail(archetype, action, "unsupported action");
3384
- const protocol = request.protocol ?? defaultProtocol?.(archetype) ?? handler.protocols[0];
3385
- if (protocol !== void 0 && handler.protocols.length > 0 && !handler.protocols.includes(protocol)) {
3386
- return fail(archetype, action, "unsupported protocol");
3444
+ const defaultHandler = getDefaultHandler?.(request.archetype);
3445
+ const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
3446
+ if (candidates.length === 0) return { result: reject("no handler") };
3447
+ const compatible = candidates.filter((candidate) => candidate.contracts.some((contract) => contract.convention === request.convention));
3448
+ if (compatible.length === 0) return { result: reject("unsupported convention") };
3449
+ const sender = context.sender;
3450
+ if (typeof sender !== "string" || sender.length === 0) {
3451
+ return { result: reject("invoke rejected") };
3387
3452
  }
3388
- let windowId;
3453
+ const selected = await pickHandler(request, compatible, sender);
3454
+ if ("error" in selected) return { result: reject(selected.error) };
3455
+ const delivery = Object.freeze({
3456
+ sender,
3457
+ archetype: request.archetype,
3458
+ action: request.action,
3459
+ convention: request.convention,
3460
+ ...request.payload === void 0 ? {} : { payload: request.payload }
3461
+ });
3462
+ const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
3463
+ ...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
3464
+ ...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
3465
+ });
3466
+ const params = Object.freeze({
3467
+ handler: selected.candidate.dTag,
3468
+ delivery,
3469
+ ...behavior === void 0 ? {} : { behavior }
3470
+ });
3471
+ let retained;
3389
3472
  try {
3390
- const opened = await windows.open({
3391
- dTag: handler.dTag,
3392
- archetype,
3393
- action,
3394
- ...protocol === void 0 ? {} : { protocol },
3395
- ...request.payload === void 0 ? {} : { payload: request.payload },
3396
- ...request.behavior === void 0 ? {} : { behavior: request.behavior },
3397
- callerWindowId: context.windowId
3398
- });
3399
- windowId = opened.windowId;
3473
+ retained = await targets.retain(params);
3400
3474
  } catch {
3401
- return fail(archetype, action, "invoke failed");
3475
+ return { result: reject("invoke rejected") };
3476
+ }
3477
+ if (!retained || typeof retained.start !== "function") {
3478
+ return { result: reject("invoke rejected") };
3402
3479
  }
3403
3480
  return {
3404
- ok: true,
3405
- archetype,
3406
- action,
3407
- handled: true,
3408
- handler: handler.dTag,
3409
- windowId,
3410
- ...protocol === void 0 ? {} : { protocol }
3481
+ result: {
3482
+ ok: true,
3483
+ archetype: request.archetype,
3484
+ action: request.action,
3485
+ convention: request.convention,
3486
+ handler: selected.candidate.dTag
3487
+ },
3488
+ retained
3411
3489
  };
3412
3490
  }
3413
3491
  async function handlers() {
@@ -3416,7 +3494,7 @@ function createCatalogIntentResolver(options) {
3416
3494
  for (const entry of catalog) {
3417
3495
  for (const slug of Object.keys(entry.archetypes)) archetypes.add(slug);
3418
3496
  }
3419
- return Promise.all([...archetypes].map((a) => availabilityFor(a)));
3497
+ return Promise.all([...archetypes].map((archetype) => availabilityFor(archetype)));
3420
3498
  }
3421
3499
  return {
3422
3500
  invoke,
@@ -3436,14 +3514,35 @@ function createCatalogIntentResolver(options) {
3436
3514
  }
3437
3515
 
3438
3516
  // src/manifest-intent-catalog.ts
3439
- var DEFAULT_ACTIONS = ["open"];
3517
+ function actionFromConvention(slug, convention) {
3518
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
3519
+ throw new TypeError("manifest archetype slug is invalid");
3520
+ }
3521
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
3522
+ if (!match || match[1] !== slug) {
3523
+ throw new TypeError("manifest archetype convention is invalid or mismatched");
3524
+ }
3525
+ return match[2];
3526
+ }
3440
3527
  function manifestToIntentCatalogEntry(manifest) {
3441
3528
  const archetypes = {};
3442
- for (const { slug, nap } of manifest.archetypes) {
3443
- archetypes[slug] = {
3444
- actions: [...DEFAULT_ACTIONS],
3445
- protocols: nap ? [nap] : []
3529
+ for (const { slug, convention, eventKinds } of manifest.archetypes) {
3530
+ const action = actionFromConvention(slug, convention);
3531
+ if (eventKinds?.some((kind) => !Number.isSafeInteger(kind) || kind < 0)) {
3532
+ throw new TypeError("manifest archetype event kinds must be unsigned safe integers");
3533
+ }
3534
+ const support = archetypes[slug] ??= {
3535
+ actions: [],
3536
+ conventions: [],
3537
+ contracts: []
3538
+ };
3539
+ const contract = {
3540
+ convention,
3541
+ ...eventKinds === void 0 || eventKinds.length === 0 ? {} : { eventKinds: [...eventKinds] }
3446
3542
  };
3543
+ support.contracts.push(contract);
3544
+ if (!support.actions.includes(action)) support.actions.push(action);
3545
+ if (!support.conventions.includes(convention)) support.conventions.push(convention);
3447
3546
  }
3448
3547
  return {
3449
3548
  dTag: manifest.dTag,
@@ -4932,7 +5031,6 @@ function createCordnDmAdapter(options) {
4932
5031
  }
4933
5032
  export {
4934
5033
  DmMemoryStore,
4935
- createAudioService,
4936
5034
  createBleService,
4937
5035
  createBrowserMediaBridge,
4938
5036
  createCacheService,