@kehto/paja 0.17.1 → 0.19.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.
@@ -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",
@@ -524,6 +525,7 @@ function renderPajaHtml(config) {
524
525
  .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
526
  .tabs:empty { display: none; }
526
527
  .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; }
528
+ .tab[data-source="local"] { grid-template-columns: minmax(0, 1fr) 24px; }
527
529
  .tab[data-active="true"] { color: var(--text); border-color: var(--accent); border-bottom-color: #151815; background: #20241f; }
528
530
  .tab-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; text-align: left; }
529
531
  .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 +535,21 @@ function renderPajaHtml(config) {
533
535
  label { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); white-space: nowrap; }
534
536
  select, input, textarea { border: 1px solid var(--line); color: var(--text); background: #20241f; min-height: 26px; border-radius: 4px; font: inherit; }
535
537
  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: 10px; display: flex; flex-direction: column; gap: 12px; }
537
- .section { display: grid; gap: 8px; }
538
+ .console { min-height: 0; overflow: auto; border-right: 1px solid var(--line); background: #121512; padding: 0; display: flex; flex-direction: column; gap: 0; }
539
+ .section { flex-shrink: 0; margin: 0; border-radius: 0; border-bottom: 1px solid var(--line); }
540
+ .section > summary { display: flex; align-items: center; justify-content: space-between; padding: 12px; cursor: pointer; list-style: none; }
541
+ .section > summary::-webkit-details-marker { display: none; }
542
+ .section > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
543
+ .section-chevron { transition: transform 120ms; }
544
+ .section[open] > summary .section-chevron { transform: rotate(90deg); }
545
+ .section-body { padding: 0 12px 12px; display: grid; gap: 8px; }
546
+ .resource-servers { display: grid; gap: 8px; }
547
+ .resource-servers > label { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
538
548
  .section-title { color: var(--accent); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0; }
549
+ .resource-servers { min-width: 0; }
550
+ .resource-servers label { white-space: normal; }
551
+ .resource-servers textarea { width: 100%; min-width: 0; padding: 6px; resize: vertical; }
552
+ .resource-servers small, .resource-servers [role="status"] { color: var(--muted); overflow-wrap: anywhere; }
539
553
  .switch-grid, .acl-grid { display: flex; flex-wrap: wrap; gap: 6px; }
540
554
  .toggle { height: 24px; padding: 0 8px; color: var(--muted); }
541
555
  .toggle[data-enabled="true"] { color: var(--text); border-color: #5f724f; background: #24301f; }
@@ -545,7 +559,7 @@ function renderPajaHtml(config) {
545
559
  .signer-controls button[data-active="true"] { border-color: var(--accent); color: var(--text); background: #2a2a1d; }
546
560
  .signer-controls input { min-width: 0; padding: 0 8px; }
547
561
  .signer-consent-clear { width: 100%; margin-top: 6px; }
548
- .pointer-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; }
562
+ .pointer-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 6px; }
549
563
  .pointer-status { min-width: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
550
564
  .log-tools { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; }
551
565
  .log-list { min-height: 160px; max-height: 38vh; overflow: auto; border: 1px solid var(--line); border-radius: 4px; background: #0b0d0b; }
@@ -559,6 +573,7 @@ function renderPajaHtml(config) {
559
573
  .stage { min-width: 0; min-height: 0; position: relative; background: #050705; }
560
574
  .empty-stage { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); font-size: 12px; }
561
575
  .empty-stage[hidden] { display: none; }
576
+ .stage.drop-active { outline: 2px dashed var(--accent); outline-offset: -6px; }
562
577
  .tab-panel { position: absolute; inset: 0; min-width: 0; min-height: 0; }
563
578
  .tab-panel[hidden] { display: none; }
564
579
  iframe { width: 100%; height: 100%; border: 0; background: white; display: block; }
@@ -644,28 +659,38 @@ function renderPajaHtml(config) {
644
659
  <main>
645
660
  <aside class="console" id="${PAJA_CONSOLE_PANEL_ID}" aria-label="Paja development controls">
646
661
  ${renderPointerControls(config)}
647
- <section class="section">
648
- <div class="section-title">Interfaces</div>
662
+ <details class="section" id="paja-section-interfaces" data-paja-section="interfaces" open>
663
+ <summary class="section-title">Interfaces<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
649
664
  <div class="switch-grid" id="interface-toggles"></div>
650
- </section>
651
- <section class="section">
652
- <div class="section-title">ACL</div>
665
+ </div></details>
666
+ <details class="section" id="paja-section-acl" data-paja-section="acl" open>
667
+ <summary class="section-title">ACL<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
653
668
  <div class="acl-grid" id="acl-controls"></div>
654
- </section>
655
- <section class="section">
656
- <div class="section-title">Signer</div>
669
+ </div></details>
670
+ <details class="section" id="paja-section-signer" data-paja-section="signer" open>
671
+ <summary class="section-title">Signer<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
657
672
  <div class="signer" id="signer-status">loading</div>
658
673
  <div class="signer-controls" id="signer-controls"></div>
659
674
  <button type="button" class="signer-consent-clear" id="signer-consent-clear" hidden>Forget remembered approvals</button>
660
- </section>
661
- <section class="section">
662
- <div class="section-title">Messages</div>
675
+ </div></details>
676
+ <details class="section" id="paja-section-resource-servers" data-paja-section="resource-servers" open>
677
+ <summary class="section-title">Resource servers<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
678
+ <form class="resource-servers" id="paja-resource-servers-form">
679
+ <label class="section-title" for="paja-resource-servers-input">Resource servers</label>
680
+ <textarea id="paja-resource-servers-input" rows="3" aria-describedby="paja-resource-servers-help paja-resource-servers-status" spellcheck="false"></textarea>
681
+ <small id="paja-resource-servers-help">Extra Blossom lookup servers, one per line. Domains use HTTPS.</small>
682
+ <button type="submit" id="paja-resource-servers-save">Save</button>
683
+ <div id="paja-resource-servers-status" role="status" aria-live="polite"></div>
684
+ </form>
685
+ </div></details>
686
+ <details class="section" id="paja-section-messages" data-paja-section="messages" open>
687
+ <summary class="section-title">Messages<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
663
688
  <div class="log-tools">
664
689
  <input id="message-filter" type="search" autocomplete="off" placeholder="filter messages" aria-label="Filter message log">
665
690
  <button type="button" id="clear-log">Clear</button>
666
691
  </div>
667
692
  <div class="log-list" id="message-log" aria-live="polite"></div>
668
- </section>
693
+ </div></details>
669
694
  </aside>
670
695
  ${renderStage(config, targetLabel)}
671
696
  </main>
@@ -733,7 +758,7 @@ function renderConfigDialog() {
733
758
  function renderStage(config, targetLabel) {
734
759
  if (config.target.mode === "runtime-pointer") {
735
760
  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>
761
+ <div class="empty-stage" id="empty-runtime-stage">Load a napplet pointer or drop an index.html to start a runtime tab.</div>
737
762
  </section>`;
738
763
  }
739
764
  return `<section class="stage" id="napplet-stage">
@@ -755,14 +780,16 @@ function renderDuplicateDialog() {
755
780
  function renderPointerControls(config) {
756
781
  if (config.target.mode !== "runtime-pointer") return "";
757
782
  const value = escapeAttribute(config.target.pointer?.value ?? "");
758
- return `<section class="section" id="runtime-pointer-section">
759
- <div class="section-title">Pointer</div>
783
+ return `<details class="section" id="runtime-pointer-section" data-paja-section="pointer" open>
784
+ <summary class="section-title">Pointer<span class="section-chevron" aria-hidden="true">\u203A</span></summary><div class="section-body">
760
785
  <form class="pointer-controls" id="runtime-pointer-form">
761
786
  <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
787
  <button type="submit" id="runtime-pointer-load">Load</button>
788
+ <button type="button" id="runtime-local-open" title="Open a self-contained single-file index.html">Open file\u2026</button>
789
+ <input id="runtime-local-file" type="file" accept=".html,.htm,text/html" aria-label="Local napplet index.html" hidden>
763
790
  </form>
764
791
  <div class="pointer-status" id="runtime-pointer-status" aria-live="polite">idle</div>
765
- </section>`;
792
+ </div></details>`;
766
793
  }
767
794
  function getModeLabel(config) {
768
795
  if (config.target.mode === "runtime-pointer") return "runtime-pointer";
@@ -785,6 +812,7 @@ function escapeJsonForHtml(value) {
785
812
  }
786
813
 
787
814
  // src/runtime-resolver.ts
815
+ import { injectNappletCsp } from "@kehto/shell";
788
816
  import { SimplePool } from "nostr-tools/pool";
789
817
  import { decode } from "nostr-tools/nip19";
790
818
  import {
@@ -867,7 +895,7 @@ async function resolvePajaPointer(value, options = {}) {
867
895
  const resolved = await resolveNapplet({
868
896
  event,
869
897
  cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
870
- fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
898
+ fetchBlob: (sha256Hex2, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex2, async (url) => {
871
899
  const response = await (options.fetcher ?? fetch)(url);
872
900
  if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
873
901
  return new Uint8Array(await response.arrayBuffer());
@@ -905,34 +933,7 @@ function resolvePointerBlossomServers(event, configured) {
905
933
  return servers;
906
934
  }
907
935
  function injectPajaRuntimeCsp(html, origins) {
908
- const grantedOrigins = [...new Set(origins)].sort();
909
- const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
910
- const value = [
911
- "default-src 'none'",
912
- "script-src 'unsafe-inline' 'wasm-unsafe-eval'",
913
- "style-src 'unsafe-inline'",
914
- "img-src data: blob:",
915
- "font-src data:",
916
- connectSrc,
917
- "worker-src 'none'",
918
- "child-src 'none'",
919
- "frame-src 'none'",
920
- "media-src 'none'",
921
- "object-src 'none'",
922
- "manifest-src 'none'",
923
- "prefetch-src 'none'",
924
- "base-uri 'none'",
925
- "form-action 'none'",
926
- "frame-ancestors 'self'"
927
- ].join("; ");
928
- const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute2(value)}">`;
929
- if (/<head[^>]*>/i.test(html)) {
930
- return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
931
- }
932
- if (/<html[^>]*>/i.test(html)) {
933
- return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
934
- }
935
- return `${meta}${html}`;
936
+ return injectNappletCsp(html, { connectOrigins: origins });
936
937
  }
937
938
  async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
938
939
  const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
@@ -1078,9 +1079,6 @@ function normalizeRelayUrl(value) {
1078
1079
  return void 0;
1079
1080
  }
1080
1081
  }
1081
- function escapeAttribute2(value) {
1082
- return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1083
- }
1084
1082
  function formatNappletManifestKinds() {
1085
1083
  return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
1086
1084
  }
@@ -1211,6 +1209,85 @@ function defaultSleep(ms) {
1211
1209
  });
1212
1210
  }
1213
1211
 
1212
+ // src/local-target.ts
1213
+ import { computeAggregateHash } from "@kehto/nip/5a";
1214
+ var PAJA_LOCAL_SINGLE_FILE_HINT = "Paja opens self-contained single-file index.html only; relative scripts, styles, and images do not load.";
1215
+ var MAX_DTAG_LENGTH = 128;
1216
+ var MAX_REPORTED_RELATIVE_ASSETS = 8;
1217
+ function isPajaLocalTarget(target) {
1218
+ return typeof target === "object" && target !== null && target.source === "local";
1219
+ }
1220
+ function isPajaLocalHtmlFile(file) {
1221
+ const type = (file.type ?? "").split(";")[0]?.trim().toLowerCase();
1222
+ return type === "text/html" || /\.html?$/i.test(file.name.trim());
1223
+ }
1224
+ async function createPajaLocalTarget(file) {
1225
+ const fileName = file.name.trim() || "index.html";
1226
+ if (!isPajaLocalHtmlFile(file)) {
1227
+ throw new Error(`"${fileName}" is not an HTML file. ${PAJA_LOCAL_SINGLE_FILE_HINT}`);
1228
+ }
1229
+ const bytes = "arrayBuffer" in file ? new Uint8Array(await file.arrayBuffer()) : new TextEncoder().encode(file.text);
1230
+ const indexHtml = new TextDecoder().decode(bytes);
1231
+ if (indexHtml.trim().length === 0) {
1232
+ throw new Error(`"${fileName}" is empty.`);
1233
+ }
1234
+ const sha256 = await sha256Hex(bytes);
1235
+ return Object.freeze({
1236
+ source: "local",
1237
+ fileName,
1238
+ dTag: readNappletIdMeta(indexHtml) ?? localDTag(fileName),
1239
+ aggregateHash: computeAggregateHash([{ path: "/index.html", sha256 }]),
1240
+ sha256,
1241
+ indexHtml,
1242
+ relays: Object.freeze([]),
1243
+ blossomServers: Object.freeze([]),
1244
+ relativeAssets: Object.freeze(findRelativeAssetReferences(indexHtml))
1245
+ });
1246
+ }
1247
+ function readNappletIdMeta(html) {
1248
+ for (const match of html.matchAll(/<meta\b([^>]*)>/gi)) {
1249
+ const attributes = parseAttributes(match[1] ?? "");
1250
+ if (attributes.get("name")?.toLowerCase() !== "napplet-id") continue;
1251
+ const value = attributes.get("content")?.trim();
1252
+ if (value) return value.slice(0, MAX_DTAG_LENGTH);
1253
+ }
1254
+ return void 0;
1255
+ }
1256
+ function findRelativeAssetReferences(html) {
1257
+ const found = /* @__PURE__ */ new Set();
1258
+ for (const match of html.matchAll(/<(?:script|link|img|source|iframe)\b([^>]*)>/gi)) {
1259
+ const attributes = parseAttributes(match[1] ?? "");
1260
+ const reference = attributes.get("src") ?? attributes.get("href");
1261
+ if (reference && isRelativeReference(reference)) found.add(reference.trim());
1262
+ if (found.size >= MAX_REPORTED_RELATIVE_ASSETS) break;
1263
+ }
1264
+ return [...found];
1265
+ }
1266
+ function isRelativeReference(value) {
1267
+ const trimmed = value.trim();
1268
+ if (trimmed.length === 0 || trimmed.startsWith("#")) return false;
1269
+ return !/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(trimmed);
1270
+ }
1271
+ function parseAttributes(source) {
1272
+ const attributes = /* @__PURE__ */ new Map();
1273
+ const pattern = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
1274
+ for (const match of source.matchAll(pattern)) {
1275
+ const name = match[1]?.toLowerCase();
1276
+ if (!name || attributes.has(name)) continue;
1277
+ attributes.set(name, match[2] ?? match[3] ?? match[4] ?? "");
1278
+ }
1279
+ return attributes;
1280
+ }
1281
+ function localDTag(fileName) {
1282
+ const stem = fileName.replace(/\.[^.]*$/, "");
1283
+ const slug = stem.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1284
+ return `local-${slug || "napplet"}`.slice(0, MAX_DTAG_LENGTH);
1285
+ }
1286
+ async function sha256Hex(bytes) {
1287
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
1288
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
1289
+ }
1290
+
1214
1291
  // src/installed-napplet-catalog.ts
1215
1292
  import { manifestToIntentCatalogEntry } from "@kehto/services";
1216
1293
  function matchesInstalledNappletRecord(record, target) {
@@ -1227,11 +1304,13 @@ var InstalledNappletCatalog = class {
1227
1304
  pointer: copyPointer(resolved.pointer),
1228
1305
  ...resolved.manifest.title === void 0 ? {} : { title: resolved.manifest.title },
1229
1306
  requires: [...resolved.manifest.requires],
1307
+ optional: [...resolved.manifest.optional ?? []],
1230
1308
  archetypes: resolved.manifest.archetypes.map((archetype) => ({
1231
1309
  slug: archetype.slug,
1232
1310
  convention: archetype.convention
1233
1311
  }))
1234
1312
  });
1313
+ if (!record.dTag) return record;
1235
1314
  this.records.set(record.dTag, record);
1236
1315
  this.notify(record.dTag);
1237
1316
  return record;
@@ -1263,9 +1342,14 @@ var InstalledNappletCatalog = class {
1263
1342
  if (this.records.get(selected.dTag) !== selected) return null;
1264
1343
  return matchesInstalledNappletRecord(selected, target) ? selected : null;
1265
1344
  }
1266
- /** Return exact handler candidates derived only from installed manifests. */
1267
- intentCatalog() {
1268
- return this.installed().filter((record) => record.requires.includes("inc")).map((record) => manifestToIntentCatalogEntry({
1345
+ /**
1346
+ * Return declared handlers whose host environment can receive intent delivery.
1347
+ * @param canReceiveIntent - Resolve intent availability for each verified identity;
1348
+ * without host policy, return declared candidates for the host to filter.
1349
+ * @returns Named intent candidates usable under the supplied host policy.
1350
+ */
1351
+ intentCatalog(canReceiveIntent = () => true) {
1352
+ return this.installed().filter(canReceiveIntent).map((record) => manifestToIntentCatalogEntry({
1269
1353
  dTag: record.dTag,
1270
1354
  ...record.title === void 0 ? {} : { title: record.title },
1271
1355
  archetypes: record.archetypes.map((archetype) => ({
@@ -1307,6 +1391,7 @@ function freezeRecord(record) {
1307
1391
  return Object.freeze({
1308
1392
  ...record,
1309
1393
  requires: Object.freeze([...record.requires]),
1394
+ optional: Object.freeze([...record.optional ?? []]),
1310
1395
  archetypes: Object.freeze(record.archetypes.map((archetype) => Object.freeze({ ...archetype })))
1311
1396
  });
1312
1397
  }
@@ -1629,6 +1714,12 @@ export {
1629
1714
  getMissingServices,
1630
1715
  ReadinessError,
1631
1716
  waitForTargetUrl,
1717
+ PAJA_LOCAL_SINGLE_FILE_HINT,
1718
+ isPajaLocalTarget,
1719
+ isPajaLocalHtmlFile,
1720
+ createPajaLocalTarget,
1721
+ readNappletIdMeta,
1722
+ findRelativeAssetReferences,
1632
1723
  InstalledNappletCatalog,
1633
1724
  BrowserIntentController,
1634
1725
  PAJA_TARGET_CORS_HINT,
@@ -1636,4 +1727,4 @@ export {
1636
1727
  probeTargetCors,
1637
1728
  startPajaServer
1638
1729
  };
1639
- //# sourceMappingURL=chunk-ESE55K3Q.js.map
1730
+ //# sourceMappingURL=chunk-GY5ET3PC.js.map