@kehto/shell 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,24 +76,23 @@ the prior opener-only interpretation is obsolete.
76
76
 
77
77
  ### NAP-INTENT binding and delivery
78
78
 
79
- Kehto implements merged [NAP-INTENT at
80
- `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INTENT.md).
81
- The protected `window.napplet.intent` binding accepts structured
82
- `invoke(request)` calls and `open(archetype, payload?, opts?)`. It defaults
83
- `action` to `open`, preserves an optional queryless convention, rejects
84
- unsupported fields and caller-supplied sender data, and resolves only
85
- parent-originated correlated results.
86
-
87
- Successful results include `handled`, `handler`, `windowId`, and `convention`
88
- after target dispatch completes. Target delivery now follows NAP-INTENT at
89
- `25b29ee49e5bff8ebfe031f4b76dce98705c8b7e`: one `intent.deliver` envelope
90
- feeds the buffered `onDelivery` binding described below. Broader invocation and
91
- result changes from that newer spec remain deferred.
92
-
93
- Phase 105 completed released `@napplet/*` package adoption and persistent live
94
- Paja/playground catalogs and target controllers. The Phase 104 Paja simulator
95
- and playground catalog builder are historical exact-contract consumers; preserve
96
- archived planning rather than presenting it as active guidance.
79
+ Kehto follows [NAP-INTENT PR #106](https://github.com/napplet/naps/blob/fc121fc264615482143eda86125863d2e1f741a2/naps/NAP-INTENT.md) at `fc121fc264615482143eda86125863d2e1f741a2`. The protected binding supports
80
+ `window.napplet.intent.invoke('napplet:profile/open?pubkey=abc%2B123')` and
81
+ `invoke(uri, { payload, handler, handlerHint, behavior })`. Query fields decode
82
+ once into text; `+` remains literal. An explicit payload cannot accompany an
83
+ inner query, and a `#naddr` recommendation cannot accompany `handlerHint`.
84
+ Unsupported options and caller-supplied sender data are rejected. Only a
85
+ parent-originated correlated result can settle the call.
86
+
87
+ Successful results identify retained delivery responsibility with `ok`,
88
+ `archetype`, `action`, `convention`, and an opaque `handler`. A target receives
89
+ one parent-attested `intent.deliver`, buffered by `onDelivery` until registration.
90
+ Acceptance is separate from later target completion.
91
+
92
+ Legacy object-form `invoke(request)` and `open(archetype, payload?, opts?)`
93
+ remain bounded compatibility adapters and warn once per iframe with
94
+ `KEHTO_COMPAT_INTENT_OBJECT_INVOKE`. Their removal conditions are tracked in
95
+ [compatibility](https://github.com/kehto/web/blob/main/docs/compatibility.md).
97
96
 
98
97
  ## Quick Start
99
98
 
@@ -256,8 +255,8 @@ MIT
256
255
 
257
256
  ## NIP-5D event compatibility
258
257
 
259
- Current manifests use a direct artifact `x` hash, plain-text `content`, independent
260
- `z`/`i` routing declarations, and required `R` / optional `O` domains. Kehto also
258
+ Current manifests use a direct artifact `x` hash, plain-text `content`, role-matched
259
+ `z`/`i` intent declarations, and required `R` / optional `O` domains. Kehto also
261
260
  accepts legacy aggregate events through an isolated compatibility adapter.
262
261
  Existing `aggregateHash` host/cache/ACL fields carry the verified artifact hash
263
262
  for current events; legacy identities keep their original aggregate. Both paths
@@ -275,5 +274,12 @@ A throwing listener does not prevent delivery to other listeners.
275
274
  When no canonical listener is registered, matching legacy INC listeners can
276
275
  consume the delivery locally. This emits `KEHTO_COMPAT_INTENT_INC` once per
277
276
  iframe; a delivery is never replayed to both APIs. See [compatibility tracking](https://github.com/kehto/web/blob/main/docs/compatibility.md).
278
- This aligns delivery with NAP-INTENT at `25b29ee`; broader invocation/result
279
- changes and the packaged SDK upgrade remain deferred.
277
+ Delivery and URI invocation follow NAP-INTENT PR #106 at
278
+ `fc121fc264615482143eda86125863d2e1f741a2`; the packaged SDK still trails that
279
+ contract, as documented in the conformance policy.
280
+
281
+ ## Paja URL integration
282
+
283
+ The shell normalizer also powers Paja intent links. See the
284
+ [Paja guide](https://kehto.github.io/web/docs/packages/paja.html#nap-intent-links-and-lifecycle)
285
+ for outer URL encoding, review, routing, and verified launcher behavior.
package/dist/index.d.ts CHANGED
@@ -639,6 +639,38 @@ declare function renderNappletNamespacePrelude(options: NappletNamespacePreludeO
639
639
  */
640
640
  declare function injectNappletNamespacePrelude(html: string, options: NappletNamespacePreludeOptions): string;
641
641
 
642
+ /** Canonical request fields produced from a NAP-INTENT convention URI. */
643
+ interface NormalizedIntentUri {
644
+ archetype: string;
645
+ action: string;
646
+ convention: string;
647
+ payload?: unknown;
648
+ handlerHint?: {
649
+ address: string;
650
+ relays?: string[];
651
+ };
652
+ handler?: string;
653
+ behavior?: {
654
+ focus?: boolean;
655
+ reuse?: boolean;
656
+ };
657
+ }
658
+ /**
659
+ * Normalize a NAP-INTENT convention URI.
660
+ *
661
+ * The host and injected NIP-5D binding use the same normalizer factory.
662
+ *
663
+ * @param uri - Complete NAP-INTENT convention URI.
664
+ * @param options - Optional payload, handler, recommendation, and behavior hints.
665
+ * @returns The canonical request fields for `intent.invoke`.
666
+ * @example
667
+ * ```ts
668
+ * normalizeIntentUri('napplet:profile/open?pubkey=abc%2B123');
669
+ * // { archetype: 'profile', action: 'open', convention: 'napplet:profile/open', payload: { pubkey: 'abc+123' } }
670
+ * ```
671
+ */
672
+ declare const normalizeIntentUri: (uri: unknown, options?: unknown) => NormalizedIntentUri;
673
+
642
674
  /** Source-list directives supported by the shell's meta-delivered CSP. */
643
675
  type NappletCspDirective = 'default-src' | 'script-src' | 'script-src-elem' | 'script-src-attr' | 'style-src' | 'style-src-elem' | 'style-src-attr' | 'img-src' | 'font-src' | 'connect-src' | 'worker-src' | 'child-src' | 'frame-src' | 'media-src' | 'object-src' | 'manifest-src' | 'base-uri' | 'form-action';
644
676
  /** Host replacements for individual CSP source lists; omitted directives retain defaults. */
@@ -1528,4 +1560,4 @@ type ResourceInbound = ResourceInfoRequest | ResourceBytesRequest | ResourceByte
1528
1560
  */
1529
1561
  type ResourceOutbound = ResourceInfoResult | ResourceInfoError | ResourceBytesResult | ResourceBytesError | ResourceBytesManyResult | ResourceBytesManyError;
1530
1562
 
1531
- export { type AclCheckEvent, type AclEntry, type AclStore, type AudioManager, type AudioSource, type AuthHooks, type BleHooks, type BrowserDeps, type CapabilityHooks, type CommonHooks, type ConfigHooks, type CryptoHooks, type DmHooks, type HotkeyHooks, type IdentityProxy, type IdentityProxyDeps, type IntentHooks, type KeysProxy, type KeysProxyDeps, type LinkHooks, type ListsHooks, type ManifestCache, type ManifestCacheEntry, type MediaProxy, type MediaProxyDeps, type NappletCspDirective, type NappletCspDirectives, type NappletCspOptions, type NappletNamespacePreludeOptions, type NappletSrcdocOptions, type NotifyProxy, type NotifyProxyDeps, type OriginIdentity, type OriginRegistry, type ProxyOriginRegistry, type RelayConfigHooks, type RelayPoolHooks, type RelayPoolLike, type ResourceBytesError, type ResourceBytesManyError, type ResourceBytesManyItem, type ResourceBytesManyRequest, type ResourceBytesManyResult, type ResourceBytesRequest, type ResourceBytesResult, type ResourceCancelRequest, type ResourceErrorCode, type ResourceInbound, type ResourceInfoError, type ResourceInfoRequest, type ResourceInfoResult, type ResourceOutbound, type ResourceRequestId, type SerialHooks, type SessionRegistry, type ShellAdapter, type ShellBridge, type ShellCapabilities, type ShellEnvironment, type ThemeProxy, type ThemeProxyDeps, type UnroutedMessageInfo, type UploadBackendLike, type UploadHooks, type WebrtcHooks, type WindowManagerHooks, type WorkerRelayHooks, type WorkerRelayLike, adaptHooks, audioManager, buildNappletCsp, buildShellCapabilities, createIdentityProxy, createKeysProxy, createMediaProxy, createNotifyProxy, createShellBridge, createThemeProxy, injectNappletCsp, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, originRegistry, prepareNappletSrcdoc, renderNappletCspMeta, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
1563
+ export { type AclCheckEvent, type AclEntry, type AclStore, type AudioManager, type AudioSource, type AuthHooks, type BleHooks, type BrowserDeps, type CapabilityHooks, type CommonHooks, type ConfigHooks, type CryptoHooks, type DmHooks, type HotkeyHooks, type IdentityProxy, type IdentityProxyDeps, type IntentHooks, type KeysProxy, type KeysProxyDeps, type LinkHooks, type ListsHooks, type ManifestCache, type ManifestCacheEntry, type MediaProxy, type MediaProxyDeps, type NappletCspDirective, type NappletCspDirectives, type NappletCspOptions, type NappletNamespacePreludeOptions, type NappletSrcdocOptions, type NormalizedIntentUri, type NotifyProxy, type NotifyProxyDeps, type OriginIdentity, type OriginRegistry, type ProxyOriginRegistry, type RelayConfigHooks, type RelayPoolHooks, type RelayPoolLike, type ResourceBytesError, type ResourceBytesManyError, type ResourceBytesManyItem, type ResourceBytesManyRequest, type ResourceBytesManyResult, type ResourceBytesRequest, type ResourceBytesResult, type ResourceCancelRequest, type ResourceErrorCode, type ResourceInbound, type ResourceInfoError, type ResourceInfoRequest, type ResourceInfoResult, type ResourceOutbound, type ResourceRequestId, type SerialHooks, type SessionRegistry, type ShellAdapter, type ShellBridge, type ShellCapabilities, type ShellEnvironment, type ThemeProxy, type ThemeProxyDeps, type UnroutedMessageInfo, type UploadBackendLike, type UploadHooks, type WebrtcHooks, type WindowManagerHooks, type WorkerRelayHooks, type WorkerRelayLike, adaptHooks, audioManager, buildNappletCsp, buildShellCapabilities, createIdentityProxy, createKeysProxy, createMediaProxy, createNotifyProxy, createShellBridge, createThemeProxy, injectNappletCsp, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, normalizeIntentUri, originRegistry, prepareNappletSrcdoc, renderNappletCspMeta, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
package/dist/index.js CHANGED
@@ -1246,6 +1246,181 @@ function createShellBridge(hooks) {
1246
1246
  };
1247
1247
  }
1248
1248
 
1249
+ // src/intent-uri.ts
1250
+ function createIntentUriNormalizer() {
1251
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1252
+ const fail = (message) => {
1253
+ throw new TypeError(message);
1254
+ };
1255
+ const decode = (value, label) => {
1256
+ try {
1257
+ return decodeURIComponent(value);
1258
+ } catch {
1259
+ return fail(`Intent URI has malformed percent-encoding in ${label}`);
1260
+ }
1261
+ };
1262
+ const decodeUtf8 = (bytes) => {
1263
+ try {
1264
+ return new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
1265
+ } catch {
1266
+ return fail("Intent URI fragment contains invalid UTF-8");
1267
+ }
1268
+ };
1269
+ const decodeHint = (fragment) => {
1270
+ if (!fragment.startsWith("naddr") || fragment !== fragment.toLowerCase()) {
1271
+ return fail("Intent URI fragment must be a bare lowercase naddr");
1272
+ }
1273
+ const separator = fragment.lastIndexOf("1");
1274
+ if (separator <= 0 || separator + 7 > fragment.length) fail("Intent URI fragment is not a valid naddr");
1275
+ const alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1276
+ const values = [];
1277
+ for (const character of fragment.slice(separator + 1)) {
1278
+ const value = alphabet.indexOf(character);
1279
+ if (value < 0) fail("Intent URI fragment is not a valid naddr");
1280
+ values.push(value);
1281
+ }
1282
+ const polymod = (input) => {
1283
+ let checksum = 1;
1284
+ for (const value of input) {
1285
+ const top = checksum >>> 25;
1286
+ checksum = (checksum & 33554431) << 5 ^ value;
1287
+ if (top & 1) checksum ^= 996825010;
1288
+ if (top & 2) checksum ^= 642813549;
1289
+ if (top & 4) checksum ^= 513874426;
1290
+ if (top & 8) checksum ^= 1027748829;
1291
+ if (top & 16) checksum ^= 705979059;
1292
+ }
1293
+ return checksum;
1294
+ };
1295
+ const hrp = fragment.slice(0, separator);
1296
+ const hrpValues = [...hrp].map((character) => character.charCodeAt(0) >>> 5).concat([0], [...hrp].map((character) => character.charCodeAt(0) & 31));
1297
+ if (hrp !== "naddr" || polymod(hrpValues.concat(values)) !== 1) fail("Intent URI fragment is not a valid naddr");
1298
+ const words = values.slice(0, -6);
1299
+ const bytes = [];
1300
+ let accumulator = 0;
1301
+ let bits = 0;
1302
+ for (const word of words) {
1303
+ accumulator = accumulator << 5 | word;
1304
+ bits += 5;
1305
+ while (bits >= 8) {
1306
+ bits -= 8;
1307
+ bytes.push(accumulator >>> bits & 255);
1308
+ }
1309
+ }
1310
+ if (bits >= 5 || (accumulator << 8 - bits & 255) !== 0) fail("Intent URI fragment is not a valid naddr");
1311
+ let index = 0;
1312
+ let identifier;
1313
+ let pubkey;
1314
+ let kind;
1315
+ const relays = [];
1316
+ while (index < bytes.length) {
1317
+ if (index + 2 > bytes.length) fail("Intent URI fragment is not a valid naddr");
1318
+ const type = bytes[index++];
1319
+ const length = bytes[index++];
1320
+ if (index + length > bytes.length) fail("Intent URI fragment is not a valid naddr");
1321
+ const value = bytes.slice(index, index + length);
1322
+ index += length;
1323
+ if (type === 0) {
1324
+ if (identifier !== void 0) fail("Intent URI fragment repeats its identifier");
1325
+ identifier = decodeUtf8(value);
1326
+ } else if (type === 1) {
1327
+ const relay = decodeUtf8(value);
1328
+ if (!relay) fail("Intent URI fragment has an empty relay hint");
1329
+ relays.push(relay);
1330
+ } else if (type === 2) {
1331
+ if (pubkey !== void 0 || value.length !== 32) fail("Intent URI fragment has an invalid pubkey");
1332
+ pubkey = value.map((part) => part.toString(16).padStart(2, "0")).join("");
1333
+ } else if (type === 3) {
1334
+ if (kind !== void 0 || value.length !== 4) fail("Intent URI fragment has an invalid kind");
1335
+ kind = (value[0] << 24 >>> 0 | value[1] << 16 | value[2] << 8 | value[3]) >>> 0;
1336
+ } else {
1337
+ fail("Intent URI fragment has unsupported naddr data");
1338
+ }
1339
+ }
1340
+ if (!identifier || !pubkey || kind !== 35129) fail("Intent URI fragment must recommend a named 35129 napplet");
1341
+ return { address: `${kind}:${pubkey}:${identifier}`, ...relays.length > 0 ? { relays } : {} };
1342
+ };
1343
+ const normalizeHint = (value) => {
1344
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("Intent handlerHint must be an object");
1345
+ const hint = value;
1346
+ if (Object.keys(hint).some((key) => key !== "address" && key !== "relays")) fail("Intent handlerHint contains unsupported fields");
1347
+ const address = hint.address;
1348
+ if (typeof address !== "string" || !/^35129:[0-9a-f]{64}:.+$/u.test(address)) {
1349
+ fail("Intent handlerHint address must be a named 35129 coordinate");
1350
+ }
1351
+ const relays = hint.relays;
1352
+ if (relays !== void 0 && (!Array.isArray(relays) || relays.some((relay) => typeof relay !== "string" || relay.length === 0))) {
1353
+ fail("Intent handlerHint relays must be non-empty text");
1354
+ }
1355
+ return { address, ...relays === void 0 ? {} : { relays: [...relays] } };
1356
+ };
1357
+ return function normalizeIntentUri2(uri, options) {
1358
+ if (typeof uri !== "string") throw new TypeError("Intent URI must be text");
1359
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)(?:\?([^#]*))?(?:#(.*))?$/u.exec(uri);
1360
+ if (match === null) return fail("Intent URI must be napplet:<archetype>/<action>");
1361
+ const archetype = match[1];
1362
+ const action = match[2];
1363
+ if (!/^[a-z0-9][a-z0-9-]*$/u.test(archetype) || !/^[a-z0-9][a-z0-9-]*$/u.test(action)) {
1364
+ fail("Intent URI archetype and action must be lowercase slugs");
1365
+ }
1366
+ const query = match[3];
1367
+ const fragment = match[4];
1368
+ const supplied = options === void 0 ? {} : options;
1369
+ if (!supplied || typeof supplied !== "object" || Array.isArray(supplied)) fail("Intent options must be an object");
1370
+ const parsedOptions = supplied;
1371
+ if (Object.keys(parsedOptions).some((key) => key !== "payload" && key !== "handler" && key !== "handlerHint" && key !== "behavior")) {
1372
+ fail("Intent options contain unsupported fields");
1373
+ }
1374
+ const handler = parsedOptions.handler;
1375
+ if (handler !== void 0 && (typeof handler !== "string" || handler.length === 0)) {
1376
+ fail("Intent handler must be non-empty text");
1377
+ }
1378
+ let behavior;
1379
+ if (parsedOptions.behavior !== void 0) {
1380
+ if (!parsedOptions.behavior || typeof parsedOptions.behavior !== "object" || Array.isArray(parsedOptions.behavior)) fail("Intent behavior must be an object");
1381
+ const value = parsedOptions.behavior;
1382
+ if (Object.keys(value).some((key) => key !== "focus" && key !== "reuse") || hasOwn(value, "focus") && typeof value.focus !== "boolean" || hasOwn(value, "reuse") && typeof value.reuse !== "boolean") fail("Intent behavior contains unsupported fields");
1383
+ behavior = {
1384
+ ...hasOwn(value, "focus") ? { focus: value.focus } : {},
1385
+ ...hasOwn(value, "reuse") ? { reuse: value.reuse } : {}
1386
+ };
1387
+ }
1388
+ let payload;
1389
+ if (query !== void 0) {
1390
+ if (hasOwn(parsedOptions, "payload")) fail("Intent URI query cannot accompany options.payload");
1391
+ if (query.length > 0) {
1392
+ const fields = /* @__PURE__ */ Object.create(null);
1393
+ for (const field of query.split("&")) {
1394
+ const equal = field.indexOf("=");
1395
+ if (equal <= 0 || field.indexOf("=", equal + 1) >= 0) fail("Intent URI query fields must be name=value");
1396
+ const name = decode(field.slice(0, equal), "a query name");
1397
+ const value = decode(field.slice(equal + 1), "a query value");
1398
+ if (!name || hasOwn(fields, name)) fail("Intent URI query names must be unique after decoding");
1399
+ Object.defineProperty(fields, name, { value, enumerable: true, writable: true, configurable: true });
1400
+ }
1401
+ payload = fields;
1402
+ }
1403
+ } else if (hasOwn(parsedOptions, "payload")) {
1404
+ payload = parsedOptions.payload;
1405
+ }
1406
+ if (fragment !== void 0) {
1407
+ if (!fragment) fail("Intent URI fragment must be a bare naddr");
1408
+ if (parsedOptions.handlerHint !== void 0) fail("Intent URI fragment cannot accompany options.handlerHint");
1409
+ }
1410
+ const handlerHint = fragment === void 0 ? parsedOptions.handlerHint === void 0 ? void 0 : normalizeHint(parsedOptions.handlerHint) : decodeHint(fragment);
1411
+ return {
1412
+ archetype,
1413
+ action,
1414
+ convention: `napplet:${archetype}/${action}`,
1415
+ ...payload === void 0 ? {} : { payload },
1416
+ ...handlerHint === void 0 ? {} : { handlerHint },
1417
+ ...handler === void 0 ? {} : { handler },
1418
+ ...behavior === void 0 ? {} : { behavior }
1419
+ };
1420
+ };
1421
+ }
1422
+ var normalizeIntentUri = createIntentUriNormalizer();
1423
+
1249
1424
  // src/napplet-namespace.ts
1250
1425
  function uniqueBareDomains(domains) {
1251
1426
  const seen = /* @__PURE__ */ new Set();
@@ -1265,7 +1440,7 @@ function scriptJson(value) {
1265
1440
  }
1266
1441
  function renderNappletNamespacePrelude(options) {
1267
1442
  const domains = uniqueBareDomains(["shell", ...options.domains]);
1268
- return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)});</script>`;
1443
+ return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)}, (${createIntentUriNormalizer.toString()})());</script>`;
1269
1444
  }
1270
1445
  function injectNappletNamespacePrelude(html, options) {
1271
1446
  const prelude = renderNappletNamespacePrelude(options);
@@ -1281,7 +1456,7 @@ function injectNappletNamespacePrelude(html, options) {
1281
1456
  }
1282
1457
  return `${prelude}${html}`;
1283
1458
  }
1284
- function nappletNamespacePrelude(domains) {
1459
+ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1285
1460
  const target = window;
1286
1461
  const allowed = new Set(domains);
1287
1462
  const requestTimeoutMs = 3e4;
@@ -2324,7 +2499,14 @@ function nappletNamespacePrelude(domains) {
2324
2499
  }
2325
2500
  function makeIntent() {
2326
2501
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
2327
- const normalizeRequest = (value) => {
2502
+ let warnedLegacyObject = false;
2503
+ const warnLegacyObject = () => {
2504
+ if (warnedLegacyObject) return;
2505
+ warnedLegacyObject = true;
2506
+ console.warn("[KEHTO_COMPAT_INTENT_OBJECT_INVOKE] Object-form intent.invoke/open is deprecated; use intent.invoke(uri, options) or intent.open(uri, options). Warns once per napplet. See docs/compatibility.md.");
2507
+ };
2508
+ const normalizeLegacyRequest = (value) => {
2509
+ warnLegacyObject();
2328
2510
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2329
2511
  throw new TypeError("Intent request must be an object");
2330
2512
  }
@@ -2332,7 +2514,7 @@ function nappletNamespacePrelude(domains) {
2332
2514
  if (hasOwn(supplied, "sender")) {
2333
2515
  throw new TypeError("Intent callers cannot supply sender");
2334
2516
  }
2335
- const allowed2 = ["archetype", "action", "convention", "payload", "handler", "behavior"];
2517
+ const allowed2 = ["archetype", "action", "convention", "payload", "handler", "handlerHint", "behavior"];
2336
2518
  if (Object.keys(supplied).some((field) => !allowed2.includes(field))) {
2337
2519
  throw new TypeError("Intent request contains unsupported fields");
2338
2520
  }
@@ -2343,51 +2525,63 @@ function nappletNamespacePrelude(domains) {
2343
2525
  if (typeof action !== "string" || action.length === 0) {
2344
2526
  throw new TypeError("Intent action must be a non-empty string");
2345
2527
  }
2346
- if (supplied.convention !== void 0 && (typeof supplied.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(supplied.convention))) {
2347
- throw new TypeError("Intent convention must be queryless");
2528
+ const uri = supplied.convention === void 0 ? `napplet:${supplied.archetype}/${action}` : supplied.convention;
2529
+ if (typeof uri !== "string") throw new TypeError("Intent convention must be text");
2530
+ if (uri.includes("?") || uri.includes("#")) throw new TypeError("Legacy intent convention must be queryless");
2531
+ return normalizeIntentUri2(uri, {
2532
+ ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2533
+ ...hasOwn(supplied, "handler") ? { handler: supplied.handler } : {},
2534
+ ...hasOwn(supplied, "handlerHint") ? { handlerHint: supplied.handlerHint } : {},
2535
+ ...hasOwn(supplied, "behavior") ? { behavior: supplied.behavior } : {}
2536
+ });
2537
+ };
2538
+ const requestInvoke = (normalized) => request(
2539
+ {
2540
+ type: "intent.invoke",
2541
+ request: normalized
2542
+ },
2543
+ "intent.invoke.result",
2544
+ (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2545
+ );
2546
+ const invoke = (uri, options) => {
2547
+ const normalized = typeof uri === "string" ? normalizeIntentUri2(uri, options) : normalizeLegacyRequest(uri);
2548
+ return requestInvoke(normalized);
2549
+ };
2550
+ const open = (uri, options, legacyOptions) => {
2551
+ if (typeof uri === "string" && uri.startsWith("napplet:") && legacyOptions === void 0) {
2552
+ const normalized2 = normalizeIntentUri2(uri, options);
2553
+ if (normalized2.action !== "open") throw new TypeError("intent.open requires a /open URI");
2554
+ return requestInvoke(normalized2);
2348
2555
  }
2349
- if (supplied.handler !== void 0 && (typeof supplied.handler !== "string" || supplied.handler.length === 0)) {
2350
- throw new TypeError("Intent handler must be a non-empty string");
2556
+ if (typeof uri !== "string" || !uri) throw new TypeError("Intent archetype must be non-empty text");
2557
+ warnLegacyObject();
2558
+ if (legacyOptions !== void 0 && (!legacyOptions || typeof legacyOptions !== "object" || Array.isArray(legacyOptions))) {
2559
+ throw new TypeError("Intent options must be an object");
2351
2560
  }
2352
- const behaviorValue = supplied?.behavior;
2353
- const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
2354
- if (behaviorValue !== void 0 && (!behavior || Array.isArray(behaviorValue) || Object.keys(behavior).some((field) => field !== "focus" && field !== "newWindow" && field !== "reuse") || hasOwn(behavior, "focus") && typeof behavior.focus !== "boolean" || hasOwn(behavior, "newWindow") && typeof behavior.newWindow !== "boolean" || hasOwn(behavior, "reuse") && typeof behavior.reuse !== "boolean")) {
2355
- throw new TypeError("Intent behavior contains unsupported fields");
2561
+ const legacy = legacyOptions;
2562
+ const convention = legacy?.convention === void 0 ? `napplet:${uri}/open` : legacy.convention;
2563
+ if (typeof convention !== "string" || convention.includes("?") || convention.includes("#")) {
2564
+ throw new TypeError("Legacy intent convention must be queryless");
2356
2565
  }
2357
- const sanitizedBehavior = {
2358
- ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
2359
- ...typeof behavior?.newWindow === "boolean" ? { newWindow: behavior.newWindow } : {},
2360
- ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
2361
- };
2362
- return {
2363
- archetype: supplied.archetype,
2364
- action,
2365
- ...typeof supplied.convention === "string" ? { convention: supplied.convention } : {},
2366
- ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2367
- ...typeof supplied.handler === "string" ? { handler: supplied.handler } : {},
2368
- ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
2369
- };
2370
- };
2371
- const invoke = (value) => {
2372
- const normalized = normalizeRequest(value);
2373
- return request(
2374
- {
2375
- type: "intent.invoke",
2376
- request: normalized
2377
- },
2378
- "intent.invoke.result",
2379
- (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2380
- );
2566
+ const normalized = normalizeIntentUri2(`napplet:${uri}/open`, {
2567
+ ...options === void 0 ? {} : { payload: options },
2568
+ ...legacy === void 0 ? {} : Object.fromEntries(
2569
+ Object.entries(legacy).filter(([key]) => key !== "convention")
2570
+ )
2571
+ });
2572
+ const legacyNormalized = convention === `napplet:${uri}/open` ? normalized : normalizeIntentUri2(convention, {
2573
+ ...options === void 0 ? {} : { payload: options },
2574
+ ...legacy === void 0 ? {} : Object.fromEntries(
2575
+ Object.entries(legacy).filter(([key]) => key !== "convention")
2576
+ )
2577
+ });
2578
+ if (legacyNormalized.action !== "open") throw new TypeError("intent.open requires a /open URI");
2579
+ return requestInvoke(legacyNormalized);
2381
2580
  };
2382
2581
  return {
2383
2582
  onDelivery: intentDelivery.onDelivery,
2384
2583
  invoke,
2385
- open: (archetype, payload, opts) => invoke({
2386
- archetype,
2387
- action: "open",
2388
- ...payload === void 0 ? {} : { payload },
2389
- ...opts
2390
- }),
2584
+ open,
2391
2585
  available: (archetype) => request(
2392
2586
  { type: "intent.available", archetype },
2393
2587
  "intent.available.result",
@@ -2905,6 +3099,7 @@ export {
2905
3099
  injectNappletNamespacePrelude,
2906
3100
  manifestCache,
2907
3101
  nappKeyRegistry,
3102
+ normalizeIntentUri,
2908
3103
  originRegistry,
2909
3104
  prepareNappletSrcdoc,
2910
3105
  renderNappletCspMeta,