@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 +20 -22
- package/dist/index.js +43 -65
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
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.
|
|
19
|
-
`
|
|
20
|
-
`
|
|
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.
|
|
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/
|
|
40
|
+
[`naps/NAP-INC.md`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INC.md)
|
|
41
41
|
on `napplet/naps` master
|
|
42
|
-
`
|
|
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
|
|
56
|
-
|
|
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
|
-
|
|
73
|
-
`
|
|
74
|
-
The protected `window.napplet.intent` binding
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
2224
|
-
if (!value || typeof value !== "object" ||
|
|
2225
|
-
|
|
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
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
|
|
2257
|
-
archetype
|
|
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
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
-
|
|
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: (
|
|
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() {
|