@kehto/paja 0.18.1 → 0.20.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 +88 -45
- package/dist/browser-host.js +2696 -1396
- package/dist/browser-host.js.map +1 -1
- package/dist/{chunk-YX6C2UU7.js → chunk-6WEO5D3Z.js} +259 -56
- package/dist/chunk-6WEO5D3Z.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +92 -18
- package/dist/index.js +9 -1
- package/dist/{server-Ia5n1Bqv.d.ts → server-5j5uoy2Z.d.ts} +5 -0
- package/package.json +5 -5
- package/dist/chunk-YX6C2UU7.js.map +0 -1
|
@@ -374,6 +374,7 @@ function createPajaRuntimeHostConfig(options = {}, now = /* @__PURE__ */ new Dat
|
|
|
374
374
|
const base = createPajaHostConfigBase(normalizePajaSimulation(options.simulation));
|
|
375
375
|
return {
|
|
376
376
|
...base,
|
|
377
|
+
...options.csp ? { csp: options.csp } : {},
|
|
377
378
|
target: {
|
|
378
379
|
mode: "runtime-pointer",
|
|
379
380
|
url: "about:blank",
|
|
@@ -697,6 +698,7 @@ function renderPajaHtml(config) {
|
|
|
697
698
|
<section class="paja-notification-center" id="paja-notification-center" aria-label="Napplet notifications" aria-live="polite"></section>
|
|
698
699
|
${renderConfirmationDialog()}
|
|
699
700
|
${renderConfigDialog()}
|
|
701
|
+
${renderIntentLinkDialog()}
|
|
700
702
|
${renderDuplicateDialog()}
|
|
701
703
|
<footer class="bar bottom">
|
|
702
704
|
<span>mode: <code>${escapeHtml(getModeLabel(config))}</code></span>
|
|
@@ -754,6 +756,27 @@ function renderConfigDialog() {
|
|
|
754
756
|
</div>
|
|
755
757
|
</dialog>`;
|
|
756
758
|
}
|
|
759
|
+
function renderIntentLinkDialog() {
|
|
760
|
+
return `<dialog class="config-dialog" id="paja-intent-link-dialog" aria-labelledby="paja-intent-link-title" aria-describedby="paja-intent-link-status">
|
|
761
|
+
<form class="dialog" method="dialog">
|
|
762
|
+
<div class="dialog-title" id="paja-intent-link-title">Review intent link</div>
|
|
763
|
+
<label>Intent URI <input id="paja-intent-link-uri" type="text" autocomplete="off" spellcheck="false"></label>
|
|
764
|
+
<label>Payload <textarea id="paja-intent-link-payload" rows="5" spellcheck="false"></textarea></label>
|
|
765
|
+
<label>Compatible handler <select id="paja-intent-link-handler" disabled></select></label>
|
|
766
|
+
<label><input id="paja-intent-link-save-default" type="checkbox"> Set as my default for this role</label>
|
|
767
|
+
<button type="button" id="paja-intent-link-clear-default">Clear saved default</button>
|
|
768
|
+
<div class="dialog-copy" id="paja-intent-link-target"></div>
|
|
769
|
+
<div class="config-error" id="paja-intent-link-status" role="status" aria-live="polite"></div>
|
|
770
|
+
<div class="dialog-actions">
|
|
771
|
+
<button type="button" id="paja-intent-link-choose">Choose another</button>
|
|
772
|
+
<button type="button" id="paja-intent-link-use-handler" disabled>Use handler</button>
|
|
773
|
+
<button type="button" id="paja-intent-link-retry">Retry</button>
|
|
774
|
+
<button type="button" id="paja-intent-link-cancel">Cancel</button>
|
|
775
|
+
<button type="button" id="paja-intent-link-launch">Launch</button>
|
|
776
|
+
</div>
|
|
777
|
+
</form>
|
|
778
|
+
</dialog>`;
|
|
779
|
+
}
|
|
757
780
|
function renderStage(config, targetLabel) {
|
|
758
781
|
if (config.target.mode === "runtime-pointer") {
|
|
759
782
|
return `<section class="stage" id="napplet-stage" aria-label="Loaded napplet runtimes">
|
|
@@ -811,6 +834,7 @@ function escapeJsonForHtml(value) {
|
|
|
811
834
|
}
|
|
812
835
|
|
|
813
836
|
// src/runtime-resolver.ts
|
|
837
|
+
import { injectNappletCsp } from "@kehto/shell";
|
|
814
838
|
import { SimplePool } from "nostr-tools/pool";
|
|
815
839
|
import { decode } from "nostr-tools/nip19";
|
|
816
840
|
import {
|
|
@@ -931,34 +955,7 @@ function resolvePointerBlossomServers(event, configured) {
|
|
|
931
955
|
return servers;
|
|
932
956
|
}
|
|
933
957
|
function injectPajaRuntimeCsp(html, origins) {
|
|
934
|
-
|
|
935
|
-
const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
|
|
936
|
-
const value = [
|
|
937
|
-
"default-src 'none'",
|
|
938
|
-
"script-src 'unsafe-inline' 'wasm-unsafe-eval'",
|
|
939
|
-
"style-src 'unsafe-inline'",
|
|
940
|
-
"img-src data: blob:",
|
|
941
|
-
"font-src data:",
|
|
942
|
-
connectSrc,
|
|
943
|
-
"worker-src 'none'",
|
|
944
|
-
"child-src 'none'",
|
|
945
|
-
"frame-src 'none'",
|
|
946
|
-
"media-src 'none'",
|
|
947
|
-
"object-src 'none'",
|
|
948
|
-
"manifest-src 'none'",
|
|
949
|
-
"prefetch-src 'none'",
|
|
950
|
-
"base-uri 'none'",
|
|
951
|
-
"form-action 'none'",
|
|
952
|
-
"frame-ancestors 'self'"
|
|
953
|
-
].join("; ");
|
|
954
|
-
const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute2(value)}">`;
|
|
955
|
-
if (/<head[^>]*>/i.test(html)) {
|
|
956
|
-
return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
|
|
957
|
-
}
|
|
958
|
-
if (/<html[^>]*>/i.test(html)) {
|
|
959
|
-
return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
|
|
960
|
-
}
|
|
961
|
-
return `${meta}${html}`;
|
|
958
|
+
return injectNappletCsp(html, { connectOrigins: origins });
|
|
962
959
|
}
|
|
963
960
|
async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
|
|
964
961
|
const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
|
|
@@ -1104,9 +1101,6 @@ function normalizeRelayUrl(value) {
|
|
|
1104
1101
|
return void 0;
|
|
1105
1102
|
}
|
|
1106
1103
|
}
|
|
1107
|
-
function escapeAttribute2(value) {
|
|
1108
|
-
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
1109
|
-
}
|
|
1110
1104
|
function formatNappletManifestKinds() {
|
|
1111
1105
|
return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
|
|
1112
1106
|
}
|
|
@@ -1319,7 +1313,7 @@ async function sha256Hex(bytes) {
|
|
|
1319
1313
|
// src/installed-napplet-catalog.ts
|
|
1320
1314
|
import { manifestToIntentCatalogEntry } from "@kehto/services";
|
|
1321
1315
|
function matchesInstalledNappletRecord(record, target) {
|
|
1322
|
-
return target.
|
|
1316
|
+
return target.id === record.id && target.aggregateHash === record.aggregateHash;
|
|
1323
1317
|
}
|
|
1324
1318
|
var InstalledNappletCatalog = class {
|
|
1325
1319
|
records = /* @__PURE__ */ new Map();
|
|
@@ -1327,6 +1321,7 @@ var InstalledNappletCatalog = class {
|
|
|
1327
1321
|
/** Insert or replace a record from an already resolver-verified pointer. */
|
|
1328
1322
|
install(resolved) {
|
|
1329
1323
|
const record = freezeRecord({
|
|
1324
|
+
id: resolved.manifest.catalogId,
|
|
1330
1325
|
dTag: resolved.dTag,
|
|
1331
1326
|
aggregateHash: resolved.aggregateHash,
|
|
1332
1327
|
pointer: copyPointer(resolved.pointer),
|
|
@@ -1335,31 +1330,31 @@ var InstalledNappletCatalog = class {
|
|
|
1335
1330
|
optional: [...resolved.manifest.optional ?? []],
|
|
1336
1331
|
archetypes: resolved.manifest.archetypes.map((archetype) => ({
|
|
1337
1332
|
slug: archetype.slug,
|
|
1338
|
-
convention: archetype.convention
|
|
1333
|
+
convention: archetype.convention,
|
|
1334
|
+
params: [...archetype.params]
|
|
1339
1335
|
}))
|
|
1340
1336
|
});
|
|
1341
|
-
|
|
1342
|
-
this.
|
|
1343
|
-
this.notify(record.dTag);
|
|
1337
|
+
this.records.set(record.id, record);
|
|
1338
|
+
this.notify(record.id);
|
|
1344
1339
|
return record;
|
|
1345
1340
|
}
|
|
1346
1341
|
/** Remove an artifact explicitly; frame teardown never calls this method. */
|
|
1347
|
-
remove(
|
|
1348
|
-
const removed = this.records.delete(
|
|
1349
|
-
if (removed) this.notify(
|
|
1342
|
+
remove(id) {
|
|
1343
|
+
const removed = this.records.delete(id);
|
|
1344
|
+
if (removed) this.notify(id);
|
|
1350
1345
|
return removed;
|
|
1351
1346
|
}
|
|
1352
|
-
/** Return whether a verified installation exists for a
|
|
1353
|
-
has(
|
|
1354
|
-
return this.records.has(
|
|
1347
|
+
/** Return whether a verified installation exists for a catalog ID. */
|
|
1348
|
+
has(id) {
|
|
1349
|
+
return this.records.has(id);
|
|
1355
1350
|
}
|
|
1356
1351
|
/** Return immutable serializable installed-artifact facts. */
|
|
1357
1352
|
installed() {
|
|
1358
1353
|
return [...this.records.values()];
|
|
1359
1354
|
}
|
|
1360
|
-
/** Return a verified record by
|
|
1361
|
-
get(
|
|
1362
|
-
return this.records.get(
|
|
1355
|
+
/** Return a verified record by catalog ID. */
|
|
1356
|
+
get(id) {
|
|
1357
|
+
return this.records.get(id);
|
|
1363
1358
|
}
|
|
1364
1359
|
/**
|
|
1365
1360
|
* Atomically confirm that a resolved target still belongs to the exact record
|
|
@@ -1367,7 +1362,7 @@ var InstalledNappletCatalog = class {
|
|
|
1367
1362
|
* token, so even a same-identity replacement cannot pass this check.
|
|
1368
1363
|
*/
|
|
1369
1364
|
validateCurrent(selected, target) {
|
|
1370
|
-
if (this.records.get(selected.
|
|
1365
|
+
if (this.records.get(selected.id) !== selected) return null;
|
|
1371
1366
|
return matchesInstalledNappletRecord(selected, target) ? selected : null;
|
|
1372
1367
|
}
|
|
1373
1368
|
/**
|
|
@@ -1378,11 +1373,12 @@ var InstalledNappletCatalog = class {
|
|
|
1378
1373
|
*/
|
|
1379
1374
|
intentCatalog(canReceiveIntent = () => true) {
|
|
1380
1375
|
return this.installed().filter(canReceiveIntent).map((record) => manifestToIntentCatalogEntry({
|
|
1381
|
-
|
|
1376
|
+
catalogId: record.id,
|
|
1382
1377
|
...record.title === void 0 ? {} : { title: record.title },
|
|
1383
1378
|
archetypes: record.archetypes.map((archetype) => ({
|
|
1384
1379
|
slug: archetype.slug,
|
|
1385
|
-
convention: archetype.convention
|
|
1380
|
+
convention: archetype.convention,
|
|
1381
|
+
params: [...archetype.params]
|
|
1386
1382
|
}))
|
|
1387
1383
|
}));
|
|
1388
1384
|
}
|
|
@@ -1391,8 +1387,8 @@ var InstalledNappletCatalog = class {
|
|
|
1391
1387
|
this.listeners.add(listener);
|
|
1392
1388
|
return () => this.listeners.delete(listener);
|
|
1393
1389
|
}
|
|
1394
|
-
notify(
|
|
1395
|
-
for (const listener of this.listeners) listener(
|
|
1390
|
+
notify(id) {
|
|
1391
|
+
for (const listener of this.listeners) listener(id);
|
|
1396
1392
|
}
|
|
1397
1393
|
};
|
|
1398
1394
|
function copyPointer(pointer) {
|
|
@@ -1420,7 +1416,7 @@ function freezeRecord(record) {
|
|
|
1420
1416
|
...record,
|
|
1421
1417
|
requires: Object.freeze([...record.requires]),
|
|
1422
1418
|
optional: Object.freeze([...record.optional ?? []]),
|
|
1423
|
-
archetypes: Object.freeze(record.archetypes.map((archetype) => Object.freeze({ ...archetype })))
|
|
1419
|
+
archetypes: Object.freeze(record.archetypes.map((archetype) => Object.freeze({ ...archetype, params: Object.freeze([...archetype.params]) })))
|
|
1424
1420
|
});
|
|
1425
1421
|
}
|
|
1426
1422
|
|
|
@@ -1433,6 +1429,13 @@ var BrowserIntentController = class {
|
|
|
1433
1429
|
}
|
|
1434
1430
|
options;
|
|
1435
1431
|
maxAttempts;
|
|
1432
|
+
/** Retain delivery work immediately; completion remains host-observable only. */
|
|
1433
|
+
accept(params) {
|
|
1434
|
+
const acceptance = Object.freeze({ completion: this.dispatch(params) });
|
|
1435
|
+
this.options.onAccepted?.(params, acceptance);
|
|
1436
|
+
return acceptance;
|
|
1437
|
+
}
|
|
1438
|
+
/** Complete an already accepted delivery. Kept public for host diagnostics and tests. */
|
|
1436
1439
|
async dispatch(params) {
|
|
1437
1440
|
const dispatch = freezeDispatch(params);
|
|
1438
1441
|
let reason = "no-current-target";
|
|
@@ -1460,9 +1463,8 @@ var BrowserIntentController = class {
|
|
|
1460
1463
|
}
|
|
1461
1464
|
try {
|
|
1462
1465
|
await this.options.send(generation, dispatch);
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
return { windowId };
|
|
1466
|
+
this.options.onDelivered?.(dispatch);
|
|
1467
|
+
return;
|
|
1466
1468
|
} catch {
|
|
1467
1469
|
this.options.onTerminal?.(dispatch, "send-failed");
|
|
1468
1470
|
throw new Error("intent target send failed");
|
|
@@ -1493,7 +1495,8 @@ function freezeDispatch(params) {
|
|
|
1493
1495
|
action: params.action,
|
|
1494
1496
|
convention: params.convention,
|
|
1495
1497
|
...params.payload === void 0 ? {} : { payload },
|
|
1496
|
-
...behavior === void 0 ? {} : { behavior }
|
|
1498
|
+
...behavior === void 0 ? {} : { behavior },
|
|
1499
|
+
...params.sourceWindowId === void 0 ? {} : { sourceWindowId: params.sourceWindowId }
|
|
1497
1500
|
});
|
|
1498
1501
|
}
|
|
1499
1502
|
function freezeValue(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
@@ -1512,6 +1515,202 @@ function freezeValue(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
|
1512
1515
|
return Object.freeze(copy);
|
|
1513
1516
|
}
|
|
1514
1517
|
|
|
1518
|
+
// src/intent-link.ts
|
|
1519
|
+
import { normalizeIntentUri } from "@kehto/shell";
|
|
1520
|
+
var PAJA_INTENT_LINK_MAX_BYTES = 16 * 1024;
|
|
1521
|
+
function fail(message) {
|
|
1522
|
+
throw new TypeError(message);
|
|
1523
|
+
}
|
|
1524
|
+
function hasOwn(value, key) {
|
|
1525
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
1526
|
+
}
|
|
1527
|
+
function strictDecode(value, label) {
|
|
1528
|
+
try {
|
|
1529
|
+
return decodeURIComponent(value);
|
|
1530
|
+
} catch {
|
|
1531
|
+
return fail(`Paja intent link has malformed percent-encoding in ${label}`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function parseOuterQuery(search) {
|
|
1535
|
+
const fields = /* @__PURE__ */ Object.create(null);
|
|
1536
|
+
if (!search) return fields;
|
|
1537
|
+
for (const part of search.slice(1).split("&")) {
|
|
1538
|
+
const equal = part.indexOf("=");
|
|
1539
|
+
if (equal <= 0) fail("Paja intent link fields must be name=value");
|
|
1540
|
+
const name = strictDecode(part.slice(0, equal), "a field name");
|
|
1541
|
+
const value = strictDecode(part.slice(equal + 1), `the ${name || "unnamed"} field`);
|
|
1542
|
+
if (!name || hasOwn(fields, name)) fail("Paja intent link field names must be unique after decoding");
|
|
1543
|
+
Object.defineProperty(fields, name, { value, enumerable: true, configurable: true });
|
|
1544
|
+
}
|
|
1545
|
+
return fields;
|
|
1546
|
+
}
|
|
1547
|
+
function rawOuterValue(search, expectedName) {
|
|
1548
|
+
if (!search) return void 0;
|
|
1549
|
+
for (const part of search.slice(1).split("&")) {
|
|
1550
|
+
const equal = part.indexOf("=");
|
|
1551
|
+
if (equal <= 0) continue;
|
|
1552
|
+
try {
|
|
1553
|
+
if (decodeURIComponent(part.slice(0, equal)) === expectedName) return part.slice(equal + 1);
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return void 0;
|
|
1558
|
+
}
|
|
1559
|
+
function assertJsonValue(value) {
|
|
1560
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
1561
|
+
if (typeof value === "number") {
|
|
1562
|
+
if (Number.isFinite(value)) return;
|
|
1563
|
+
fail("Paja intent payload must be JSON");
|
|
1564
|
+
}
|
|
1565
|
+
if (Array.isArray(value)) {
|
|
1566
|
+
value.forEach(assertJsonValue);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
1570
|
+
fail("Paja intent payload must be JSON");
|
|
1571
|
+
}
|
|
1572
|
+
for (const [, nested] of Object.entries(value)) {
|
|
1573
|
+
assertJsonValue(nested);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
function parseJsonPayload(value) {
|
|
1577
|
+
let parsed;
|
|
1578
|
+
try {
|
|
1579
|
+
parsed = JSON.parse(value);
|
|
1580
|
+
} catch {
|
|
1581
|
+
return fail("Paja intent payload must be valid JSON");
|
|
1582
|
+
}
|
|
1583
|
+
assertJsonValue(parsed);
|
|
1584
|
+
return parsed;
|
|
1585
|
+
}
|
|
1586
|
+
function encodeJsonPayload(value) {
|
|
1587
|
+
assertJsonValue(value);
|
|
1588
|
+
const encoded = JSON.stringify(value);
|
|
1589
|
+
if (encoded === void 0) fail("Paja intent payload must be JSON");
|
|
1590
|
+
return encoded;
|
|
1591
|
+
}
|
|
1592
|
+
function pointerParameter(pointer) {
|
|
1593
|
+
if (pointer.startsWith("naddr")) return "naddr";
|
|
1594
|
+
if (pointer.startsWith("nevent")) return "nevent";
|
|
1595
|
+
return "pointer";
|
|
1596
|
+
}
|
|
1597
|
+
function assertLinkSize(url) {
|
|
1598
|
+
if (new TextEncoder().encode(url.href).length > PAJA_INTENT_LINK_MAX_BYTES) {
|
|
1599
|
+
fail(`Paja intent links must not exceed ${PAJA_INTENT_LINK_MAX_BYTES} UTF-8 bytes`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function parsePajaIntentLink(href) {
|
|
1603
|
+
let url;
|
|
1604
|
+
try {
|
|
1605
|
+
url = new URL(href);
|
|
1606
|
+
} catch {
|
|
1607
|
+
return fail("Paja intent link must be an absolute URL");
|
|
1608
|
+
}
|
|
1609
|
+
const rawIntent = rawOuterValue(url.search, "intent");
|
|
1610
|
+
if (rawIntent === void 0) {
|
|
1611
|
+
if (rawOuterValue(url.search, "payload") !== void 0) fail("Paja intent payload requires an intent URI");
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
if (url.hash) fail("Paja intent links do not support an outer fragment");
|
|
1615
|
+
assertLinkSize(url);
|
|
1616
|
+
const fields = parseOuterQuery(url.search);
|
|
1617
|
+
if (Object.keys(fields).some((field) => !["intent", "payload", "pointer", "naddr", "nevent"].includes(field))) {
|
|
1618
|
+
fail("Paja intent link contains unsupported fields");
|
|
1619
|
+
}
|
|
1620
|
+
const pointers = ["pointer", "naddr", "nevent"].filter((name) => hasOwn(fields, name));
|
|
1621
|
+
if (pointers.length > 1) fail("Paja intent link must not contain multiple pointer aliases");
|
|
1622
|
+
if (!hasOwn(fields, "intent") || !rawIntent || /[:/?#&=]/u.test(rawIntent) || !/%[0-9a-f]{2}/iu.test(rawIntent)) {
|
|
1623
|
+
fail("Paja intent URI must be completely percent-encoded");
|
|
1624
|
+
}
|
|
1625
|
+
const uri = fields.intent;
|
|
1626
|
+
const payload = hasOwn(fields, "payload") ? parseJsonPayload(fields.payload) : void 0;
|
|
1627
|
+
const request = normalizeIntentUri(uri, payload === void 0 ? void 0 : { payload });
|
|
1628
|
+
const pointer = pointers.length === 1 ? fields[pointers[0]] : void 0;
|
|
1629
|
+
if (pointer !== void 0 && pointer.length === 0) fail("Paja intent pointer must be non-empty text");
|
|
1630
|
+
if (pointer !== void 0 && request.handlerHint !== void 0) {
|
|
1631
|
+
fail("Paja intent link cannot combine a target pointer with a URI recommendation");
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
uri,
|
|
1635
|
+
...pointer === void 0 ? {} : { pointer },
|
|
1636
|
+
...payload === void 0 ? {} : { payload },
|
|
1637
|
+
request
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function createPajaIntentLink(descriptor, href) {
|
|
1641
|
+
if (!descriptor || typeof descriptor !== "object" || Array.isArray(descriptor)) fail("Paja intent link descriptor must be an object");
|
|
1642
|
+
if (typeof descriptor.uri !== "string") fail("Paja intent URI must be text");
|
|
1643
|
+
if (descriptor.pointer !== void 0 && (typeof descriptor.pointer !== "string" || descriptor.pointer.length === 0)) {
|
|
1644
|
+
fail("Paja intent pointer must be non-empty text");
|
|
1645
|
+
}
|
|
1646
|
+
const request = normalizeIntentUri(
|
|
1647
|
+
descriptor.uri,
|
|
1648
|
+
hasOwn(descriptor, "payload") ? { payload: descriptor.payload } : void 0
|
|
1649
|
+
);
|
|
1650
|
+
if (descriptor.pointer !== void 0 && request.handlerHint !== void 0) {
|
|
1651
|
+
fail("Paja intent link cannot combine a target pointer with a URI recommendation");
|
|
1652
|
+
}
|
|
1653
|
+
let url;
|
|
1654
|
+
try {
|
|
1655
|
+
url = new URL(href);
|
|
1656
|
+
} catch {
|
|
1657
|
+
return fail("Paja intent link must use an absolute base URL");
|
|
1658
|
+
}
|
|
1659
|
+
url.search = "";
|
|
1660
|
+
url.hash = "";
|
|
1661
|
+
const pairs = [`intent=${encodeURIComponent(descriptor.uri)}`];
|
|
1662
|
+
if (descriptor.pointer !== void 0) pairs.push(`${pointerParameter(descriptor.pointer)}=${encodeURIComponent(descriptor.pointer)}`);
|
|
1663
|
+
if (hasOwn(descriptor, "payload")) pairs.push(`payload=${encodeURIComponent(encodeJsonPayload(descriptor.payload))}`);
|
|
1664
|
+
url.search = `?${pairs.join("&")}`;
|
|
1665
|
+
assertLinkSize(url);
|
|
1666
|
+
return url.href;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/intent-launcher.ts
|
|
1670
|
+
import { resolveNapplet as resolveNapplet2 } from "@kehto/nip/5d";
|
|
1671
|
+
import { verifyEvent } from "nostr-tools/pure";
|
|
1672
|
+
import { neventEncode } from "nostr-tools/nip19";
|
|
1673
|
+
var LAUNCHER_HTML = `<!doctype html><meta charset="utf-8"><title>Paja intent launcher</title><script>let launched=false;addEventListener('message',(event)=>{if(launched||event.source!==parent)return;const data=event.data;if(!data||data.type!=='paja.intent.launch'||typeof data.uri!=='string'||typeof data.correlation!=='string')return;launched=true;window.napplet?.intent?.invoke(data.uri,data.options).then((result)=>parent.postMessage({type:'paja.intent.launcher.result',correlation:data.correlation,result},'*')).catch(()=>parent.postMessage({type:'paja.intent.launcher.result',correlation:data.correlation,result:{ok:false,error:'invoke rejected'}},'*'));});parent.postMessage({type:'paja.intent.launcher.ready'},'*');</script>`;
|
|
1674
|
+
var LAUNCHER_EVENT = {
|
|
1675
|
+
kind: 35129,
|
|
1676
|
+
created_at: 1760054400,
|
|
1677
|
+
content: "Paja verified intent launcher",
|
|
1678
|
+
tags: [
|
|
1679
|
+
["d", "paja-intent-launcher"],
|
|
1680
|
+
["x", "77cf3d2df847786177f4c16eb0b6ae1be6f5cd4e8d9be02a80bccd7c88e51bb8"],
|
|
1681
|
+
["R", "intent"],
|
|
1682
|
+
["title", "Paja intent launcher"]
|
|
1683
|
+
],
|
|
1684
|
+
pubkey: "24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c",
|
|
1685
|
+
id: "37a0b8b71857b161ea7a8749f4642915c9101258509be6d1ce978e07c683bfb6",
|
|
1686
|
+
sig: "eb8e92509894dd3d86a28111a62412cff86ec36f591e4f3f692b5058174add6ef64c7996d66aae38bbd887355b3338ed6a64b300c8241875619b2e12e87d7903"
|
|
1687
|
+
};
|
|
1688
|
+
async function resolvePajaIntentLauncher() {
|
|
1689
|
+
const event = { ...LAUNCHER_EVENT, tags: LAUNCHER_EVENT.tags.map((tag) => [...tag]) };
|
|
1690
|
+
if (!verifyEvent(event)) throw new Error("Paja intent launcher manifest signature is invalid");
|
|
1691
|
+
const resolved = await resolveNapplet2({
|
|
1692
|
+
event,
|
|
1693
|
+
fetchBlob: async (hash) => {
|
|
1694
|
+
if (hash !== event.tags[1]?.[1]) throw new Error("Paja intent launcher requested an unknown artifact");
|
|
1695
|
+
return new TextEncoder().encode(LAUNCHER_HTML);
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
return Object.freeze({
|
|
1699
|
+
pointer: Object.freeze({ type: "nevent", value: neventEncode({ id: event.id, author: event.pubkey, kind: event.kind }), id: event.id, author: event.pubkey, kind: event.kind, relays: Object.freeze([]) }),
|
|
1700
|
+
event: Object.freeze({ ...event, tags: Object.freeze(event.tags.map((tag) => Object.freeze([...tag]))) }),
|
|
1701
|
+
relays: Object.freeze([]),
|
|
1702
|
+
blossomServers: Object.freeze([]),
|
|
1703
|
+
dTag: resolved.dTag,
|
|
1704
|
+
aggregateHash: resolved.aggregateHash,
|
|
1705
|
+
indexHtml: resolved.indexHtml,
|
|
1706
|
+
manifest: resolved.manifest
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
function postPajaIntentLauncherMessage(frame, message) {
|
|
1710
|
+
if (!frame.contentWindow) throw new Error("Paja intent launcher frame is unavailable");
|
|
1711
|
+
frame.contentWindow.postMessage(message, "*");
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1515
1714
|
// src/server.ts
|
|
1516
1715
|
import { createServer } from "http";
|
|
1517
1716
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -1750,9 +1949,13 @@ export {
|
|
|
1750
1949
|
findRelativeAssetReferences,
|
|
1751
1950
|
InstalledNappletCatalog,
|
|
1752
1951
|
BrowserIntentController,
|
|
1952
|
+
parsePajaIntentLink,
|
|
1953
|
+
createPajaIntentLink,
|
|
1954
|
+
resolvePajaIntentLauncher,
|
|
1955
|
+
postPajaIntentLauncherMessage,
|
|
1753
1956
|
PAJA_TARGET_CORS_HINT,
|
|
1754
1957
|
classifyTargetCors,
|
|
1755
1958
|
probeTargetCors,
|
|
1756
1959
|
startPajaServer
|
|
1757
1960
|
};
|
|
1758
|
-
//# sourceMappingURL=chunk-
|
|
1961
|
+
//# sourceMappingURL=chunk-6WEO5D3Z.js.map
|