@kehto/paja 0.17.0 → 0.18.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 +128 -15
- package/dist/browser-host.js +16978 -16392
- package/dist/browser-host.js.map +1 -1
- package/dist/{chunk-ESE55K3Q.js → chunk-YX6C2UU7.js} +143 -24
- package/dist/chunk-YX6C2UU7.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +132 -4
- package/dist/index.js +13 -1
- package/package.json +5 -5
- package/dist/chunk-ESE55K3Q.js.map +0 -1
|
@@ -524,6 +524,7 @@ function renderPajaHtml(config) {
|
|
|
524
524
|
.tabs { display: flex; align-items: stretch; align-self: flex-end; gap: 4px; min-width: 120px; max-width: min(100%, 760px); overflow-x: auto; scrollbar-width: thin; }
|
|
525
525
|
.tabs:empty { display: none; }
|
|
526
526
|
.tab { min-width: 116px; max-width: 240px; height: 30px; display: grid; grid-template-columns: minmax(0, 1fr) 24px 24px; align-items: center; gap: 2px; border: 1px solid var(--line); border-bottom-color: transparent; background: #151815; color: var(--muted); border-radius: 5px 5px 0 0; padding: 0 2px 0 9px; }
|
|
527
|
+
.tab[data-source="local"] { grid-template-columns: minmax(0, 1fr) 24px; }
|
|
527
528
|
.tab[data-active="true"] { color: var(--text); border-color: var(--accent); border-bottom-color: #151815; background: #20241f; }
|
|
528
529
|
.tab-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; text-align: left; }
|
|
529
530
|
.tab-share, .tab-close { width: 20px; height: 20px; padding: 0; border: 0; background: transparent; color: var(--muted); display: inline-grid; place-items: center; }
|
|
@@ -533,9 +534,21 @@ function renderPajaHtml(config) {
|
|
|
533
534
|
label { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); white-space: nowrap; }
|
|
534
535
|
select, input, textarea { border: 1px solid var(--line); color: var(--text); background: #20241f; min-height: 26px; border-radius: 4px; font: inherit; }
|
|
535
536
|
main { min-height: 0; display: grid; grid-template-columns: var(--paja-console-column) minmax(0, 1fr); }
|
|
536
|
-
.console { min-height: 0; overflow: auto; border-right: 1px solid var(--line); background: #121512; padding:
|
|
537
|
-
.section {
|
|
537
|
+
.console { min-height: 0; overflow: auto; border-right: 1px solid var(--line); background: #121512; padding: 0; display: flex; flex-direction: column; gap: 0; }
|
|
538
|
+
.section { flex-shrink: 0; margin: 0; border-radius: 0; border-bottom: 1px solid var(--line); }
|
|
539
|
+
.section > summary { display: flex; align-items: center; justify-content: space-between; padding: 12px; cursor: pointer; list-style: none; }
|
|
540
|
+
.section > summary::-webkit-details-marker { display: none; }
|
|
541
|
+
.section > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
542
|
+
.section-chevron { transition: transform 120ms; }
|
|
543
|
+
.section[open] > summary .section-chevron { transform: rotate(90deg); }
|
|
544
|
+
.section-body { padding: 0 12px 12px; display: grid; gap: 8px; }
|
|
545
|
+
.resource-servers { display: grid; gap: 8px; }
|
|
546
|
+
.resource-servers > label { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
538
547
|
.section-title { color: var(--accent); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0; }
|
|
548
|
+
.resource-servers { min-width: 0; }
|
|
549
|
+
.resource-servers label { white-space: normal; }
|
|
550
|
+
.resource-servers textarea { width: 100%; min-width: 0; padding: 6px; resize: vertical; }
|
|
551
|
+
.resource-servers small, .resource-servers [role="status"] { color: var(--muted); overflow-wrap: anywhere; }
|
|
539
552
|
.switch-grid, .acl-grid { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
540
553
|
.toggle { height: 24px; padding: 0 8px; color: var(--muted); }
|
|
541
554
|
.toggle[data-enabled="true"] { color: var(--text); border-color: #5f724f; background: #24301f; }
|
|
@@ -545,7 +558,7 @@ function renderPajaHtml(config) {
|
|
|
545
558
|
.signer-controls button[data-active="true"] { border-color: var(--accent); color: var(--text); background: #2a2a1d; }
|
|
546
559
|
.signer-controls input { min-width: 0; padding: 0 8px; }
|
|
547
560
|
.signer-consent-clear { width: 100%; margin-top: 6px; }
|
|
548
|
-
.pointer-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; }
|
|
561
|
+
.pointer-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 6px; }
|
|
549
562
|
.pointer-status { min-width: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
550
563
|
.log-tools { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; }
|
|
551
564
|
.log-list { min-height: 160px; max-height: 38vh; overflow: auto; border: 1px solid var(--line); border-radius: 4px; background: #0b0d0b; }
|
|
@@ -559,6 +572,7 @@ function renderPajaHtml(config) {
|
|
|
559
572
|
.stage { min-width: 0; min-height: 0; position: relative; background: #050705; }
|
|
560
573
|
.empty-stage { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); font-size: 12px; }
|
|
561
574
|
.empty-stage[hidden] { display: none; }
|
|
575
|
+
.stage.drop-active { outline: 2px dashed var(--accent); outline-offset: -6px; }
|
|
562
576
|
.tab-panel { position: absolute; inset: 0; min-width: 0; min-height: 0; }
|
|
563
577
|
.tab-panel[hidden] { display: none; }
|
|
564
578
|
iframe { width: 100%; height: 100%; border: 0; background: white; display: block; }
|
|
@@ -644,28 +658,38 @@ function renderPajaHtml(config) {
|
|
|
644
658
|
<main>
|
|
645
659
|
<aside class="console" id="${PAJA_CONSOLE_PANEL_ID}" aria-label="Paja development controls">
|
|
646
660
|
${renderPointerControls(config)}
|
|
647
|
-
<
|
|
648
|
-
<
|
|
661
|
+
<details class="section" id="paja-section-interfaces" data-paja-section="interfaces" open>
|
|
662
|
+
<summary class="section-title">Interfaces<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
|
|
649
663
|
<div class="switch-grid" id="interface-toggles"></div>
|
|
650
|
-
</
|
|
651
|
-
<
|
|
652
|
-
<
|
|
664
|
+
</div></details>
|
|
665
|
+
<details class="section" id="paja-section-acl" data-paja-section="acl" open>
|
|
666
|
+
<summary class="section-title">ACL<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
|
|
653
667
|
<div class="acl-grid" id="acl-controls"></div>
|
|
654
|
-
</
|
|
655
|
-
<
|
|
656
|
-
<
|
|
668
|
+
</div></details>
|
|
669
|
+
<details class="section" id="paja-section-signer" data-paja-section="signer" open>
|
|
670
|
+
<summary class="section-title">Signer<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
|
|
657
671
|
<div class="signer" id="signer-status">loading</div>
|
|
658
672
|
<div class="signer-controls" id="signer-controls"></div>
|
|
659
673
|
<button type="button" class="signer-consent-clear" id="signer-consent-clear" hidden>Forget remembered approvals</button>
|
|
660
|
-
</
|
|
661
|
-
<
|
|
662
|
-
<
|
|
674
|
+
</div></details>
|
|
675
|
+
<details class="section" id="paja-section-resource-servers" data-paja-section="resource-servers" open>
|
|
676
|
+
<summary class="section-title">Resource servers<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
|
|
677
|
+
<form class="resource-servers" id="paja-resource-servers-form">
|
|
678
|
+
<label class="section-title" for="paja-resource-servers-input">Resource servers</label>
|
|
679
|
+
<textarea id="paja-resource-servers-input" rows="3" aria-describedby="paja-resource-servers-help paja-resource-servers-status" spellcheck="false"></textarea>
|
|
680
|
+
<small id="paja-resource-servers-help">Extra Blossom lookup servers, one per line. Domains use HTTPS.</small>
|
|
681
|
+
<button type="submit" id="paja-resource-servers-save">Save</button>
|
|
682
|
+
<div id="paja-resource-servers-status" role="status" aria-live="polite"></div>
|
|
683
|
+
</form>
|
|
684
|
+
</div></details>
|
|
685
|
+
<details class="section" id="paja-section-messages" data-paja-section="messages" open>
|
|
686
|
+
<summary class="section-title">Messages<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
|
|
663
687
|
<div class="log-tools">
|
|
664
688
|
<input id="message-filter" type="search" autocomplete="off" placeholder="filter messages" aria-label="Filter message log">
|
|
665
689
|
<button type="button" id="clear-log">Clear</button>
|
|
666
690
|
</div>
|
|
667
691
|
<div class="log-list" id="message-log" aria-live="polite"></div>
|
|
668
|
-
</
|
|
692
|
+
</div></details>
|
|
669
693
|
</aside>
|
|
670
694
|
${renderStage(config, targetLabel)}
|
|
671
695
|
</main>
|
|
@@ -733,7 +757,7 @@ function renderConfigDialog() {
|
|
|
733
757
|
function renderStage(config, targetLabel) {
|
|
734
758
|
if (config.target.mode === "runtime-pointer") {
|
|
735
759
|
return `<section class="stage" id="napplet-stage" aria-label="Loaded napplet runtimes">
|
|
736
|
-
<div class="empty-stage" id="empty-runtime-stage">Load a napplet pointer to start a runtime tab.</div>
|
|
760
|
+
<div class="empty-stage" id="empty-runtime-stage">Load a napplet pointer or drop an index.html to start a runtime tab.</div>
|
|
737
761
|
</section>`;
|
|
738
762
|
}
|
|
739
763
|
return `<section class="stage" id="napplet-stage">
|
|
@@ -755,14 +779,16 @@ function renderDuplicateDialog() {
|
|
|
755
779
|
function renderPointerControls(config) {
|
|
756
780
|
if (config.target.mode !== "runtime-pointer") return "";
|
|
757
781
|
const value = escapeAttribute(config.target.pointer?.value ?? "");
|
|
758
|
-
return `<
|
|
759
|
-
<
|
|
782
|
+
return `<details class="section" id="runtime-pointer-section" data-paja-section="pointer" open>
|
|
783
|
+
<summary class="section-title">Pointer<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
|
|
760
784
|
<form class="pointer-controls" id="runtime-pointer-form">
|
|
761
785
|
<input id="runtime-pointer-input" type="text" inputmode="url" autocomplete="off" spellcheck="false" placeholder="naddr or nevent" aria-label="Runtime napplet pointer" value="${value}">
|
|
762
786
|
<button type="submit" id="runtime-pointer-load">Load</button>
|
|
787
|
+
<button type="button" id="runtime-local-open" title="Open a self-contained single-file index.html">Open file\u2026</button>
|
|
788
|
+
<input id="runtime-local-file" type="file" accept=".html,.htm,text/html" aria-label="Local napplet index.html" hidden>
|
|
763
789
|
</form>
|
|
764
790
|
<div class="pointer-status" id="runtime-pointer-status" aria-live="polite">idle</div>
|
|
765
|
-
</
|
|
791
|
+
</div></details>`;
|
|
766
792
|
}
|
|
767
793
|
function getModeLabel(config) {
|
|
768
794
|
if (config.target.mode === "runtime-pointer") return "runtime-pointer";
|
|
@@ -867,7 +893,7 @@ async function resolvePajaPointer(value, options = {}) {
|
|
|
867
893
|
const resolved = await resolveNapplet({
|
|
868
894
|
event,
|
|
869
895
|
cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
|
|
870
|
-
fetchBlob: (
|
|
896
|
+
fetchBlob: (sha256Hex2, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex2, async (url) => {
|
|
871
897
|
const response = await (options.fetcher ?? fetch)(url);
|
|
872
898
|
if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
|
|
873
899
|
return new Uint8Array(await response.arrayBuffer());
|
|
@@ -1211,6 +1237,85 @@ function defaultSleep(ms) {
|
|
|
1211
1237
|
});
|
|
1212
1238
|
}
|
|
1213
1239
|
|
|
1240
|
+
// src/local-target.ts
|
|
1241
|
+
import { computeAggregateHash } from "@kehto/nip/5a";
|
|
1242
|
+
var PAJA_LOCAL_SINGLE_FILE_HINT = "Paja opens self-contained single-file index.html only; relative scripts, styles, and images do not load.";
|
|
1243
|
+
var MAX_DTAG_LENGTH = 128;
|
|
1244
|
+
var MAX_REPORTED_RELATIVE_ASSETS = 8;
|
|
1245
|
+
function isPajaLocalTarget(target) {
|
|
1246
|
+
return typeof target === "object" && target !== null && target.source === "local";
|
|
1247
|
+
}
|
|
1248
|
+
function isPajaLocalHtmlFile(file) {
|
|
1249
|
+
const type = (file.type ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1250
|
+
return type === "text/html" || /\.html?$/i.test(file.name.trim());
|
|
1251
|
+
}
|
|
1252
|
+
async function createPajaLocalTarget(file) {
|
|
1253
|
+
const fileName = file.name.trim() || "index.html";
|
|
1254
|
+
if (!isPajaLocalHtmlFile(file)) {
|
|
1255
|
+
throw new Error(`"${fileName}" is not an HTML file. ${PAJA_LOCAL_SINGLE_FILE_HINT}`);
|
|
1256
|
+
}
|
|
1257
|
+
const bytes = "arrayBuffer" in file ? new Uint8Array(await file.arrayBuffer()) : new TextEncoder().encode(file.text);
|
|
1258
|
+
const indexHtml = new TextDecoder().decode(bytes);
|
|
1259
|
+
if (indexHtml.trim().length === 0) {
|
|
1260
|
+
throw new Error(`"${fileName}" is empty.`);
|
|
1261
|
+
}
|
|
1262
|
+
const sha256 = await sha256Hex(bytes);
|
|
1263
|
+
return Object.freeze({
|
|
1264
|
+
source: "local",
|
|
1265
|
+
fileName,
|
|
1266
|
+
dTag: readNappletIdMeta(indexHtml) ?? localDTag(fileName),
|
|
1267
|
+
aggregateHash: computeAggregateHash([{ path: "/index.html", sha256 }]),
|
|
1268
|
+
sha256,
|
|
1269
|
+
indexHtml,
|
|
1270
|
+
relays: Object.freeze([]),
|
|
1271
|
+
blossomServers: Object.freeze([]),
|
|
1272
|
+
relativeAssets: Object.freeze(findRelativeAssetReferences(indexHtml))
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
function readNappletIdMeta(html) {
|
|
1276
|
+
for (const match of html.matchAll(/<meta\b([^>]*)>/gi)) {
|
|
1277
|
+
const attributes = parseAttributes(match[1] ?? "");
|
|
1278
|
+
if (attributes.get("name")?.toLowerCase() !== "napplet-id") continue;
|
|
1279
|
+
const value = attributes.get("content")?.trim();
|
|
1280
|
+
if (value) return value.slice(0, MAX_DTAG_LENGTH);
|
|
1281
|
+
}
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
function findRelativeAssetReferences(html) {
|
|
1285
|
+
const found = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const match of html.matchAll(/<(?:script|link|img|source|iframe)\b([^>]*)>/gi)) {
|
|
1287
|
+
const attributes = parseAttributes(match[1] ?? "");
|
|
1288
|
+
const reference = attributes.get("src") ?? attributes.get("href");
|
|
1289
|
+
if (reference && isRelativeReference(reference)) found.add(reference.trim());
|
|
1290
|
+
if (found.size >= MAX_REPORTED_RELATIVE_ASSETS) break;
|
|
1291
|
+
}
|
|
1292
|
+
return [...found];
|
|
1293
|
+
}
|
|
1294
|
+
function isRelativeReference(value) {
|
|
1295
|
+
const trimmed = value.trim();
|
|
1296
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) return false;
|
|
1297
|
+
return !/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(trimmed);
|
|
1298
|
+
}
|
|
1299
|
+
function parseAttributes(source) {
|
|
1300
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1301
|
+
const pattern = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
1302
|
+
for (const match of source.matchAll(pattern)) {
|
|
1303
|
+
const name = match[1]?.toLowerCase();
|
|
1304
|
+
if (!name || attributes.has(name)) continue;
|
|
1305
|
+
attributes.set(name, match[2] ?? match[3] ?? match[4] ?? "");
|
|
1306
|
+
}
|
|
1307
|
+
return attributes;
|
|
1308
|
+
}
|
|
1309
|
+
function localDTag(fileName) {
|
|
1310
|
+
const stem = fileName.replace(/\.[^.]*$/, "");
|
|
1311
|
+
const slug = stem.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1312
|
+
return `local-${slug || "napplet"}`.slice(0, MAX_DTAG_LENGTH);
|
|
1313
|
+
}
|
|
1314
|
+
async function sha256Hex(bytes) {
|
|
1315
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1316
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1214
1319
|
// src/installed-napplet-catalog.ts
|
|
1215
1320
|
import { manifestToIntentCatalogEntry } from "@kehto/services";
|
|
1216
1321
|
function matchesInstalledNappletRecord(record, target) {
|
|
@@ -1227,11 +1332,13 @@ var InstalledNappletCatalog = class {
|
|
|
1227
1332
|
pointer: copyPointer(resolved.pointer),
|
|
1228
1333
|
...resolved.manifest.title === void 0 ? {} : { title: resolved.manifest.title },
|
|
1229
1334
|
requires: [...resolved.manifest.requires],
|
|
1335
|
+
optional: [...resolved.manifest.optional ?? []],
|
|
1230
1336
|
archetypes: resolved.manifest.archetypes.map((archetype) => ({
|
|
1231
1337
|
slug: archetype.slug,
|
|
1232
1338
|
convention: archetype.convention
|
|
1233
1339
|
}))
|
|
1234
1340
|
});
|
|
1341
|
+
if (!record.dTag) return record;
|
|
1235
1342
|
this.records.set(record.dTag, record);
|
|
1236
1343
|
this.notify(record.dTag);
|
|
1237
1344
|
return record;
|
|
@@ -1263,9 +1370,14 @@ var InstalledNappletCatalog = class {
|
|
|
1263
1370
|
if (this.records.get(selected.dTag) !== selected) return null;
|
|
1264
1371
|
return matchesInstalledNappletRecord(selected, target) ? selected : null;
|
|
1265
1372
|
}
|
|
1266
|
-
/**
|
|
1267
|
-
|
|
1268
|
-
|
|
1373
|
+
/**
|
|
1374
|
+
* Return declared handlers whose host environment can receive intent delivery.
|
|
1375
|
+
* @param canReceiveIntent - Resolve intent availability for each verified identity;
|
|
1376
|
+
* without host policy, return declared candidates for the host to filter.
|
|
1377
|
+
* @returns Named intent candidates usable under the supplied host policy.
|
|
1378
|
+
*/
|
|
1379
|
+
intentCatalog(canReceiveIntent = () => true) {
|
|
1380
|
+
return this.installed().filter(canReceiveIntent).map((record) => manifestToIntentCatalogEntry({
|
|
1269
1381
|
dTag: record.dTag,
|
|
1270
1382
|
...record.title === void 0 ? {} : { title: record.title },
|
|
1271
1383
|
archetypes: record.archetypes.map((archetype) => ({
|
|
@@ -1307,6 +1419,7 @@ function freezeRecord(record) {
|
|
|
1307
1419
|
return Object.freeze({
|
|
1308
1420
|
...record,
|
|
1309
1421
|
requires: Object.freeze([...record.requires]),
|
|
1422
|
+
optional: Object.freeze([...record.optional ?? []]),
|
|
1310
1423
|
archetypes: Object.freeze(record.archetypes.map((archetype) => Object.freeze({ ...archetype })))
|
|
1311
1424
|
});
|
|
1312
1425
|
}
|
|
@@ -1629,6 +1742,12 @@ export {
|
|
|
1629
1742
|
getMissingServices,
|
|
1630
1743
|
ReadinessError,
|
|
1631
1744
|
waitForTargetUrl,
|
|
1745
|
+
PAJA_LOCAL_SINGLE_FILE_HINT,
|
|
1746
|
+
isPajaLocalTarget,
|
|
1747
|
+
isPajaLocalHtmlFile,
|
|
1748
|
+
createPajaLocalTarget,
|
|
1749
|
+
readNappletIdMeta,
|
|
1750
|
+
findRelativeAssetReferences,
|
|
1632
1751
|
InstalledNappletCatalog,
|
|
1633
1752
|
BrowserIntentController,
|
|
1634
1753
|
PAJA_TARGET_CORS_HINT,
|
|
@@ -1636,4 +1755,4 @@ export {
|
|
|
1636
1755
|
probeTargetCors,
|
|
1637
1756
|
startPajaServer
|
|
1638
1757
|
};
|
|
1639
|
-
//# sourceMappingURL=chunk-
|
|
1758
|
+
//# sourceMappingURL=chunk-YX6C2UU7.js.map
|