@kehto/shell 0.21.2 → 0.22.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 +71 -5
- package/dist/index.d.ts +77 -1
- package/dist/index.js +194 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Current draft behaviors this package enforces:
|
|
|
34
34
|
when the runtime can identify it. The shell forwards this optional context
|
|
35
35
|
without imposing a consent policy; host runtimes may ignore it or use it to
|
|
36
36
|
resolve their own identity-scoped signer decisions.
|
|
37
|
-
- `
|
|
37
|
+
- `prepareNappletSrcdoc()` inserts validated CSP first, then the NIP-5D bootstrap and mandatory NAP-SHELL shim, outside verified artifact bytes. The shim installs the parent-bound `shell.init` receiver, emits one `shell.ready`, and exposes callable NAP interfaces before authored scripts run. Optional namespaces are filtered to the bare-domain allowlist; `shell` is always retained. Published core 0.32.0 and shim 0.30.0 omit generic mandatory shell, so Kehto retains this host-owned prelude under NAP-SHELL `5ac0490461ca6fec2f0d2e45b4835cf9bc08de24` until an upstream correction is reviewed.
|
|
38
38
|
- The injected resource projection follows draft NAP-RESOURCE `9511232f69313aa7953d110e35d32cc28d506f66`: `bytes(url, { servers? })` carries advisory Blossom locations and `bytesMany(requests)` keeps each `{ url, servers? }` entry intact.
|
|
39
39
|
- `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.
|
|
40
40
|
- 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.
|
|
@@ -85,9 +85,10 @@ unsupported fields and caller-supplied sender data, and resolves only
|
|
|
85
85
|
parent-originated correlated results.
|
|
86
86
|
|
|
87
87
|
Successful results include `handled`, `handler`, `windowId`, and `convention`
|
|
88
|
-
after target dispatch completes.
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
91
92
|
|
|
92
93
|
Phase 105 completed released `@napplet/*` package adoption and persistent live
|
|
93
94
|
Paja/playground catalogs and target controllers. The Phase 104 Paja simulator
|
|
@@ -135,11 +136,52 @@ const bridge = createShellBridge({
|
|
|
135
136
|
```
|
|
136
137
|
|
|
137
138
|
### Shell init
|
|
139
|
+
- `prepareNappletSrcdoc` — recommended verified-document entry point; applies CSP and namespace together
|
|
140
|
+
- `buildNappletCsp`, `renderNappletCspMeta`, `injectNappletCsp` — shared policy construction and CSP-only composition utilities
|
|
138
141
|
- `buildShellCapabilities` — construct the immutable domain-only `ShellCapabilities` payload emitted during the `shell.ready` / `shell.init` handshake
|
|
139
142
|
- `resolveShellEnvironment(hooks, identity)` — host-integrator-only utility that narrows the live environment for a trusted creation-time identity before `shell.init`; it is not installed on `window.napplet` and is not a shim-facing napplet API
|
|
140
|
-
- `injectNappletNamespacePrelude` —
|
|
143
|
+
- `injectNappletNamespacePrelude` — low-level bootstrap-only insertion for development wrappers; does not apply CSP
|
|
141
144
|
- `renderNappletNamespacePrelude` — render only the bootstrap `<script>` for hosts that already own HTML insertion
|
|
142
145
|
|
|
146
|
+
### Host CSP overrides
|
|
147
|
+
|
|
148
|
+
Resolve and verify the signed manifest, aggregate and artifact bytes before
|
|
149
|
+
calling `prepareNappletSrcdoc`. Keep the verified bytes for identity and caching;
|
|
150
|
+
only the rendered copy receives policy and bootstrap. Bind the iframe window to
|
|
151
|
+
the verified identity before assigning `srcdoc`, with `sandbox="allow-scripts"`.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { prepareNappletSrcdoc } from '@kehto/shell';
|
|
155
|
+
|
|
156
|
+
const srcdoc = prepareNappletSrcdoc(verifiedHtml, {
|
|
157
|
+
domains: ['theme'],
|
|
158
|
+
csp: {
|
|
159
|
+
connectOrigins: ['https://api.example'],
|
|
160
|
+
directives: { 'media-src': ['blob:'], 'img-src': ['data:'] },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Omitting `csp` applies NIP-5D's conservative baseline, including WASM compilation.
|
|
166
|
+
Directive overrides replace individual source lists. They cannot disable CSP
|
|
167
|
+
insertion, allow `'unsafe-eval'`, or suppress the inline namespace bootstrap.
|
|
168
|
+
`connect-src` defaults to the exact `connectOrigins` grants and can be narrowed;
|
|
169
|
+
wildcards, scheme-wide grants, credentials, paths, query strings and fragments
|
|
170
|
+
are rejected. Empty source lists are rejected; use `["'none'"]` to deny a
|
|
171
|
+
directive. Hosts may remove WASM permission by setting
|
|
172
|
+
`'script-src': ["'unsafe-inline'"]`.
|
|
173
|
+
|
|
174
|
+
`script-src` and an explicit `script-src-elem` must permit the inline bootstrap.
|
|
175
|
+
Nonce/hash/`strict-dynamic` combinations that suppress that allowance are rejected;
|
|
176
|
+
nonce-based bootstrap authorization is not supported by this API. Existing
|
|
177
|
+
artifact CSP and inherited response policies still apply cumulatively and may
|
|
178
|
+
block execution; this helper never strips them to make a napplet run.
|
|
179
|
+
|
|
180
|
+
Meta-unsupported directives (`frame-ancestors`, `sandbox`, `report-uri`) and
|
|
181
|
+
unsupported directive names are rejected. Configure embedding restrictions on
|
|
182
|
+
the host HTTP response. See the [CSP enforcement contract](https://github.com/kehto/web/blob/main/docs/policies/NIP-5D-CONFORMANCE.md#shell-csp-enforcement)
|
|
183
|
+
for normative requirements, enforced recommendations and configurable defaults.
|
|
184
|
+
|
|
143
185
|
### Domain proxies (NIP-5D composition seams)
|
|
144
186
|
- `createIdentityProxy` — intercept identity read requests; direct `emit()` is prohibited
|
|
145
187
|
- `createThemeProxy` — intercept `theme.get`; direct `emit()` is prohibited
|
|
@@ -211,3 +253,27 @@ Generated API module: `docs/api/modules/_kehto_shell.html` (run `pnpm docs:api`)
|
|
|
211
253
|
## License
|
|
212
254
|
|
|
213
255
|
MIT
|
|
256
|
+
|
|
257
|
+
## NIP-5D event compatibility
|
|
258
|
+
|
|
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
|
|
261
|
+
accepts legacy aggregate events through an isolated compatibility adapter.
|
|
262
|
+
Existing `aggregateHash` host/cache/ACL fields carry the verified artifact hash
|
|
263
|
+
for current events; legacy identities keep their original aggregate. Both paths
|
|
264
|
+
verify signatures and bytes before runtime injection and `srcdoc` execution.
|
|
265
|
+
For the schema and removal boundary, see [event migration](https://kehto.github.io/web/docs/migrations/NIP-5D-EVENT-SCHEMA.html).
|
|
266
|
+
|
|
267
|
+
### Intent delivery
|
|
268
|
+
|
|
269
|
+
The injected `window.napplet.intent.onDelivery(handler)` returns a closeable
|
|
270
|
+
subscription and drains buffered parent deliveries in order. Register it to
|
|
271
|
+
receive `{ sender, archetype, action, convention, payload? }`; INC is not required.
|
|
272
|
+
Its receiver is installed before `shell.ready` and survives namespace reassignment.
|
|
273
|
+
A throwing listener does not prevent delivery to other listeners.
|
|
274
|
+
|
|
275
|
+
When no canonical listener is registered, matching legacy INC listeners can
|
|
276
|
+
consume the delivery locally. This emits `KEHTO_COMPAT_INTENT_INC` once per
|
|
277
|
+
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -639,6 +639,82 @@ declare function renderNappletNamespacePrelude(options: NappletNamespacePreludeO
|
|
|
639
639
|
*/
|
|
640
640
|
declare function injectNappletNamespacePrelude(html: string, options: NappletNamespacePreludeOptions): string;
|
|
641
641
|
|
|
642
|
+
/** Source-list directives supported by the shell's meta-delivered CSP. */
|
|
643
|
+
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
|
+
/** Host replacements for individual CSP source lists; omitted directives retain defaults. */
|
|
645
|
+
type NappletCspDirectives = Readonly<Partial<Record<NappletCspDirective, readonly string[]>>>;
|
|
646
|
+
/** Host policy for a verified napplet's CSP, separate from its signed manifest. */
|
|
647
|
+
interface NappletCspOptions {
|
|
648
|
+
/** Explicit HTTP(S)/WS(S) origins granted direct connections; defaults to none. */
|
|
649
|
+
readonly connectOrigins?: readonly string[];
|
|
650
|
+
/** Source-list replacements. connect-src may only narrow the explicit grants. */
|
|
651
|
+
readonly directives?: NappletCspDirectives;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Build the default NIP-5D CSP with validated host source-list replacements.
|
|
655
|
+
* Meta-unsupported directives must be configured on the host HTTP response.
|
|
656
|
+
*
|
|
657
|
+
* @param options - Explicit connection grants and directive replacements.
|
|
658
|
+
* @returns A serialized policy. Invalid or conflicting overrides throw.
|
|
659
|
+
* @example
|
|
660
|
+
* ```ts
|
|
661
|
+
* const policy = buildNappletCsp({
|
|
662
|
+
* connectOrigins: ['https://api.example'],
|
|
663
|
+
* directives: { 'media-src': ['blob:'] },
|
|
664
|
+
* });
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
declare function buildNappletCsp(options?: NappletCspOptions): string;
|
|
668
|
+
/**
|
|
669
|
+
* Render the shell's validated CSP meta element.
|
|
670
|
+
*
|
|
671
|
+
* @param options - Explicit connection grants and directive replacements.
|
|
672
|
+
* @returns An attribute-escaped CSP meta element for the first position in head.
|
|
673
|
+
* @example
|
|
674
|
+
* ```ts
|
|
675
|
+
* const meta = renderNappletCspMeta({ directives: { 'img-src': ['data:'] } });
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
declare function renderNappletCspMeta(options?: NappletCspOptions): string;
|
|
679
|
+
/**
|
|
680
|
+
* Insert CSP before authored content. The caller must verify the artifact first.
|
|
681
|
+
* Existing authored CSP remains in place and can only further restrict execution.
|
|
682
|
+
*
|
|
683
|
+
* @param html - Already-verified artifact HTML; never use the result for its hash.
|
|
684
|
+
* @param options - Explicit connection grants and directive replacements.
|
|
685
|
+
* @returns A srcdoc copy with the CSP first in head.
|
|
686
|
+
* @example
|
|
687
|
+
* ```ts
|
|
688
|
+
* const protectedHtml = injectNappletCsp(verifiedHtml);
|
|
689
|
+
* ```
|
|
690
|
+
*/
|
|
691
|
+
declare function injectNappletCsp(html: string, options?: NappletCspOptions): string;
|
|
692
|
+
|
|
693
|
+
/** Shell bootstrap availability and host CSP for a verified napplet document. */
|
|
694
|
+
interface NappletSrcdocOptions extends NappletNamespacePreludeOptions {
|
|
695
|
+
/** Validated host policy changes. Omission uses the NIP-5D baseline. */
|
|
696
|
+
readonly csp?: NappletCspOptions;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Prepare verified artifact HTML with CSP first, then the mandatory namespace.
|
|
700
|
+
* There is no CSP opt-out. Resolve and verify the artifact before calling this;
|
|
701
|
+
* bind its identity to the sandboxed iframe before assigning the returned srcdoc.
|
|
702
|
+
* Keep the original verified bytes for hashing and caching.
|
|
703
|
+
*
|
|
704
|
+
* @param html - Already-verified napplet artifact HTML.
|
|
705
|
+
* @param options - Exposed domains and validated host CSP changes.
|
|
706
|
+
* @returns Srcdoc HTML with policy and bootstrap preceding authored content.
|
|
707
|
+
* @example
|
|
708
|
+
* ```ts
|
|
709
|
+
* iframe.sandbox.value = 'allow-scripts';
|
|
710
|
+
* iframe.srcdoc = prepareNappletSrcdoc(verifiedHtml, {
|
|
711
|
+
* domains: ['shell', 'theme'],
|
|
712
|
+
* csp: { directives: { 'media-src': ['blob:'] } },
|
|
713
|
+
* });
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
declare function prepareNappletSrcdoc(html: string, options: NappletSrcdocOptions): string;
|
|
717
|
+
|
|
642
718
|
/**
|
|
643
719
|
* Manifest cache — persists verified NIP-5A aggregate hashes per napplet identity.
|
|
644
720
|
*/
|
|
@@ -1452,4 +1528,4 @@ type ResourceInbound = ResourceInfoRequest | ResourceBytesRequest | ResourceByte
|
|
|
1452
1528
|
*/
|
|
1453
1529
|
type ResourceOutbound = ResourceInfoResult | ResourceInfoError | ResourceBytesResult | ResourceBytesError | ResourceBytesManyResult | ResourceBytesManyError;
|
|
1454
1530
|
|
|
1455
|
-
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 NappletNamespacePreludeOptions, 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, buildShellCapabilities, createIdentityProxy, createKeysProxy, createMediaProxy, createNotifyProxy, createShellBridge, createThemeProxy, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, originRegistry, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -389,7 +389,7 @@ var originRegistry = {
|
|
|
389
389
|
*/
|
|
390
390
|
getIdentity(win) {
|
|
391
391
|
const entry = registry.get(win);
|
|
392
|
-
if (
|
|
392
|
+
if (typeof entry?.dTag !== "string" || !entry?.aggregateHash) return void 0;
|
|
393
393
|
return { dTag: entry.dTag, aggregateHash: entry.aggregateHash };
|
|
394
394
|
},
|
|
395
395
|
/** Store the immutable environment captured for this exact frame registration. */
|
|
@@ -1691,7 +1691,13 @@ function nappletNamespacePrelude(domains) {
|
|
|
1691
1691
|
fire({ type: "inc.subscribe", id: subscriptionId, topic: stableTopic });
|
|
1692
1692
|
}
|
|
1693
1693
|
state.handlers.add(callback);
|
|
1694
|
+
const offLegacy = intentDelivery?.onLegacy(stableTopic, (delivery) => callback({
|
|
1695
|
+
topic: delivery.convention,
|
|
1696
|
+
sender: delivery.sender,
|
|
1697
|
+
...Object.prototype.hasOwnProperty.call(delivery, "payload") ? { payload: delivery.payload } : {}
|
|
1698
|
+
}));
|
|
1694
1699
|
return subscriptionHandle(() => {
|
|
1700
|
+
offLegacy?.();
|
|
1695
1701
|
const current = topicStates.get(stableTopic);
|
|
1696
1702
|
if (!current) return;
|
|
1697
1703
|
current.handlers.delete(callback);
|
|
@@ -2253,6 +2259,69 @@ function nappletNamespacePrelude(domains) {
|
|
|
2253
2259
|
}
|
|
2254
2260
|
};
|
|
2255
2261
|
}
|
|
2262
|
+
function makeIntentDelivery() {
|
|
2263
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
2264
|
+
const legacy = /* @__PURE__ */ new Map();
|
|
2265
|
+
const pending = [];
|
|
2266
|
+
let draining = false;
|
|
2267
|
+
let warned = false;
|
|
2268
|
+
function drain() {
|
|
2269
|
+
if (draining) return;
|
|
2270
|
+
draining = true;
|
|
2271
|
+
try {
|
|
2272
|
+
while (pending.length > 0) {
|
|
2273
|
+
const index = handlers.size > 0 ? 0 : pending.findIndex((delivery2) => [...legacy.values()].includes(delivery2.convention));
|
|
2274
|
+
if (index < 0) break;
|
|
2275
|
+
const [delivery] = pending.splice(index, 1);
|
|
2276
|
+
const canonical = handlers.size > 0;
|
|
2277
|
+
const recipients = canonical ? [...handlers] : [...legacy.entries()].filter(([, topic]) => topic === delivery.convention).map(([handler]) => handler);
|
|
2278
|
+
if (!canonical && !warned) {
|
|
2279
|
+
warned = true;
|
|
2280
|
+
console.warn("[KEHTO_COMPAT_INTENT_INC] Intent delivered to a legacy INC listener. Migrate to window.napplet.intent.onDelivery. See docs/compatibility.md.");
|
|
2281
|
+
}
|
|
2282
|
+
for (const handler of recipients) {
|
|
2283
|
+
try {
|
|
2284
|
+
handler(delivery);
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
console.error("Intent delivery handler failed", error);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
} finally {
|
|
2291
|
+
draining = false;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
listen((event) => {
|
|
2295
|
+
if (!isParentMessage(event)) return;
|
|
2296
|
+
const message = event.data;
|
|
2297
|
+
if (!message || message.type !== "intent.deliver") return;
|
|
2298
|
+
const value = message.delivery;
|
|
2299
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2300
|
+
const delivery = value;
|
|
2301
|
+
if (typeof delivery.sender !== "string" || typeof delivery.archetype !== "string" || typeof delivery.action !== "string" || typeof delivery.convention !== "string") return;
|
|
2302
|
+
pending.push(Object.freeze({
|
|
2303
|
+
sender: delivery.sender,
|
|
2304
|
+
archetype: delivery.archetype,
|
|
2305
|
+
action: delivery.action,
|
|
2306
|
+
convention: delivery.convention,
|
|
2307
|
+
...Object.prototype.hasOwnProperty.call(delivery, "payload") ? { payload: delivery.payload } : {}
|
|
2308
|
+
}));
|
|
2309
|
+
drain();
|
|
2310
|
+
});
|
|
2311
|
+
return {
|
|
2312
|
+
onDelivery(handler) {
|
|
2313
|
+
if (typeof handler !== "function") throw new TypeError("Intent delivery handler must be a function");
|
|
2314
|
+
handlers.add(handler);
|
|
2315
|
+
drain();
|
|
2316
|
+
return subscriptionHandle(() => handlers.delete(handler));
|
|
2317
|
+
},
|
|
2318
|
+
onLegacy(topic, handler) {
|
|
2319
|
+
legacy.set(handler, topic);
|
|
2320
|
+
drain();
|
|
2321
|
+
return () => legacy.delete(handler);
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2256
2325
|
function makeIntent() {
|
|
2257
2326
|
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
2258
2327
|
const normalizeRequest = (value) => {
|
|
@@ -2311,6 +2380,7 @@ function nappletNamespacePrelude(domains) {
|
|
|
2311
2380
|
);
|
|
2312
2381
|
};
|
|
2313
2382
|
return {
|
|
2383
|
+
onDelivery: intentDelivery.onDelivery,
|
|
2314
2384
|
invoke,
|
|
2315
2385
|
open: (archetype, payload, opts) => invoke({
|
|
2316
2386
|
archetype,
|
|
@@ -2490,6 +2560,7 @@ function nappletNamespacePrelude(domains) {
|
|
|
2490
2560
|
onChanged: (callback) => domainEventHandle("fs.changed", callback, (msg) => msg.change)
|
|
2491
2561
|
};
|
|
2492
2562
|
}
|
|
2563
|
+
const intentDelivery = allowed.has("intent") ? makeIntentDelivery() : void 0;
|
|
2493
2564
|
const shell = makeShell();
|
|
2494
2565
|
let inc;
|
|
2495
2566
|
let identity;
|
|
@@ -2623,6 +2694,124 @@ function nappletNamespacePrelude(domains) {
|
|
|
2623
2694
|
});
|
|
2624
2695
|
}
|
|
2625
2696
|
|
|
2697
|
+
// src/napplet-head.ts
|
|
2698
|
+
function insertNappletHead(html, content) {
|
|
2699
|
+
const opening = /^([\t\n\f\r ]*(?:<!doctype[\t\n\f\r ]+html[\t\n\f\r ]*>[\t\n\f\r ]*)?(?:<html>[\t\n\f\r ]*)?)(<head>)?/i.exec(html);
|
|
2700
|
+
const prefix = opening[0];
|
|
2701
|
+
const insertion = opening[2] ? content : `<head>${content}</head>`;
|
|
2702
|
+
return `${prefix}${insertion}${html.slice(prefix.length)}`;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// src/napplet-csp.ts
|
|
2706
|
+
var DEFAULT_DIRECTIVES = {
|
|
2707
|
+
"default-src": ["'none'"],
|
|
2708
|
+
"script-src": ["'unsafe-inline'", "'wasm-unsafe-eval'"],
|
|
2709
|
+
"style-src": ["'unsafe-inline'"],
|
|
2710
|
+
"img-src": ["data:", "blob:"],
|
|
2711
|
+
"font-src": ["data:"],
|
|
2712
|
+
"connect-src": ["'none'"],
|
|
2713
|
+
"worker-src": ["'none'"],
|
|
2714
|
+
"child-src": ["'none'"],
|
|
2715
|
+
"frame-src": ["'none'"],
|
|
2716
|
+
"media-src": ["'none'"],
|
|
2717
|
+
"object-src": ["'none'"],
|
|
2718
|
+
"manifest-src": ["'none'"],
|
|
2719
|
+
"base-uri": ["'none'"],
|
|
2720
|
+
"form-action": ["'none'"]
|
|
2721
|
+
};
|
|
2722
|
+
var OPTIONAL_DIRECTIVES = /* @__PURE__ */ new Set([
|
|
2723
|
+
"script-src-elem",
|
|
2724
|
+
"script-src-attr",
|
|
2725
|
+
"style-src-elem",
|
|
2726
|
+
"style-src-attr"
|
|
2727
|
+
]);
|
|
2728
|
+
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
2729
|
+
"'none'",
|
|
2730
|
+
"'self'",
|
|
2731
|
+
"'unsafe-inline'",
|
|
2732
|
+
"'wasm-unsafe-eval'",
|
|
2733
|
+
"'unsafe-hashes'",
|
|
2734
|
+
"'strict-dynamic'",
|
|
2735
|
+
"'report-sample'"
|
|
2736
|
+
]);
|
|
2737
|
+
function connectionOrigin(source) {
|
|
2738
|
+
let url;
|
|
2739
|
+
try {
|
|
2740
|
+
url = new URL(source);
|
|
2741
|
+
} catch {
|
|
2742
|
+
throw new Error(`CSP connection grant must be an exact HTTP(S)/WS(S) origin: ${source}`);
|
|
2743
|
+
}
|
|
2744
|
+
if (!/^(https?|wss?):$/.test(url.protocol) || url.username || url.password || url.pathname !== "/" && url.pathname !== "" || url.search || url.hash || /[\s*\\;,"<>'`]/.test(source + url.origin) || !source.startsWith(`${url.protocol}//`)) {
|
|
2745
|
+
throw new Error(`CSP connection grant must be an exact HTTP(S)/WS(S) origin: ${source}`);
|
|
2746
|
+
}
|
|
2747
|
+
return url.origin;
|
|
2748
|
+
}
|
|
2749
|
+
function validateSources(directive, sources2) {
|
|
2750
|
+
if (!Array.isArray(sources2) || sources2.length === 0) {
|
|
2751
|
+
throw new Error(`CSP ${directive} needs a nonempty source list; use 'none' to deny it`);
|
|
2752
|
+
}
|
|
2753
|
+
const result = [...new Set(sources2)];
|
|
2754
|
+
for (const source of result) {
|
|
2755
|
+
if (typeof source !== "string" || !/^[\x21-\x7e]+$/.test(source) || /[;,"<>\\]/.test(source)) {
|
|
2756
|
+
throw new Error(`Invalid CSP source in ${directive}`);
|
|
2757
|
+
}
|
|
2758
|
+
if (source.toLowerCase() === "'unsafe-eval'") {
|
|
2759
|
+
throw new Error("NIP-5D CSP forbids 'unsafe-eval'; use 'wasm-unsafe-eval' for WebAssembly");
|
|
2760
|
+
}
|
|
2761
|
+
if (source.includes("'") && !KEYWORDS.has(source) && !/^'(?:nonce-|sha(?:256|384|512)-)[A-Za-z0-9+/_-]+=*'$/.test(source)) {
|
|
2762
|
+
throw new Error(`Invalid CSP keyword in ${directive}: ${source}`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (result.includes("'none'") && result.length !== 1) {
|
|
2766
|
+
throw new Error(`CSP ${directive} cannot combine 'none' with other sources`);
|
|
2767
|
+
}
|
|
2768
|
+
return result;
|
|
2769
|
+
}
|
|
2770
|
+
function requireBootstrapScripts(directive, sources2) {
|
|
2771
|
+
if (!sources2.includes("'unsafe-inline'") || sources2.some(
|
|
2772
|
+
(source) => /^'(?:nonce-|sha(?:256|384|512)-|strict-dynamic')/.test(source)
|
|
2773
|
+
)) {
|
|
2774
|
+
throw new Error(`CSP ${directive} must allow the shell's inline namespace bootstrap`);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function buildNappletCsp(options = {}) {
|
|
2778
|
+
const grants = [...new Set((options.connectOrigins ?? []).map(connectionOrigin))].sort();
|
|
2779
|
+
const policy = {
|
|
2780
|
+
...DEFAULT_DIRECTIVES,
|
|
2781
|
+
"connect-src": grants.length ? grants : ["'none'"]
|
|
2782
|
+
};
|
|
2783
|
+
for (const [directive, sources2] of Object.entries(options.directives ?? {})) {
|
|
2784
|
+
if (!Object.hasOwn(DEFAULT_DIRECTIVES, directive) && !OPTIONAL_DIRECTIVES.has(directive)) {
|
|
2785
|
+
throw new Error(`Unsupported meta CSP directive: ${directive}`);
|
|
2786
|
+
}
|
|
2787
|
+
policy[directive] = validateSources(directive, sources2);
|
|
2788
|
+
}
|
|
2789
|
+
const connect = policy["connect-src"];
|
|
2790
|
+
if (!(connect.length === 1 && connect[0] === "'none'")) {
|
|
2791
|
+
policy["connect-src"] = [...new Set(connect.map((source) => {
|
|
2792
|
+
const origin = connectionOrigin(source);
|
|
2793
|
+
if (!grants.includes(origin)) throw new Error(`CSP connection origin was not granted: ${origin}`);
|
|
2794
|
+
return origin;
|
|
2795
|
+
}))].sort();
|
|
2796
|
+
}
|
|
2797
|
+
requireBootstrapScripts("script-src", policy["script-src"]);
|
|
2798
|
+
if (policy["script-src-elem"]) requireBootstrapScripts("script-src-elem", policy["script-src-elem"]);
|
|
2799
|
+
return Object.entries(policy).map(([name, sources2]) => `${name} ${sources2.join(" ")}`).join("; ");
|
|
2800
|
+
}
|
|
2801
|
+
function renderNappletCspMeta(options = {}) {
|
|
2802
|
+
const policy = buildNappletCsp(options).replace(/&/g, "&").replace(/"/g, """);
|
|
2803
|
+
return `<meta http-equiv="Content-Security-Policy" content="${policy}">`;
|
|
2804
|
+
}
|
|
2805
|
+
function injectNappletCsp(html, options = {}) {
|
|
2806
|
+
return insertNappletHead(html, renderNappletCspMeta(options));
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// src/napplet-srcdoc.ts
|
|
2810
|
+
function prepareNappletSrcdoc(html, options) {
|
|
2811
|
+
const meta = renderNappletCspMeta(options.csp);
|
|
2812
|
+
return insertNappletHead(html, meta + renderNappletNamespacePrelude(options));
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2626
2815
|
// src/index.ts
|
|
2627
2816
|
import { ALL_CAPABILITIES as ALL_CAPABILITIES2 } from "@kehto/runtime";
|
|
2628
2817
|
import { createEnforceGate, createNapEnforceGate, formatDenialReason } from "@kehto/runtime";
|
|
@@ -2701,6 +2890,7 @@ export {
|
|
|
2701
2890
|
TOPICS,
|
|
2702
2891
|
adaptHooks,
|
|
2703
2892
|
audioManager,
|
|
2893
|
+
buildNappletCsp,
|
|
2704
2894
|
buildShellCapabilities,
|
|
2705
2895
|
createEnforceGate,
|
|
2706
2896
|
createIdentityProxy,
|
|
@@ -2711,10 +2901,13 @@ export {
|
|
|
2711
2901
|
createShellBridge,
|
|
2712
2902
|
createThemeProxy,
|
|
2713
2903
|
formatDenialReason,
|
|
2904
|
+
injectNappletCsp,
|
|
2714
2905
|
injectNappletNamespacePrelude,
|
|
2715
2906
|
manifestCache,
|
|
2716
2907
|
nappKeyRegistry,
|
|
2717
2908
|
originRegistry,
|
|
2909
|
+
prepareNappletSrcdoc,
|
|
2910
|
+
renderNappletCspMeta,
|
|
2718
2911
|
renderNappletNamespacePrelude,
|
|
2719
2912
|
resolveShellEnvironment,
|
|
2720
2913
|
sessionRegistry
|