@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/README.md +71 -24
- package/dist/index.d.ts +109 -1
- package/dist/index.js +359 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
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 (
|
|
2350
|
-
|
|
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
|
|
2353
|
-
const
|
|
2354
|
-
if (
|
|
2355
|
-
throw new TypeError("
|
|
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
|
|
2358
|
-
...
|
|
2359
|
-
...
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
...
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
|
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, "&").replace(/"/g, """);
|
|
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
|