@kehto/shell 0.18.0 → 0.19.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
@@ -15,9 +15,9 @@ pnpm add @kehto/shell
15
15
  ## Published Napplet Compatibility
16
16
 
17
17
  `@kehto/shell` publishes against `@napplet/core` and `@napplet/nap`
18
- `>=0.29.0 <0.30.0`. The installed core/nap 0.29.0 contracts are from source
19
- `dd7b3a728eb9c838b7218fcec7bb7bb00e7cc88b` and release
20
- `60889f1c2476e063500c7ab6624af6abe0dbcbe5`.
18
+ `>=0.31.0 <0.32.0`. The installed core/nap 0.31.0 contracts are from source
19
+ `7b675622e13870628ce174833d7b2a33cf32a0ab` and release
20
+ `03ad65b66413e5798536ef48695ffc4c2508f2c3`.
21
21
 
22
22
  ## Overview
23
23
 
@@ -28,7 +28,7 @@ The primary entry point is `createShellBridge()` — it owns the postMessage lis
28
28
  Current draft behaviors this package enforces:
29
29
 
30
30
  - The shell does not inject a host-provided nostr object into napplets — NIP-5D explicitly forbids napplet-visible signing. Napplets call `relay.publish` / `relay.publishEncrypted` and the shell mediates the signing flow internally (NIP-44 default, NIP-04 opt-in for encrypted envelopes).
31
- - `injectNappletNamespacePrelude()` implements the NIP-5D injected-domain bootstrap and mandatory NAP-SHELL shim: hosts prepend it to `srcdoc` outside verified artifact bytes, install the parent-bound `shell.init` receiver, emit one `shell.ready`, and expose callable NAP interfaces before authored scripts run. Optional namespaces are filtered to the bare-domain allowlist; `shell` is always retained. Published core 0.29.0 and shim 0.27.0 omit generic mandatory shell, so Kehto retains this host-owned prelude under NAP-SHELL `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24` until an upstream correction is reviewed.
31
+ - `injectNappletNamespacePrelude()` implements the NIP-5D injected-domain bootstrap and mandatory NAP-SHELL shim: hosts prepend it to `srcdoc` outside verified artifact bytes, install the parent-bound `shell.init` receiver, emit one `shell.ready`, and expose callable NAP interfaces before authored scripts run. Optional namespaces are filtered to the bare-domain allowlist; `shell` is always retained. Published core 0.31.0 and shim 0.29.0 omit generic mandatory shell, so Kehto retains this host-owned prelude under NAP-SHELL `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24` until an upstream correction is reviewed.
32
32
  - `window.napplet.shell.supports(domain)` answers synchronously and locally from the cached first `shell.init` environment. It returns `false` before `shell.init`, for unknown values, and for domains that are not live and granted to that napplet; it never sends a support-query message.
33
33
  - Five optional per-domain proxies — `createIdentityProxy`, `createThemeProxy`, `createKeysProxy`, `createMediaProxy`, `createNotifyProxy` — can be composed between napplet and runtime to intercept request traffic per NAP. They are NOT wired by default. Identity/theme proxy `emit()` compatibility members fail closed; hosts must deliver automatic changes through `ShellBridge.publishIdentityChanged()` / `publishTheme()`, which enforce live session, granted domain, and current ACL.
34
34
  - `keys.forward` is napplet-to-shell only. Active napplets suppress locally-bound keys from `keys.bindings` before forwarding; shell-initiated action triggers use `keys.action`.
@@ -37,9 +37,9 @@ Current draft behaviors this package enforces:
37
37
  ### NAP-INC binding and channel contract
38
38
 
39
39
  The injected INC binding follows merged
40
- [`naps/NAP-INC.md`](https://github.com/napplet/naps/blob/6461e4b37c29dc09a20dff35d9515889c4433874/naps/NAP-INC.md)
40
+ [`naps/NAP-INC.md`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INC.md)
41
41
  on `napplet/naps` master
42
- `6461e4b37c29dc09a20dff35d9515889c4433874`. The specification remains marked
42
+ `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`. The specification remains marked
43
43
  draft, but its merged path is the protocol authority.
44
44
 
45
45
  The released package projection and `window.napplet.inc` binding own
@@ -52,10 +52,8 @@ dispatch, and does not infer payload kinds. Runtime delivery supplies the
52
52
  **runtime-attested dTag**; no caller sender is accepted, topic source exclusion
53
53
  is runtime-owned, and IDs and payloads are opaque.
54
54
 
55
- Merged NAP-INC delivers one `IncEvent` to `on(topic, callback)`. Released
56
- `@napplet/nap@0.29.0` instead declares and implements a
57
- `(payload, NostrEvent)` callback. The protected binding follows the NAP;
58
- package-based consumers must bridge that upstream callback drift.
55
+ Merged NAP-INC and released `@napplet/nap@0.31.0` both deliver one `IncEvent`
56
+ to `on(topic, callback)`.
59
57
 
60
58
  For channels, runtime ACL checks are open-only rather than per-message. The
61
59
  target `inc.channel.opened` before the opener result creates an equivalent target
@@ -69,18 +67,18 @@ the prior opener-only interpretation is obsolete.
69
67
 
70
68
  ### NAP-INTENT binding and delivery
71
69
 
72
- Phase 104 implements the draft [NAP-INTENT PR #91 at
73
- `a718915ddefa2f03a0126579601f59d8bd86f7c4`](https://github.com/napplet/naps/pull/91).
74
- The protected `window.napplet.intent` binding normalizes URI invocation into
75
- exact `archetype`, `action`, queryless `convention`, and optional payload
76
- fields. It rejects fragments, malformed/repeated queries, query plus explicit
77
- payload, mismatched normalized fields, and caller-supplied sender data.
78
-
79
- Only parent-originated result, change, and delivery envelopes settle the
80
- binding. Incoming no-ID `intent.deliver` values are retained until an
81
- `onDelivery` handler is registered. The runtime, not the source, attests the
82
- sender dTag; an accepted result records retained responsibility and does not
83
- expose target window, handled, protocol, retry, or lifecycle state.
70
+ Kehto implements merged [NAP-INTENT at
71
+ `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INTENT.md).
72
+ The protected `window.napplet.intent` binding accepts structured
73
+ `invoke(request)` calls and `open(archetype, payload?, opts?)`. It defaults
74
+ `action` to `open`, preserves an optional queryless convention, rejects
75
+ unsupported fields and caller-supplied sender data, and resolves only
76
+ parent-originated correlated results.
77
+
78
+ Successful results include `handled`, `handler`, `windowId`, and `convention`
79
+ after target dispatch completes. There is no `intent.deliver` or `onDelivery`;
80
+ selected targets consume their convention through the ordinary
81
+ runtime-attested INC binding.
84
82
 
85
83
  Phase 105 completed released `@napplet/*` package adoption and persistent live
86
84
  Paja/playground catalogs and target controllers. The Phase 104 Paja simulator
package/dist/index.js CHANGED
@@ -2217,77 +2217,57 @@ function nappletNamespacePrelude(domains) {
2217
2217
  };
2218
2218
  }
2219
2219
  function makeIntent() {
2220
- const deliveryHandlers = /* @__PURE__ */ new Set();
2221
- const pendingDeliveries = [];
2222
2220
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
2223
- const normalizeDelivery = (value) => {
2224
- if (!value || typeof value !== "object" || typeof value.sender !== "string" || typeof value.archetype !== "string" || typeof value.action !== "string" || typeof value.convention !== "string") {
2225
- return void 0;
2221
+ const normalizeRequest = (value) => {
2222
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2223
+ throw new TypeError("Intent request must be an object");
2226
2224
  }
2227
- const delivery = value;
2228
- return {
2229
- sender: delivery.sender,
2230
- archetype: delivery.archetype,
2231
- action: delivery.action,
2232
- convention: delivery.convention,
2233
- ...hasOwn(delivery, "payload") ? { payload: delivery.payload } : {}
2234
- };
2235
- };
2236
- listen((event) => {
2237
- if (!isParentMessage(event)) return;
2238
- const msg = event.data;
2239
- if (typeof msg !== "object" || msg === null || msg.type !== "intent.deliver") return;
2240
- const delivery = normalizeDelivery(msg.delivery);
2241
- if (!delivery) return;
2242
- if (deliveryHandlers.size === 0) pendingDeliveries.push(delivery);
2243
- else for (const handler of deliveryHandlers) handler(delivery);
2244
- });
2245
- function normalizeIntentUri(uri, explicitPayload, actionMustBe) {
2246
- if (typeof uri !== "string" || !uri.startsWith("napplet:")) {
2247
- throw new TypeError("Intent convention URI must start with napplet:");
2225
+ const supplied = value;
2226
+ if (hasOwn(supplied, "sender")) {
2227
+ throw new TypeError("Intent callers cannot supply sender");
2248
2228
  }
2249
- if (uri.includes("#")) throw new TypeError("Intent convention URI cannot contain fragments");
2250
- const normalized = normalizeConventionUri(uri, explicitPayload);
2251
- const match = /^napplet:([^/?#]+)\/([^/?#]+)$/.exec(normalized.topic);
2252
- if (!match) throw new TypeError("Intent convention URI must be napplet:<archetype>/<intent>");
2253
- if (actionMustBe !== void 0 && match[2] !== actionMustBe) {
2254
- throw new TypeError(`Intent convention URI must use the ${actionMustBe} intent`);
2229
+ const allowed2 = ["archetype", "action", "convention", "payload", "handler", "behavior"];
2230
+ if (Object.keys(supplied).some((field) => !allowed2.includes(field))) {
2231
+ throw new TypeError("Intent request contains unsupported fields");
2255
2232
  }
2256
- return {
2257
- archetype: match[1],
2258
- action: match[2],
2259
- convention: normalized.topic,
2260
- ...normalized.payload === void 0 ? {} : { payload: normalized.payload }
2261
- };
2262
- }
2263
- const invoke = (uri, opts, actionMustBe) => {
2264
- const supplied = opts && typeof opts === "object" ? opts : void 0;
2265
- const explicitPayload = supplied?.payload !== void 0;
2266
- const normalized = normalizeIntentUri(uri, explicitPayload, actionMustBe);
2267
- if (supplied && hasOwn(supplied, "sender")) {
2268
- throw new TypeError("Intent callers cannot supply sender");
2233
+ if (typeof supplied.archetype !== "string" || supplied.archetype.length === 0) {
2234
+ throw new TypeError("Intent request requires an archetype");
2269
2235
  }
2270
- for (const field of ["archetype", "action", "convention"]) {
2271
- if (supplied && hasOwn(supplied, field) && supplied[field] !== normalized[field]) {
2272
- throw new TypeError(`Intent options cannot override URI-derived ${field}`);
2273
- }
2236
+ const action = supplied.action === void 0 ? "open" : supplied.action;
2237
+ if (typeof action !== "string" || action.length === 0) {
2238
+ throw new TypeError("Intent action must be a non-empty string");
2239
+ }
2240
+ if (supplied.convention !== void 0 && (typeof supplied.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(supplied.convention))) {
2241
+ throw new TypeError("Intent convention must be queryless");
2242
+ }
2243
+ if (supplied.handler !== void 0 && (typeof supplied.handler !== "string" || supplied.handler.length === 0)) {
2244
+ throw new TypeError("Intent handler must be a non-empty string");
2274
2245
  }
2275
2246
  const behaviorValue = supplied?.behavior;
2276
2247
  const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
2248
+ 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")) {
2249
+ throw new TypeError("Intent behavior contains unsupported fields");
2250
+ }
2277
2251
  const sanitizedBehavior = {
2278
2252
  ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
2253
+ ...typeof behavior?.newWindow === "boolean" ? { newWindow: behavior.newWindow } : {},
2279
2254
  ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
2280
2255
  };
2281
- const handler = typeof supplied?.handler === "string" ? supplied.handler : void 0;
2256
+ return {
2257
+ archetype: supplied.archetype,
2258
+ action,
2259
+ ...typeof supplied.convention === "string" ? { convention: supplied.convention } : {},
2260
+ ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2261
+ ...typeof supplied.handler === "string" ? { handler: supplied.handler } : {},
2262
+ ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
2263
+ };
2264
+ };
2265
+ const invoke = (value) => {
2266
+ const normalized = normalizeRequest(value);
2282
2267
  return request(
2283
2268
  {
2284
2269
  type: "intent.invoke",
2285
- request: {
2286
- ...normalized,
2287
- ...explicitPayload ? { payload: supplied?.payload } : {},
2288
- ...handler === void 0 ? {} : { handler },
2289
- ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
2290
- }
2270
+ request: normalized
2291
2271
  },
2292
2272
  "intent.invoke.result",
2293
2273
  (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
@@ -2295,7 +2275,12 @@ function nappletNamespacePrelude(domains) {
2295
2275
  };
2296
2276
  return {
2297
2277
  invoke,
2298
- open: (uri, opts) => invoke(uri, opts, "open"),
2278
+ open: (archetype, payload, opts) => invoke({
2279
+ archetype,
2280
+ action: "open",
2281
+ ...payload === void 0 ? {} : { payload },
2282
+ ...opts
2283
+ }),
2299
2284
  available: (archetype) => request(
2300
2285
  { type: "intent.available", archetype },
2301
2286
  "intent.available.result",
@@ -2310,14 +2295,7 @@ function nappletNamespacePrelude(domains) {
2310
2295
  "intent.changed",
2311
2296
  callback,
2312
2297
  (msg) => msg.availability
2313
- ),
2314
- onDelivery: (callback) => {
2315
- if (typeof callback !== "function") throw new TypeError("Intent delivery handler must be a function");
2316
- deliveryHandlers.add(callback);
2317
- const pending = pendingDeliveries.splice(0);
2318
- for (const delivery of pending) callback(delivery);
2319
- return subscriptionHandle(() => deliveryHandlers.delete(callback));
2320
- }
2298
+ )
2321
2299
  };
2322
2300
  }
2323
2301
  function makeWebrtc() {