@kehto/shell 0.18.0 → 0.19.1
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 +24 -24
- package/dist/index.js +53 -71
- 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,25 +37,25 @@ 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
|
|
46
46
|
query-to-text-payload
|
|
47
47
|
transposition: it converts a convention URI query before emitting a stable,
|
|
48
|
-
exact
|
|
49
|
-
|
|
48
|
+
exact stable convention topic identity. Subscriptions reject query- or fragment-bearing
|
|
49
|
+
**convention** identities; arbitrary opaque topics remain byte-for-byte exact,
|
|
50
|
+
including `?` and `#`. The binding never creates a normalized query-bearing
|
|
51
|
+
wire/discovery identity,
|
|
50
52
|
does not do prefix/wildcard/query-aware matching or service-over-INC prefix
|
|
51
53
|
dispatch, and does not infer payload kinds. Runtime delivery supplies the
|
|
52
54
|
**runtime-attested dTag**; no caller sender is accepted, topic source exclusion
|
|
53
55
|
is runtime-owned, and IDs and payloads are opaque.
|
|
54
56
|
|
|
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.
|
|
57
|
+
Merged NAP-INC and released `@napplet/nap@0.31.0` both deliver one `IncEvent`
|
|
58
|
+
to `on(topic, callback)`.
|
|
59
59
|
|
|
60
60
|
For channels, runtime ACL checks are open-only rather than per-message. The
|
|
61
61
|
target `inc.channel.opened` before the opener result creates an equivalent target
|
|
@@ -69,18 +69,18 @@ the prior opener-only interpretation is obsolete.
|
|
|
69
69
|
|
|
70
70
|
### NAP-INTENT binding and delivery
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
The protected `window.napplet.intent` binding
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
Kehto implements merged [NAP-INTENT at
|
|
73
|
+
`5ac0490461ca6fec2f0d2e45b4835cf9bc08de24`](https://github.com/napplet/naps/blob/5ac0490461ca6fec2f0d2e45b4835cf9bc08de24/naps/NAP-INTENT.md).
|
|
74
|
+
The protected `window.napplet.intent` binding accepts structured
|
|
75
|
+
`invoke(request)` calls and `open(archetype, payload?, opts?)`. It defaults
|
|
76
|
+
`action` to `open`, preserves an optional queryless convention, rejects
|
|
77
|
+
unsupported fields and caller-supplied sender data, and resolves only
|
|
78
|
+
parent-originated correlated results.
|
|
79
|
+
|
|
80
|
+
Successful results include `handled`, `handler`, `windowId`, and `convention`
|
|
81
|
+
after target dispatch completes. There is no `intent.deliver` or `onDelivery`;
|
|
82
|
+
selected targets consume their convention through the ordinary
|
|
83
|
+
runtime-attested INC binding.
|
|
84
84
|
|
|
85
85
|
Phase 105 completed released `@napplet/*` package adoption and persistent live
|
|
86
86
|
Paja/playground catalogs and target controllers. The Phase 104 Paja simulator
|
package/dist/index.js
CHANGED
|
@@ -1334,9 +1334,14 @@ function nappletNamespacePrelude(domains) {
|
|
|
1334
1334
|
function fire(message) {
|
|
1335
1335
|
post(message);
|
|
1336
1336
|
}
|
|
1337
|
+
function isConventionTopic(topic) {
|
|
1338
|
+
const pathEnd = topic.search(/[?#]/);
|
|
1339
|
+
const path = pathEnd === -1 ? topic : topic.slice(0, pathEnd);
|
|
1340
|
+
return /^napplet:[^/?#]+\/[^/?#]+$/.test(path);
|
|
1341
|
+
}
|
|
1337
1342
|
function normalizeConventionUri(topic, hasExplicitPayload) {
|
|
1338
1343
|
if (typeof topic !== "string") throw new TypeError("INC topic must be text");
|
|
1339
|
-
if (!topic
|
|
1344
|
+
if (!isConventionTopic(topic)) return { topic };
|
|
1340
1345
|
if (topic.includes("#")) throw new TypeError("Convention topics cannot contain fragments");
|
|
1341
1346
|
const queryStart = topic.indexOf("?");
|
|
1342
1347
|
if (queryStart === -1) {
|
|
@@ -1345,7 +1350,7 @@ function nappletNamespacePrelude(domains) {
|
|
|
1345
1350
|
if (hasExplicitPayload) {
|
|
1346
1351
|
throw new TypeError("Convention queries cannot be combined with an explicit payload");
|
|
1347
1352
|
}
|
|
1348
|
-
const
|
|
1353
|
+
const entries = [];
|
|
1349
1354
|
const names = /* @__PURE__ */ new Set();
|
|
1350
1355
|
const query = topic.slice(queryStart + 1);
|
|
1351
1356
|
if (query) {
|
|
@@ -1364,10 +1369,10 @@ function nappletNamespacePrelude(domains) {
|
|
|
1364
1369
|
}
|
|
1365
1370
|
if (names.has(name)) throw new TypeError("Convention query names must be unique");
|
|
1366
1371
|
names.add(name);
|
|
1367
|
-
|
|
1372
|
+
entries.push([name, value]);
|
|
1368
1373
|
}
|
|
1369
1374
|
}
|
|
1370
|
-
return { topic: topic.slice(0, queryStart), payload:
|
|
1375
|
+
return { topic: topic.slice(0, queryStart), payload: Object.fromEntries(entries) };
|
|
1371
1376
|
}
|
|
1372
1377
|
function resultOrError(msg) {
|
|
1373
1378
|
return msg;
|
|
@@ -1526,7 +1531,7 @@ function nappletNamespacePrelude(domains) {
|
|
|
1526
1531
|
const unopenedClosed = /* @__PURE__ */ new Map();
|
|
1527
1532
|
function stableSubscriptionTopic(topic) {
|
|
1528
1533
|
if (typeof topic !== "string") throw new TypeError("INC topic must be text");
|
|
1529
|
-
if (topic.includes("?") || topic.includes("#")) {
|
|
1534
|
+
if (isConventionTopic(topic) && (topic.includes("?") || topic.includes("#"))) {
|
|
1530
1535
|
throw new TypeError("INC subscriptions require a queryless topic identity");
|
|
1531
1536
|
}
|
|
1532
1537
|
return topic;
|
|
@@ -1547,7 +1552,6 @@ function nappletNamespacePrelude(domains) {
|
|
|
1547
1552
|
},
|
|
1548
1553
|
on(handler) {
|
|
1549
1554
|
if (typeof handler !== "function") throw new TypeError("INC channel event handler must be a function");
|
|
1550
|
-
if (state.closed) return subscriptionHandle(() => void 0);
|
|
1551
1555
|
state.eventHandlers.add(handler);
|
|
1552
1556
|
const pending = state.pendingEvents.splice(0);
|
|
1553
1557
|
for (const event of pending) handler(event);
|
|
@@ -2217,77 +2221,57 @@ function nappletNamespacePrelude(domains) {
|
|
|
2217
2221
|
};
|
|
2218
2222
|
}
|
|
2219
2223
|
function makeIntent() {
|
|
2220
|
-
const deliveryHandlers = /* @__PURE__ */ new Set();
|
|
2221
|
-
const pendingDeliveries = [];
|
|
2222
2224
|
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
2223
|
-
const
|
|
2224
|
-
if (!value || typeof value !== "object" ||
|
|
2225
|
-
|
|
2225
|
+
const normalizeRequest = (value) => {
|
|
2226
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2227
|
+
throw new TypeError("Intent request must be an object");
|
|
2226
2228
|
}
|
|
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:");
|
|
2229
|
+
const supplied = value;
|
|
2230
|
+
if (hasOwn(supplied, "sender")) {
|
|
2231
|
+
throw new TypeError("Intent callers cannot supply sender");
|
|
2248
2232
|
}
|
|
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`);
|
|
2233
|
+
const allowed2 = ["archetype", "action", "convention", "payload", "handler", "behavior"];
|
|
2234
|
+
if (Object.keys(supplied).some((field) => !allowed2.includes(field))) {
|
|
2235
|
+
throw new TypeError("Intent request contains unsupported fields");
|
|
2255
2236
|
}
|
|
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");
|
|
2237
|
+
if (typeof supplied.archetype !== "string" || supplied.archetype.length === 0) {
|
|
2238
|
+
throw new TypeError("Intent request requires an archetype");
|
|
2269
2239
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2240
|
+
const action = supplied.action === void 0 ? "open" : supplied.action;
|
|
2241
|
+
if (typeof action !== "string" || action.length === 0) {
|
|
2242
|
+
throw new TypeError("Intent action must be a non-empty string");
|
|
2243
|
+
}
|
|
2244
|
+
if (supplied.convention !== void 0 && (typeof supplied.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(supplied.convention))) {
|
|
2245
|
+
throw new TypeError("Intent convention must be queryless");
|
|
2246
|
+
}
|
|
2247
|
+
if (supplied.handler !== void 0 && (typeof supplied.handler !== "string" || supplied.handler.length === 0)) {
|
|
2248
|
+
throw new TypeError("Intent handler must be a non-empty string");
|
|
2274
2249
|
}
|
|
2275
2250
|
const behaviorValue = supplied?.behavior;
|
|
2276
2251
|
const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
|
|
2252
|
+
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")) {
|
|
2253
|
+
throw new TypeError("Intent behavior contains unsupported fields");
|
|
2254
|
+
}
|
|
2277
2255
|
const sanitizedBehavior = {
|
|
2278
2256
|
...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
|
|
2257
|
+
...typeof behavior?.newWindow === "boolean" ? { newWindow: behavior.newWindow } : {},
|
|
2279
2258
|
...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
|
|
2280
2259
|
};
|
|
2281
|
-
|
|
2260
|
+
return {
|
|
2261
|
+
archetype: supplied.archetype,
|
|
2262
|
+
action,
|
|
2263
|
+
...typeof supplied.convention === "string" ? { convention: supplied.convention } : {},
|
|
2264
|
+
...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
|
|
2265
|
+
...typeof supplied.handler === "string" ? { handler: supplied.handler } : {},
|
|
2266
|
+
...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
|
|
2267
|
+
};
|
|
2268
|
+
};
|
|
2269
|
+
const invoke = (value) => {
|
|
2270
|
+
const normalized = normalizeRequest(value);
|
|
2282
2271
|
return request(
|
|
2283
2272
|
{
|
|
2284
2273
|
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
|
-
}
|
|
2274
|
+
request: normalized
|
|
2291
2275
|
},
|
|
2292
2276
|
"intent.invoke.result",
|
|
2293
2277
|
(msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
|
|
@@ -2295,7 +2279,12 @@ function nappletNamespacePrelude(domains) {
|
|
|
2295
2279
|
};
|
|
2296
2280
|
return {
|
|
2297
2281
|
invoke,
|
|
2298
|
-
open: (
|
|
2282
|
+
open: (archetype, payload, opts) => invoke({
|
|
2283
|
+
archetype,
|
|
2284
|
+
action: "open",
|
|
2285
|
+
...payload === void 0 ? {} : { payload },
|
|
2286
|
+
...opts
|
|
2287
|
+
}),
|
|
2299
2288
|
available: (archetype) => request(
|
|
2300
2289
|
{ type: "intent.available", archetype },
|
|
2301
2290
|
"intent.available.result",
|
|
@@ -2310,14 +2299,7 @@ function nappletNamespacePrelude(domains) {
|
|
|
2310
2299
|
"intent.changed",
|
|
2311
2300
|
callback,
|
|
2312
2301
|
(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
|
-
}
|
|
2302
|
+
)
|
|
2321
2303
|
};
|
|
2322
2304
|
}
|
|
2323
2305
|
function makeWebrtc() {
|