@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.
@@ -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: 10px; display: flex; flex-direction: column; gap: 12px; }
537
- .section { display: grid; gap: 8px; }
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
- <section class="section">
648
- <div class="section-title">Interfaces</div>
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
- </section>
651
- <section class="section">
652
- <div class="section-title">ACL</div>
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
- </section>
655
- <section class="section">
656
- <div class="section-title">Signer</div>
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
- </section>
661
- <section class="section">
662
- <div class="section-title">Messages</div>
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
- </section>
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 `<section class="section" id="runtime-pointer-section">
759
- <div class="section-title">Pointer</div>
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
- </section>`;
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: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
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
- /** Return exact handler candidates derived only from installed manifests. */
1267
- intentCatalog() {
1268
- return this.installed().filter((record) => record.requires.includes("inc")).map((record) => manifestToIntentCatalogEntry({
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-ESE55K3Q.js.map
1758
+ //# sourceMappingURL=chunk-YX6C2UU7.js.map