@kehto/shell 0.27.0 → 0.28.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 +10 -0
- package/dist/index.d.ts +37 -1
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,16 @@ survives same-frame navigation, but the port lives only in the verified document
|
|
|
126
126
|
demand.
|
|
127
127
|
- `ShellBridge.observeFrame(frame, windowId)` watches for the second load. Call
|
|
128
128
|
it after the frame is in the DOM and before assigning `srcdoc`.
|
|
129
|
+
- Load counting cannot see a napplet that navigates its frame before its own
|
|
130
|
+
first `load`: only the replacement fires `load`, and browsers fire no
|
|
131
|
+
MessagePort `close` event. Lock the frame instead: put
|
|
132
|
+
`renderNappletFramePolicyMeta()` (`Content-Security-Policy: frame-src 'none'`,
|
|
133
|
+
exported as `NAPPLET_FRAME_POLICY`) in the host page `<head>`, or send the
|
|
134
|
+
equivalent HTTP header. Browsers check every navigation of a child frame
|
|
135
|
+
against the embedding page's `frame-src`, including one the frame starts
|
|
136
|
+
itself, while an unfetched `srcdoc` still loads. `hasNappletFramePolicy(document)`
|
|
137
|
+
reports whether the meta element is present, and `observeFrame` warns once
|
|
138
|
+
when it is not. This is host page policy, not napplet protocol surface.
|
|
129
139
|
- `originRegistry.register()` throws for a `Window` that already handed over (or
|
|
130
140
|
lost) its port. Create a fresh iframe for every reload or navigation.
|
|
131
141
|
- `ShellAdapter.onNappletMessage(windowId, message)` observes each port envelope
|
package/dist/index.d.ts
CHANGED
|
@@ -932,6 +932,42 @@ interface NappletSrcdocOptions extends NappletNamespacePreludeOptions {
|
|
|
932
932
|
*/
|
|
933
933
|
declare function prepareNappletSrcdoc(html: string, options: NappletSrcdocOptions): string;
|
|
934
934
|
|
|
935
|
+
/**
|
|
936
|
+
* @kehto/shell -- the embedding-page policy that keeps a napplet frame on its verified document.
|
|
937
|
+
*
|
|
938
|
+
* NIP-5D treats a second `load`, a second `napplet.hello` or the port closing as evidence that the verified document
|
|
939
|
+
* was replaced. None of these can observe a napplet that navigates its frame before its own first `load`: only the
|
|
940
|
+
* replacement fires `load`, and browsers fire no MessagePort `close` event. CSP checks every navigation of a child
|
|
941
|
+
* frame against the embedding page's `frame-src`, including one the frame starts itself, while an unfetched `srcdoc`
|
|
942
|
+
* document still loads. Setting this policy on the host page therefore blocks the replacement outright.
|
|
943
|
+
* This is host page policy, not napplet protocol surface: https://github.com/nostr-protocol/nips/pull/2303
|
|
944
|
+
*/
|
|
945
|
+
/** The `frame-src` directive a host page sets so napplet frames cannot navigate away from their `srcdoc`. */
|
|
946
|
+
declare const NAPPLET_FRAME_POLICY = "frame-src 'none'";
|
|
947
|
+
/**
|
|
948
|
+
* Render the host-page `<meta>` element carrying {@link NAPPLET_FRAME_POLICY}. Place it in the host page's `<head>`;
|
|
949
|
+
* a `meta` CSP outside `<head>` is ignored. An equivalent HTTP `Content-Security-Policy` header works too.
|
|
950
|
+
*
|
|
951
|
+
* @returns The meta element HTML
|
|
952
|
+
* @example
|
|
953
|
+
* ```ts
|
|
954
|
+
* const head = `<head>${renderNappletFramePolicyMeta()}</head>`;
|
|
955
|
+
* ```
|
|
956
|
+
*/
|
|
957
|
+
declare function renderNappletFramePolicyMeta(): string;
|
|
958
|
+
/**
|
|
959
|
+
* Whether a host document carries {@link NAPPLET_FRAME_POLICY} in a `<meta>` CSP. A policy delivered only as an HTTP
|
|
960
|
+
* header is not visible here.
|
|
961
|
+
*
|
|
962
|
+
* @param doc - The host page document
|
|
963
|
+
* @returns True when a CSP meta element contains the directive
|
|
964
|
+
* @example
|
|
965
|
+
* ```ts
|
|
966
|
+
* if (!hasNappletFramePolicy(document)) throw new Error('napplet frames are not navigation-locked');
|
|
967
|
+
* ```
|
|
968
|
+
*/
|
|
969
|
+
declare function hasNappletFramePolicy(doc: Pick<Document, 'querySelectorAll'>): boolean;
|
|
970
|
+
|
|
935
971
|
/**
|
|
936
972
|
* Manifest cache — persists verified NIP-5A aggregate hashes per napplet identity.
|
|
937
973
|
*/
|
|
@@ -1764,4 +1800,4 @@ type ResourceInbound = ResourceInfoRequest | ResourceBytesRequest | ResourceByte
|
|
|
1764
1800
|
*/
|
|
1765
1801
|
type ResourceOutbound = ResourceInfoResult | ResourceInfoError | ResourceBytesResult | ResourceBytesError | ResourceBytesManyResult | ResourceBytesManyError;
|
|
1766
1802
|
|
|
1767
|
-
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 LlmHooks, type ManifestCache, type ManifestCacheEntry, type MediaProxy, type MediaProxyDeps, NAPPLET_HELLO, type NappletCspDirective, type NappletCspDirectives, type NappletCspOptions, type NappletMessageTarget, type NappletNamespacePreludeOptions, type NappletRevocation, type NappletRevocationReason, 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 ShellDomainState, 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, denyNappletControllerAccess, getShellDomainState, injectNappletCsp, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, normalizeIntentUri, originRegistry, prepareNappletSrcdoc, renderNappletCspMeta, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
|
|
1803
|
+
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 LlmHooks, type ManifestCache, type ManifestCacheEntry, type MediaProxy, type MediaProxyDeps, NAPPLET_FRAME_POLICY, NAPPLET_HELLO, type NappletCspDirective, type NappletCspDirectives, type NappletCspOptions, type NappletMessageTarget, type NappletNamespacePreludeOptions, type NappletRevocation, type NappletRevocationReason, 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 ShellDomainState, 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, denyNappletControllerAccess, getShellDomainState, hasNappletFramePolicy, injectNappletCsp, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, normalizeIntentUri, originRegistry, prepareNappletSrcdoc, renderNappletCspMeta, renderNappletFramePolicyMeta, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
|
package/dist/index.js
CHANGED
|
@@ -1280,6 +1280,15 @@ function postShellInit(windowId, environment) {
|
|
|
1280
1280
|
originRegistry.postToNapplet(windowId, initMsg);
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
|
+
// src/frame-policy.ts
|
|
1284
|
+
var NAPPLET_FRAME_POLICY = "frame-src 'none'";
|
|
1285
|
+
function renderNappletFramePolicyMeta() {
|
|
1286
|
+
return `<meta http-equiv="Content-Security-Policy" content="${NAPPLET_FRAME_POLICY}">`;
|
|
1287
|
+
}
|
|
1288
|
+
function hasNappletFramePolicy(doc) {
|
|
1289
|
+
return [...doc.querySelectorAll('meta[http-equiv="Content-Security-Policy" i]')].some((meta) => meta.content.split(";").some((directive) => directive.trim() === NAPPLET_FRAME_POLICY));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1283
1292
|
// src/nip5d-transport.ts
|
|
1284
1293
|
var NAPPLET_HELLO = "napplet.hello";
|
|
1285
1294
|
function reportUnrouted(hooks, event, reason) {
|
|
@@ -1302,6 +1311,15 @@ var REVOCATION_DETAIL = {
|
|
|
1302
1311
|
function isHello(data) {
|
|
1303
1312
|
return typeof data === "object" && data !== null && data.type === NAPPLET_HELLO;
|
|
1304
1313
|
}
|
|
1314
|
+
var framePolicyWarned = false;
|
|
1315
|
+
function warnWithoutFramePolicy(frame) {
|
|
1316
|
+
const doc = frame.ownerDocument;
|
|
1317
|
+
if (framePolicyWarned || !doc || hasNappletFramePolicy(doc)) return;
|
|
1318
|
+
framePolicyWarned = true;
|
|
1319
|
+
console.warn(
|
|
1320
|
+
`[kehto] The host page has no "${NAPPLET_FRAME_POLICY}" CSP. A napplet that navigates its frame before its first load is invisible to NIP-5D's replacement evidence; add renderNappletFramePolicyMeta() to the host page <head>.`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1305
1323
|
function createNip5dTransport({ hooks, runtime, state: shellReadyState }) {
|
|
1306
1324
|
const bindings = /* @__PURE__ */ new Map();
|
|
1307
1325
|
function currentBinding(windowId) {
|
|
@@ -1404,6 +1422,7 @@ function createNip5dTransport({ hooks, runtime, state: shellReadyState }) {
|
|
|
1404
1422
|
bindPort(windowId, sourceWindow, event.origin, ports[0]);
|
|
1405
1423
|
}
|
|
1406
1424
|
function observeFrame(frame, windowId) {
|
|
1425
|
+
warnWithoutFramePolicy(frame);
|
|
1407
1426
|
let loads = 0;
|
|
1408
1427
|
const onLoad = () => {
|
|
1409
1428
|
loads += 1;
|
|
@@ -1695,6 +1714,28 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
|
|
|
1695
1714
|
const target = window;
|
|
1696
1715
|
const allowed = new Set(domains);
|
|
1697
1716
|
const requestTimeoutMs = 3e4;
|
|
1717
|
+
const rtcGlobals = /* @__PURE__ */ new Set([
|
|
1718
|
+
"webkitRTCPeerConnection",
|
|
1719
|
+
"mozRTCPeerConnection",
|
|
1720
|
+
"webkitRTCSessionDescription",
|
|
1721
|
+
"mozRTCSessionDescription",
|
|
1722
|
+
"webkitRTCIceCandidate",
|
|
1723
|
+
"mozRTCIceCandidate"
|
|
1724
|
+
]);
|
|
1725
|
+
for (const key of Object.getOwnPropertyNames(target)) {
|
|
1726
|
+
if (key.startsWith("RTC")) rtcGlobals.add(key);
|
|
1727
|
+
}
|
|
1728
|
+
for (const rtcName of rtcGlobals) {
|
|
1729
|
+
try {
|
|
1730
|
+
Object.defineProperty(target, rtcName, {
|
|
1731
|
+
value: void 0,
|
|
1732
|
+
writable: false,
|
|
1733
|
+
configurable: false,
|
|
1734
|
+
enumerable: false
|
|
1735
|
+
});
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1698
1739
|
function id() {
|
|
1699
1740
|
if (target.crypto?.randomUUID) return target.crypto.randomUUID();
|
|
1700
1741
|
return `napplet-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
@@ -3483,6 +3524,7 @@ function createNotifyProxy(deps) {
|
|
|
3483
3524
|
}
|
|
3484
3525
|
export {
|
|
3485
3526
|
ALL_CAPABILITIES2 as ALL_CAPABILITIES,
|
|
3527
|
+
NAPPLET_FRAME_POLICY,
|
|
3486
3528
|
NAPPLET_HELLO,
|
|
3487
3529
|
TOPICS,
|
|
3488
3530
|
adaptHooks,
|
|
@@ -3500,6 +3542,7 @@ export {
|
|
|
3500
3542
|
denyNappletControllerAccess,
|
|
3501
3543
|
formatDenialReason,
|
|
3502
3544
|
getShellDomainState,
|
|
3545
|
+
hasNappletFramePolicy,
|
|
3503
3546
|
injectNappletCsp,
|
|
3504
3547
|
injectNappletNamespacePrelude,
|
|
3505
3548
|
manifestCache,
|
|
@@ -3508,6 +3551,7 @@ export {
|
|
|
3508
3551
|
originRegistry,
|
|
3509
3552
|
prepareNappletSrcdoc,
|
|
3510
3553
|
renderNappletCspMeta,
|
|
3554
|
+
renderNappletFramePolicyMeta,
|
|
3511
3555
|
renderNappletNamespacePrelude,
|
|
3512
3556
|
resolveShellEnvironment,
|
|
3513
3557
|
sessionRegistry
|