@kehto/shell 0.21.3 → 0.23.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/dist/index.js CHANGED
@@ -1246,6 +1246,181 @@ function createShellBridge(hooks) {
1246
1246
  };
1247
1247
  }
1248
1248
 
1249
+ // src/intent-uri.ts
1250
+ function createIntentUriNormalizer() {
1251
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1252
+ const fail = (message) => {
1253
+ throw new TypeError(message);
1254
+ };
1255
+ const decode = (value, label) => {
1256
+ try {
1257
+ return decodeURIComponent(value);
1258
+ } catch {
1259
+ return fail(`Intent URI has malformed percent-encoding in ${label}`);
1260
+ }
1261
+ };
1262
+ const decodeUtf8 = (bytes) => {
1263
+ try {
1264
+ return new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
1265
+ } catch {
1266
+ return fail("Intent URI fragment contains invalid UTF-8");
1267
+ }
1268
+ };
1269
+ const decodeHint = (fragment) => {
1270
+ if (!fragment.startsWith("naddr") || fragment !== fragment.toLowerCase()) {
1271
+ return fail("Intent URI fragment must be a bare lowercase naddr");
1272
+ }
1273
+ const separator = fragment.lastIndexOf("1");
1274
+ if (separator <= 0 || separator + 7 > fragment.length) fail("Intent URI fragment is not a valid naddr");
1275
+ const alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1276
+ const values = [];
1277
+ for (const character of fragment.slice(separator + 1)) {
1278
+ const value = alphabet.indexOf(character);
1279
+ if (value < 0) fail("Intent URI fragment is not a valid naddr");
1280
+ values.push(value);
1281
+ }
1282
+ const polymod = (input) => {
1283
+ let checksum = 1;
1284
+ for (const value of input) {
1285
+ const top = checksum >>> 25;
1286
+ checksum = (checksum & 33554431) << 5 ^ value;
1287
+ if (top & 1) checksum ^= 996825010;
1288
+ if (top & 2) checksum ^= 642813549;
1289
+ if (top & 4) checksum ^= 513874426;
1290
+ if (top & 8) checksum ^= 1027748829;
1291
+ if (top & 16) checksum ^= 705979059;
1292
+ }
1293
+ return checksum;
1294
+ };
1295
+ const hrp = fragment.slice(0, separator);
1296
+ const hrpValues = [...hrp].map((character) => character.charCodeAt(0) >>> 5).concat([0], [...hrp].map((character) => character.charCodeAt(0) & 31));
1297
+ if (hrp !== "naddr" || polymod(hrpValues.concat(values)) !== 1) fail("Intent URI fragment is not a valid naddr");
1298
+ const words = values.slice(0, -6);
1299
+ const bytes = [];
1300
+ let accumulator = 0;
1301
+ let bits = 0;
1302
+ for (const word of words) {
1303
+ accumulator = accumulator << 5 | word;
1304
+ bits += 5;
1305
+ while (bits >= 8) {
1306
+ bits -= 8;
1307
+ bytes.push(accumulator >>> bits & 255);
1308
+ }
1309
+ }
1310
+ if (bits >= 5 || (accumulator << 8 - bits & 255) !== 0) fail("Intent URI fragment is not a valid naddr");
1311
+ let index = 0;
1312
+ let identifier;
1313
+ let pubkey;
1314
+ let kind;
1315
+ const relays = [];
1316
+ while (index < bytes.length) {
1317
+ if (index + 2 > bytes.length) fail("Intent URI fragment is not a valid naddr");
1318
+ const type = bytes[index++];
1319
+ const length = bytes[index++];
1320
+ if (index + length > bytes.length) fail("Intent URI fragment is not a valid naddr");
1321
+ const value = bytes.slice(index, index + length);
1322
+ index += length;
1323
+ if (type === 0) {
1324
+ if (identifier !== void 0) fail("Intent URI fragment repeats its identifier");
1325
+ identifier = decodeUtf8(value);
1326
+ } else if (type === 1) {
1327
+ const relay = decodeUtf8(value);
1328
+ if (!relay) fail("Intent URI fragment has an empty relay hint");
1329
+ relays.push(relay);
1330
+ } else if (type === 2) {
1331
+ if (pubkey !== void 0 || value.length !== 32) fail("Intent URI fragment has an invalid pubkey");
1332
+ pubkey = value.map((part) => part.toString(16).padStart(2, "0")).join("");
1333
+ } else if (type === 3) {
1334
+ if (kind !== void 0 || value.length !== 4) fail("Intent URI fragment has an invalid kind");
1335
+ kind = (value[0] << 24 >>> 0 | value[1] << 16 | value[2] << 8 | value[3]) >>> 0;
1336
+ } else {
1337
+ fail("Intent URI fragment has unsupported naddr data");
1338
+ }
1339
+ }
1340
+ if (!identifier || !pubkey || kind !== 35129) fail("Intent URI fragment must recommend a named 35129 napplet");
1341
+ return { address: `${kind}:${pubkey}:${identifier}`, ...relays.length > 0 ? { relays } : {} };
1342
+ };
1343
+ const normalizeHint = (value) => {
1344
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("Intent handlerHint must be an object");
1345
+ const hint = value;
1346
+ if (Object.keys(hint).some((key) => key !== "address" && key !== "relays")) fail("Intent handlerHint contains unsupported fields");
1347
+ const address = hint.address;
1348
+ if (typeof address !== "string" || !/^35129:[0-9a-f]{64}:.+$/u.test(address)) {
1349
+ fail("Intent handlerHint address must be a named 35129 coordinate");
1350
+ }
1351
+ const relays = hint.relays;
1352
+ if (relays !== void 0 && (!Array.isArray(relays) || relays.some((relay) => typeof relay !== "string" || relay.length === 0))) {
1353
+ fail("Intent handlerHint relays must be non-empty text");
1354
+ }
1355
+ return { address, ...relays === void 0 ? {} : { relays: [...relays] } };
1356
+ };
1357
+ return function normalizeIntentUri2(uri, options) {
1358
+ if (typeof uri !== "string") throw new TypeError("Intent URI must be text");
1359
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)(?:\?([^#]*))?(?:#(.*))?$/u.exec(uri);
1360
+ if (match === null) return fail("Intent URI must be napplet:<archetype>/<action>");
1361
+ const archetype = match[1];
1362
+ const action = match[2];
1363
+ if (!/^[a-z0-9][a-z0-9-]*$/u.test(archetype) || !/^[a-z0-9][a-z0-9-]*$/u.test(action)) {
1364
+ fail("Intent URI archetype and action must be lowercase slugs");
1365
+ }
1366
+ const query = match[3];
1367
+ const fragment = match[4];
1368
+ const supplied = options === void 0 ? {} : options;
1369
+ if (!supplied || typeof supplied !== "object" || Array.isArray(supplied)) fail("Intent options must be an object");
1370
+ const parsedOptions = supplied;
1371
+ if (Object.keys(parsedOptions).some((key) => key !== "payload" && key !== "handler" && key !== "handlerHint" && key !== "behavior")) {
1372
+ fail("Intent options contain unsupported fields");
1373
+ }
1374
+ const handler = parsedOptions.handler;
1375
+ if (handler !== void 0 && (typeof handler !== "string" || handler.length === 0)) {
1376
+ fail("Intent handler must be non-empty text");
1377
+ }
1378
+ let behavior;
1379
+ if (parsedOptions.behavior !== void 0) {
1380
+ if (!parsedOptions.behavior || typeof parsedOptions.behavior !== "object" || Array.isArray(parsedOptions.behavior)) fail("Intent behavior must be an object");
1381
+ const value = parsedOptions.behavior;
1382
+ if (Object.keys(value).some((key) => key !== "focus" && key !== "reuse") || hasOwn(value, "focus") && typeof value.focus !== "boolean" || hasOwn(value, "reuse") && typeof value.reuse !== "boolean") fail("Intent behavior contains unsupported fields");
1383
+ behavior = {
1384
+ ...hasOwn(value, "focus") ? { focus: value.focus } : {},
1385
+ ...hasOwn(value, "reuse") ? { reuse: value.reuse } : {}
1386
+ };
1387
+ }
1388
+ let payload;
1389
+ if (query !== void 0) {
1390
+ if (hasOwn(parsedOptions, "payload")) fail("Intent URI query cannot accompany options.payload");
1391
+ if (query.length > 0) {
1392
+ const fields = /* @__PURE__ */ Object.create(null);
1393
+ for (const field of query.split("&")) {
1394
+ const equal = field.indexOf("=");
1395
+ if (equal <= 0 || field.indexOf("=", equal + 1) >= 0) fail("Intent URI query fields must be name=value");
1396
+ const name = decode(field.slice(0, equal), "a query name");
1397
+ const value = decode(field.slice(equal + 1), "a query value");
1398
+ if (!name || hasOwn(fields, name)) fail("Intent URI query names must be unique after decoding");
1399
+ Object.defineProperty(fields, name, { value, enumerable: true, writable: true, configurable: true });
1400
+ }
1401
+ payload = fields;
1402
+ }
1403
+ } else if (hasOwn(parsedOptions, "payload")) {
1404
+ payload = parsedOptions.payload;
1405
+ }
1406
+ if (fragment !== void 0) {
1407
+ if (!fragment) fail("Intent URI fragment must be a bare naddr");
1408
+ if (parsedOptions.handlerHint !== void 0) fail("Intent URI fragment cannot accompany options.handlerHint");
1409
+ }
1410
+ const handlerHint = fragment === void 0 ? parsedOptions.handlerHint === void 0 ? void 0 : normalizeHint(parsedOptions.handlerHint) : decodeHint(fragment);
1411
+ return {
1412
+ archetype,
1413
+ action,
1414
+ convention: `napplet:${archetype}/${action}`,
1415
+ ...payload === void 0 ? {} : { payload },
1416
+ ...handlerHint === void 0 ? {} : { handlerHint },
1417
+ ...handler === void 0 ? {} : { handler },
1418
+ ...behavior === void 0 ? {} : { behavior }
1419
+ };
1420
+ };
1421
+ }
1422
+ var normalizeIntentUri = createIntentUriNormalizer();
1423
+
1249
1424
  // src/napplet-namespace.ts
1250
1425
  function uniqueBareDomains(domains) {
1251
1426
  const seen = /* @__PURE__ */ new Set();
@@ -1265,7 +1440,7 @@ function scriptJson(value) {
1265
1440
  }
1266
1441
  function renderNappletNamespacePrelude(options) {
1267
1442
  const domains = uniqueBareDomains(["shell", ...options.domains]);
1268
- return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)});</script>`;
1443
+ return `<script data-kehto-nip5d-injection>(${nappletNamespacePrelude.toString()})(${scriptJson(domains)}, (${createIntentUriNormalizer.toString()})());</script>`;
1269
1444
  }
1270
1445
  function injectNappletNamespacePrelude(html, options) {
1271
1446
  const prelude = renderNappletNamespacePrelude(options);
@@ -1281,7 +1456,7 @@ function injectNappletNamespacePrelude(html, options) {
1281
1456
  }
1282
1457
  return `${prelude}${html}`;
1283
1458
  }
1284
- function nappletNamespacePrelude(domains) {
1459
+ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1285
1460
  const target = window;
1286
1461
  const allowed = new Set(domains);
1287
1462
  const requestTimeoutMs = 3e4;
@@ -2324,7 +2499,14 @@ function nappletNamespacePrelude(domains) {
2324
2499
  }
2325
2500
  function makeIntent() {
2326
2501
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
2327
- const normalizeRequest = (value) => {
2502
+ let warnedLegacyObject = false;
2503
+ const warnLegacyObject = () => {
2504
+ if (warnedLegacyObject) return;
2505
+ warnedLegacyObject = true;
2506
+ console.warn("[KEHTO_COMPAT_INTENT_OBJECT_INVOKE] Object-form intent.invoke/open is deprecated; use intent.invoke(uri, options) or intent.open(uri, options). Warns once per napplet. See docs/compatibility.md.");
2507
+ };
2508
+ const normalizeLegacyRequest = (value) => {
2509
+ warnLegacyObject();
2328
2510
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2329
2511
  throw new TypeError("Intent request must be an object");
2330
2512
  }
@@ -2332,7 +2514,7 @@ function nappletNamespacePrelude(domains) {
2332
2514
  if (hasOwn(supplied, "sender")) {
2333
2515
  throw new TypeError("Intent callers cannot supply sender");
2334
2516
  }
2335
- const allowed2 = ["archetype", "action", "convention", "payload", "handler", "behavior"];
2517
+ const allowed2 = ["archetype", "action", "convention", "payload", "handler", "handlerHint", "behavior"];
2336
2518
  if (Object.keys(supplied).some((field) => !allowed2.includes(field))) {
2337
2519
  throw new TypeError("Intent request contains unsupported fields");
2338
2520
  }
@@ -2343,51 +2525,63 @@ function nappletNamespacePrelude(domains) {
2343
2525
  if (typeof action !== "string" || action.length === 0) {
2344
2526
  throw new TypeError("Intent action must be a non-empty string");
2345
2527
  }
2346
- if (supplied.convention !== void 0 && (typeof supplied.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(supplied.convention))) {
2347
- throw new TypeError("Intent convention must be queryless");
2528
+ const uri = supplied.convention === void 0 ? `napplet:${supplied.archetype}/${action}` : supplied.convention;
2529
+ if (typeof uri !== "string") throw new TypeError("Intent convention must be text");
2530
+ if (uri.includes("?") || uri.includes("#")) throw new TypeError("Legacy intent convention must be queryless");
2531
+ return normalizeIntentUri2(uri, {
2532
+ ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2533
+ ...hasOwn(supplied, "handler") ? { handler: supplied.handler } : {},
2534
+ ...hasOwn(supplied, "handlerHint") ? { handlerHint: supplied.handlerHint } : {},
2535
+ ...hasOwn(supplied, "behavior") ? { behavior: supplied.behavior } : {}
2536
+ });
2537
+ };
2538
+ const requestInvoke = (normalized) => request(
2539
+ {
2540
+ type: "intent.invoke",
2541
+ request: normalized
2542
+ },
2543
+ "intent.invoke.result",
2544
+ (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2545
+ );
2546
+ const invoke = (uri, options) => {
2547
+ const normalized = typeof uri === "string" ? normalizeIntentUri2(uri, options) : normalizeLegacyRequest(uri);
2548
+ return requestInvoke(normalized);
2549
+ };
2550
+ const open = (uri, options, legacyOptions) => {
2551
+ if (typeof uri === "string" && uri.startsWith("napplet:") && legacyOptions === void 0) {
2552
+ const normalized2 = normalizeIntentUri2(uri, options);
2553
+ if (normalized2.action !== "open") throw new TypeError("intent.open requires a /open URI");
2554
+ return requestInvoke(normalized2);
2348
2555
  }
2349
- if (supplied.handler !== void 0 && (typeof supplied.handler !== "string" || supplied.handler.length === 0)) {
2350
- throw new TypeError("Intent handler must be a non-empty string");
2556
+ if (typeof uri !== "string" || !uri) throw new TypeError("Intent archetype must be non-empty text");
2557
+ warnLegacyObject();
2558
+ if (legacyOptions !== void 0 && (!legacyOptions || typeof legacyOptions !== "object" || Array.isArray(legacyOptions))) {
2559
+ throw new TypeError("Intent options must be an object");
2351
2560
  }
2352
- const behaviorValue = supplied?.behavior;
2353
- const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
2354
- 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")) {
2355
- throw new TypeError("Intent behavior contains unsupported fields");
2561
+ const legacy = legacyOptions;
2562
+ const convention = legacy?.convention === void 0 ? `napplet:${uri}/open` : legacy.convention;
2563
+ if (typeof convention !== "string" || convention.includes("?") || convention.includes("#")) {
2564
+ throw new TypeError("Legacy intent convention must be queryless");
2356
2565
  }
2357
- const sanitizedBehavior = {
2358
- ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
2359
- ...typeof behavior?.newWindow === "boolean" ? { newWindow: behavior.newWindow } : {},
2360
- ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
2361
- };
2362
- return {
2363
- archetype: supplied.archetype,
2364
- action,
2365
- ...typeof supplied.convention === "string" ? { convention: supplied.convention } : {},
2366
- ...hasOwn(supplied, "payload") ? { payload: supplied.payload } : {},
2367
- ...typeof supplied.handler === "string" ? { handler: supplied.handler } : {},
2368
- ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
2369
- };
2370
- };
2371
- const invoke = (value) => {
2372
- const normalized = normalizeRequest(value);
2373
- return request(
2374
- {
2375
- type: "intent.invoke",
2376
- request: normalized
2377
- },
2378
- "intent.invoke.result",
2379
- (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2380
- );
2566
+ const normalized = normalizeIntentUri2(`napplet:${uri}/open`, {
2567
+ ...options === void 0 ? {} : { payload: options },
2568
+ ...legacy === void 0 ? {} : Object.fromEntries(
2569
+ Object.entries(legacy).filter(([key]) => key !== "convention")
2570
+ )
2571
+ });
2572
+ const legacyNormalized = convention === `napplet:${uri}/open` ? normalized : normalizeIntentUri2(convention, {
2573
+ ...options === void 0 ? {} : { payload: options },
2574
+ ...legacy === void 0 ? {} : Object.fromEntries(
2575
+ Object.entries(legacy).filter(([key]) => key !== "convention")
2576
+ )
2577
+ });
2578
+ if (legacyNormalized.action !== "open") throw new TypeError("intent.open requires a /open URI");
2579
+ return requestInvoke(legacyNormalized);
2381
2580
  };
2382
2581
  return {
2383
2582
  onDelivery: intentDelivery.onDelivery,
2384
2583
  invoke,
2385
- open: (archetype, payload, opts) => invoke({
2386
- archetype,
2387
- action: "open",
2388
- ...payload === void 0 ? {} : { payload },
2389
- ...opts
2390
- }),
2584
+ open,
2391
2585
  available: (archetype) => request(
2392
2586
  { type: "intent.available", archetype },
2393
2587
  "intent.available.result",
@@ -2694,6 +2888,124 @@ function nappletNamespacePrelude(domains) {
2694
2888
  });
2695
2889
  }
2696
2890
 
2891
+ // src/napplet-head.ts
2892
+ function insertNappletHead(html, content) {
2893
+ 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);
2894
+ const prefix = opening[0];
2895
+ const insertion = opening[2] ? content : `<head>${content}</head>`;
2896
+ return `${prefix}${insertion}${html.slice(prefix.length)}`;
2897
+ }
2898
+
2899
+ // src/napplet-csp.ts
2900
+ var DEFAULT_DIRECTIVES = {
2901
+ "default-src": ["'none'"],
2902
+ "script-src": ["'unsafe-inline'", "'wasm-unsafe-eval'"],
2903
+ "style-src": ["'unsafe-inline'"],
2904
+ "img-src": ["data:", "blob:"],
2905
+ "font-src": ["data:"],
2906
+ "connect-src": ["'none'"],
2907
+ "worker-src": ["'none'"],
2908
+ "child-src": ["'none'"],
2909
+ "frame-src": ["'none'"],
2910
+ "media-src": ["'none'"],
2911
+ "object-src": ["'none'"],
2912
+ "manifest-src": ["'none'"],
2913
+ "base-uri": ["'none'"],
2914
+ "form-action": ["'none'"]
2915
+ };
2916
+ var OPTIONAL_DIRECTIVES = /* @__PURE__ */ new Set([
2917
+ "script-src-elem",
2918
+ "script-src-attr",
2919
+ "style-src-elem",
2920
+ "style-src-attr"
2921
+ ]);
2922
+ var KEYWORDS = /* @__PURE__ */ new Set([
2923
+ "'none'",
2924
+ "'self'",
2925
+ "'unsafe-inline'",
2926
+ "'wasm-unsafe-eval'",
2927
+ "'unsafe-hashes'",
2928
+ "'strict-dynamic'",
2929
+ "'report-sample'"
2930
+ ]);
2931
+ function connectionOrigin(source) {
2932
+ let url;
2933
+ try {
2934
+ url = new URL(source);
2935
+ } catch {
2936
+ throw new Error(`CSP connection grant must be an exact HTTP(S)/WS(S) origin: ${source}`);
2937
+ }
2938
+ 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}//`)) {
2939
+ throw new Error(`CSP connection grant must be an exact HTTP(S)/WS(S) origin: ${source}`);
2940
+ }
2941
+ return url.origin;
2942
+ }
2943
+ function validateSources(directive, sources2) {
2944
+ if (!Array.isArray(sources2) || sources2.length === 0) {
2945
+ throw new Error(`CSP ${directive} needs a nonempty source list; use 'none' to deny it`);
2946
+ }
2947
+ const result = [...new Set(sources2)];
2948
+ for (const source of result) {
2949
+ if (typeof source !== "string" || !/^[\x21-\x7e]+$/.test(source) || /[;,"<>\\]/.test(source)) {
2950
+ throw new Error(`Invalid CSP source in ${directive}`);
2951
+ }
2952
+ if (source.toLowerCase() === "'unsafe-eval'") {
2953
+ throw new Error("NIP-5D CSP forbids 'unsafe-eval'; use 'wasm-unsafe-eval' for WebAssembly");
2954
+ }
2955
+ if (source.includes("'") && !KEYWORDS.has(source) && !/^'(?:nonce-|sha(?:256|384|512)-)[A-Za-z0-9+/_-]+=*'$/.test(source)) {
2956
+ throw new Error(`Invalid CSP keyword in ${directive}: ${source}`);
2957
+ }
2958
+ }
2959
+ if (result.includes("'none'") && result.length !== 1) {
2960
+ throw new Error(`CSP ${directive} cannot combine 'none' with other sources`);
2961
+ }
2962
+ return result;
2963
+ }
2964
+ function requireBootstrapScripts(directive, sources2) {
2965
+ if (!sources2.includes("'unsafe-inline'") || sources2.some(
2966
+ (source) => /^'(?:nonce-|sha(?:256|384|512)-|strict-dynamic')/.test(source)
2967
+ )) {
2968
+ throw new Error(`CSP ${directive} must allow the shell's inline namespace bootstrap`);
2969
+ }
2970
+ }
2971
+ function buildNappletCsp(options = {}) {
2972
+ const grants = [...new Set((options.connectOrigins ?? []).map(connectionOrigin))].sort();
2973
+ const policy = {
2974
+ ...DEFAULT_DIRECTIVES,
2975
+ "connect-src": grants.length ? grants : ["'none'"]
2976
+ };
2977
+ for (const [directive, sources2] of Object.entries(options.directives ?? {})) {
2978
+ if (!Object.hasOwn(DEFAULT_DIRECTIVES, directive) && !OPTIONAL_DIRECTIVES.has(directive)) {
2979
+ throw new Error(`Unsupported meta CSP directive: ${directive}`);
2980
+ }
2981
+ policy[directive] = validateSources(directive, sources2);
2982
+ }
2983
+ const connect = policy["connect-src"];
2984
+ if (!(connect.length === 1 && connect[0] === "'none'")) {
2985
+ policy["connect-src"] = [...new Set(connect.map((source) => {
2986
+ const origin = connectionOrigin(source);
2987
+ if (!grants.includes(origin)) throw new Error(`CSP connection origin was not granted: ${origin}`);
2988
+ return origin;
2989
+ }))].sort();
2990
+ }
2991
+ requireBootstrapScripts("script-src", policy["script-src"]);
2992
+ if (policy["script-src-elem"]) requireBootstrapScripts("script-src-elem", policy["script-src-elem"]);
2993
+ return Object.entries(policy).map(([name, sources2]) => `${name} ${sources2.join(" ")}`).join("; ");
2994
+ }
2995
+ function renderNappletCspMeta(options = {}) {
2996
+ const policy = buildNappletCsp(options).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
2997
+ return `<meta http-equiv="Content-Security-Policy" content="${policy}">`;
2998
+ }
2999
+ function injectNappletCsp(html, options = {}) {
3000
+ return insertNappletHead(html, renderNappletCspMeta(options));
3001
+ }
3002
+
3003
+ // src/napplet-srcdoc.ts
3004
+ function prepareNappletSrcdoc(html, options) {
3005
+ const meta = renderNappletCspMeta(options.csp);
3006
+ return insertNappletHead(html, meta + renderNappletNamespacePrelude(options));
3007
+ }
3008
+
2697
3009
  // src/index.ts
2698
3010
  import { ALL_CAPABILITIES as ALL_CAPABILITIES2 } from "@kehto/runtime";
2699
3011
  import { createEnforceGate, createNapEnforceGate, formatDenialReason } from "@kehto/runtime";
@@ -2772,6 +3084,7 @@ export {
2772
3084
  TOPICS,
2773
3085
  adaptHooks,
2774
3086
  audioManager,
3087
+ buildNappletCsp,
2775
3088
  buildShellCapabilities,
2776
3089
  createEnforceGate,
2777
3090
  createIdentityProxy,
@@ -2782,10 +3095,14 @@ export {
2782
3095
  createShellBridge,
2783
3096
  createThemeProxy,
2784
3097
  formatDenialReason,
3098
+ injectNappletCsp,
2785
3099
  injectNappletNamespacePrelude,
2786
3100
  manifestCache,
2787
3101
  nappKeyRegistry,
3102
+ normalizeIntentUri,
2788
3103
  originRegistry,
3104
+ prepareNappletSrcdoc,
3105
+ renderNappletCspMeta,
2789
3106
  renderNappletNamespacePrelude,
2790
3107
  resolveShellEnvironment,
2791
3108
  sessionRegistry