@kehto/paja 0.20.0 → 0.20.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 CHANGED
@@ -482,7 +482,7 @@ Parameters start omitted; selecting Include with an empty field sends an explici
482
482
  empty string. Names stay in advertised order and extra named fields are allowed.
483
483
  No parameter type or requiredness is inferred. An empty advertised list still
484
484
  permits a payload. Opening or editing the builder never invokes an intent.
485
- Use **Copy link** or explicitly **Test intent** to open the review view.
485
+ Use **Copy link** to share it, or **Test intent** to open the shared content.
486
486
 
487
487
  The outer URL carries a complete percent-encoded convention URI in `intent`.
488
488
  For example, `?intent=napplet%3Aprofile%2Fopen%3Fpubkey%3Dabc%252B123`
@@ -493,17 +493,27 @@ cannot accompany an inner query. URLs are limited to 16 KiB UTF-8. Duplicate
493
493
  keys, malformed encoding, and conflicting target instructions are rejected.
494
494
  Ordinary pointer-only app links retain their existing behavior.
495
495
 
496
- Choose recipient default routing, a named-35129 `#naddr` recommendation, or an
497
- exact outer `pointer`. Explicit pointers must verify and advertise the exact
498
- contract; failure never silently falls back. Otherwise an applicable user default
499
- wins before a recommendation, then ordinary compatible selection applies.
500
- Recommendation relay hints remain untrusted discovery inputs. Installing a
501
- recommendation requires confirmation after signed metadata and bytes verify.
502
-
503
- Incoming links show **Review intent** before execution. Edit, choose another
504
- handler, explicitly save or clear a default, launch, retry, or cancel. Defaults
505
- never change implicitly. Cancellation before Launch sends nothing; accepted
506
- work is retained while completion or failure remains visible in the host.
496
+ New links recommend the app using a named-35129 `#naddr` when available;
497
+ snapshot or unnamed apps use an exact outer `pointer`. The builder also offers
498
+ recipient-default routing. Explicit pointers must verify and advertise the exact
499
+ contract; failure never silently falls back. Recommendation relay hints remain
500
+ untrusted discovery inputs. Invalid, incompatible, or disallowed recommendations
501
+ do not override an applicable saved default.
502
+
503
+ Opening a shared link opens its verified app and delivers the content directly,
504
+ following Paja's ordinary pointer navigation and capability policy. No URI or
505
+ payload review is shown. If an applicable saved default conflicts with the
506
+ link's app, Paja names both apps and asks which to use. A one-time choice leaves
507
+ the default unchanged; **Always use this app** explicitly saves the chosen app.
508
+ Without a usable hint or default, ordinary installed-handler selection applies.
509
+ Installed app pointers are saved separately from open tabs and reverified when
510
+ loaded; cached pointers never authorize execution or supply trusted metadata.
511
+
512
+ Progress and failures use a small notice. Successful delivery dismisses it.
513
+ Closing the notice or cancelling an app choice before dispatch sends nothing.
514
+ After acceptance, the incoming intent is removed from the current URL so reload
515
+ restores the app without delivering the shared content again. Accepted work is
516
+ retained until the verified target is ready.
507
517
 
508
518
  Paja invokes through a separate signed launcher napplet with a verified artifact
509
519
  and authenticated iframe endpoint. Its source supplies the opaque sender; the
@@ -14497,12 +14497,12 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
14497
14497
  }
14498
14498
  return Object.freeze(strings);
14499
14499
  }
14500
- function supports(domain) {
14500
+ function supports2(domain) {
14501
14501
  return typeof domain === "string" && environment?.capabilities.domains.includes(domain) === true;
14502
14502
  }
14503
14503
  const shell2 = {
14504
14504
  ready: () => ready,
14505
- supports,
14505
+ supports: supports2,
14506
14506
  get services() {
14507
14507
  return environment?.services ?? emptyServices;
14508
14508
  },
@@ -23732,6 +23732,21 @@ function createPajaThemeBroadcastLink() {
23732
23732
 
23733
23733
  // src/browser-host-storage.ts
23734
23734
  var PAJA_INTENT_DEFAULTS_STORAGE_KEY = "kehto.paja.intent-defaults.v1";
23735
+ var PAJA_INTENT_CATALOG_STORAGE_KEY = "kehto.paja.intent-catalog.v1";
23736
+ function readPajaIntentCatalogPointers() {
23737
+ try {
23738
+ const value = JSON.parse(window.localStorage.getItem(PAJA_INTENT_CATALOG_STORAGE_KEY) ?? "[]");
23739
+ return Array.isArray(value) ? value.filter((pointer) => typeof pointer === "string" && pointer.length > 0) : [];
23740
+ } catch {
23741
+ return [];
23742
+ }
23743
+ }
23744
+ function writePajaIntentCatalogPointers(pointers) {
23745
+ try {
23746
+ window.localStorage.setItem(PAJA_INTENT_CATALOG_STORAGE_KEY, JSON.stringify([...new Set(pointers)]));
23747
+ } catch {
23748
+ }
23749
+ }
23735
23750
  function runtimeTabsStorage(config) {
23736
23751
  if (config.target.mode !== "runtime-pointer") return null;
23737
23752
  try {
@@ -23957,193 +23972,139 @@ function createPajaIntentLink(descriptor, href) {
23957
23972
  }
23958
23973
 
23959
23974
  // src/browser-intent-links.ts
23960
- function createPajaIntentLinkReviewController(document2 = window.document, options = {}) {
23975
+ function createPajaIntentLinkController(document2 = window.document, options = {}) {
23961
23976
  const dialog = document2.getElementById("paja-intent-link-dialog");
23962
- const uri = document2.getElementById("paja-intent-link-uri");
23963
- const payload = document2.getElementById("paja-intent-link-payload");
23964
- const target = document2.getElementById("paja-intent-link-target");
23965
- const status = document2.getElementById("paja-intent-link-status");
23977
+ const title = document2.getElementById("paja-intent-link-title");
23978
+ const copy = document2.getElementById("paja-intent-link-copy");
23979
+ const apps = document2.getElementById("paja-intent-link-apps");
23980
+ const remember = document2.getElementById("paja-intent-link-save-default");
23966
23981
  const cancel = document2.getElementById("paja-intent-link-cancel");
23967
- const launch = document2.getElementById("paja-intent-link-launch");
23982
+ const notice = document2.getElementById("paja-intent-link-notice");
23983
+ const status = document2.getElementById("paja-intent-link-status");
23968
23984
  const retry = document2.getElementById("paja-intent-link-retry");
23969
- const choose2 = document2.getElementById("paja-intent-link-choose");
23970
- const handler = document2.getElementById("paja-intent-link-handler");
23971
- const useHandler = document2.getElementById("paja-intent-link-use-handler");
23972
- const saveDefault = document2.getElementById("paja-intent-link-save-default");
23973
- const clearDefault = document2.getElementById("paja-intent-link-clear-default");
23974
- if (!(dialog instanceof HTMLDialogElement) || !(uri instanceof HTMLInputElement) || !(payload instanceof HTMLTextAreaElement) || !(target instanceof HTMLElement) || !(status instanceof HTMLElement) || !(cancel instanceof HTMLButtonElement) || !(launch instanceof HTMLButtonElement) || !(retry instanceof HTMLButtonElement) || !(choose2 instanceof HTMLButtonElement) || !(handler instanceof HTMLSelectElement) || !(useHandler instanceof HTMLButtonElement) || !(saveDefault instanceof HTMLInputElement)) return null;
23975
- const clearDefaultButton = clearDefault instanceof HTMLButtonElement ? clearDefault : null;
23976
- let settle6 = null;
23977
- let trigger = null;
23978
- let pointer;
23979
- let forceChooser = false;
23980
- let run = null;
23981
- let chooseSettle = null;
23982
- let activeArchetype;
23985
+ const dismiss = document2.getElementById("paja-intent-link-dismiss");
23986
+ if (!(dialog instanceof HTMLDialogElement) || !(title instanceof HTMLElement) || !(copy instanceof HTMLElement) || !(apps instanceof HTMLElement) || !(remember instanceof HTMLInputElement) || !(cancel instanceof HTMLButtonElement) || !(notice instanceof HTMLElement) || !(status instanceof HTMLElement) || !(retry instanceof HTMLButtonElement) || !(dismiss instanceof HTMLButtonElement)) return null;
23983
23987
  let generation = 0;
23984
- let busy = false;
23985
- const isCurrent = (runGeneration) => generation === runGeneration && settle6 !== null;
23986
- const setBusy = (value) => {
23987
- busy = value;
23988
- launch.disabled = value;
23989
- retry.disabled = value || !run;
23990
- choose2.disabled = value;
23991
- };
23992
- const setTargetCopy = () => {
23993
- target.textContent = pointer ? "This link names one verified target. If it cannot satisfy this exact convention, choose another deliberately." : forceChooser ? "Choose a compatible handler deliberately. This launch will not use a saved default or recommendation." : "Paja will use your saved default, recommendation, or ask you to choose a compatible handler.";
23994
- };
23995
- const finish = () => {
23996
- generation += 1;
23997
- busy = false;
23998
- const done = settle6;
23999
- settle6 = null;
24000
- run = null;
24001
- chooseSettle?.(void 0);
24002
- chooseSettle = null;
23988
+ let run;
23989
+ let archetype = "";
23990
+ let settleChoice;
23991
+ let trigger = null;
23992
+ const closeChoice = (id) => {
23993
+ const settle6 = settleChoice;
23994
+ settleChoice = void 0;
23995
+ if (id && remember.checked) options.setDefault?.(archetype, id);
24003
23996
  if (dialog.open) dialog.close();
24004
23997
  trigger?.focus();
24005
23998
  trigger = null;
24006
- done?.();
24007
- };
24008
- const cancelReview = () => finish();
24009
- const chooseAnother = () => {
24010
- pointer = void 0;
24011
- forceChooser = true;
24012
- setTargetCopy();
24013
- status.textContent = "Choose another will open the compatible-handler chooser and bypass saved defaults.";
24014
- };
24015
- const retryReview = () => {
24016
- if (!busy) run?.();
24017
- };
24018
- const launchReview = () => {
24019
- if (!busy) run?.();
24020
- };
24021
- const useSelectedHandler = () => {
24022
- const settleChoice = chooseSettle;
24023
- chooseSettle = null;
24024
- handler.disabled = true;
24025
- useHandler.disabled = true;
24026
- settleChoice?.(handler.value || void 0);
24027
- };
24028
- const clearSavedDefault = () => {
24029
- if (!activeArchetype) return;
24030
- options.clearDefault?.(activeArchetype);
24031
- status.textContent = `Cleared the saved default for ${activeArchetype}.`;
24032
- };
24033
- cancel.addEventListener("click", cancelReview);
24034
- launch.addEventListener("click", launchReview);
24035
- retry.addEventListener("click", retryReview);
24036
- choose2.addEventListener("click", chooseAnother);
24037
- useHandler.addEventListener("click", useSelectedHandler);
24038
- clearDefaultButton?.addEventListener("click", clearSavedDefault);
24039
- dialog.addEventListener("cancel", cancelReview);
23999
+ settle6?.(id);
24000
+ };
24001
+ const cancelChoice = () => {
24002
+ closeChoice();
24003
+ generation += 1;
24004
+ notice.hidden = true;
24005
+ run = void 0;
24006
+ };
24007
+ const dismissNotice = () => {
24008
+ generation += 1;
24009
+ notice.hidden = true;
24010
+ run = void 0;
24011
+ closeChoice();
24012
+ };
24013
+ const retryLink = () => run?.();
24014
+ const showError = (_error) => {
24015
+ status.textContent = "Couldn\u2019t open this shared content. Try again, or ask the sender for a new link.";
24016
+ notice.hidden = false;
24017
+ retry.hidden = !run;
24018
+ };
24019
+ const choose2 = (heading, description, candidates) => {
24020
+ closeChoice();
24021
+ trigger = document2.activeElement instanceof HTMLElement ? document2.activeElement : null;
24022
+ title.textContent = heading;
24023
+ copy.textContent = description;
24024
+ remember.checked = false;
24025
+ apps.replaceChildren();
24026
+ for (const candidate of candidates) {
24027
+ const button2 = document2.createElement("button");
24028
+ button2.type = "button";
24029
+ button2.textContent = `Open with ${candidate.title}`;
24030
+ button2.addEventListener("click", () => closeChoice(candidate.id));
24031
+ apps.append(button2);
24032
+ }
24033
+ notice.hidden = true;
24034
+ const result2 = new Promise((resolve) => {
24035
+ settleChoice = resolve;
24036
+ });
24037
+ dialog.showModal();
24038
+ return result2;
24039
+ };
24040
+ cancel.addEventListener("click", cancelChoice);
24041
+ dialog.addEventListener("cancel", cancelChoice);
24042
+ dismiss.addEventListener("click", dismissNotice);
24043
+ retry.addEventListener("click", retryLink);
24040
24044
  return {
24041
- review(link, onLaunch) {
24042
- if (settle6) finish();
24043
- uri.value = link.uri;
24044
- pointer = link.pointer;
24045
- forceChooser = false;
24046
- payload.value = Object.hasOwn(link, "payload") ? JSON.stringify(link.payload) : "";
24047
- trigger = document2.activeElement instanceof HTMLElement ? document2.activeElement : null;
24048
- activeArchetype = link.request.archetype;
24049
- setTargetCopy();
24050
- status.textContent = "Review the URI and payload, then explicitly launch.";
24045
+ open(link, launch) {
24046
+ dismissNotice();
24047
+ archetype = link.request.archetype;
24051
24048
  run = () => {
24052
- if (busy) return;
24053
- const runGeneration = ++generation;
24054
- let review;
24055
- try {
24056
- const hasPayload = payload.value.trim().length > 0;
24057
- const parsedPayload = hasPayload ? JSON.parse(payload.value) : void 0;
24058
- const request = normalizeIntentUri(uri.value, {
24059
- ...hasPayload ? { payload: parsedPayload } : {},
24060
- ...forceChooser ? { handler: "choose" } : {}
24061
- });
24062
- if (pointer !== void 0 && request.handlerHint !== void 0) {
24063
- throw new TypeError("Paja intent link cannot combine a target pointer with a URI recommendation");
24064
- }
24065
- activeArchetype = request.archetype;
24066
- review = { uri: uri.value, ...pointer === void 0 ? {} : { pointer }, ...hasPayload ? { payload: parsedPayload } : {}, request };
24067
- } catch (error) {
24068
- if (isCurrent(runGeneration)) status.textContent = error instanceof Error ? error.message : "Intent link is invalid.";
24069
- return;
24070
- }
24071
- setBusy(true);
24072
- status.textContent = "Resolving verified handler policy\u2026";
24049
+ const current = ++generation;
24073
24050
  let terminal = false;
24051
+ let consumed = false;
24052
+ const isActive = () => current === generation;
24053
+ const consume = () => {
24054
+ if (!consumed) {
24055
+ consumed = true;
24056
+ options.onAccepted?.();
24057
+ }
24058
+ };
24059
+ status.textContent = "Opening shared content\u2026";
24060
+ notice.hidden = false;
24061
+ retry.hidden = true;
24074
24062
  const progress = {
24075
- isActive: () => isCurrent(runGeneration),
24076
- selectedTarget(title) {
24077
- if (!isCurrent(runGeneration)) return;
24078
- target.textContent = `Verified target: ${title}`;
24063
+ isActive,
24064
+ selectedTarget(name) {
24065
+ if (isActive() && !terminal) status.textContent = `Opening ${name}\u2026`;
24079
24066
  },
24067
+ chooseConflict: (saved, shared) => choose2(
24068
+ "Which app would you like to use?",
24069
+ `This link uses ${shared.title}. Your saved preference is ${saved.title}.`,
24070
+ [saved, shared]
24071
+ ),
24080
24072
  accepted() {
24081
- if (!isCurrent(runGeneration) || terminal) return;
24082
- status.textContent = "Accepted: delivery is queued and the target may still be loading.";
24073
+ if (isActive()) consume();
24083
24074
  },
24084
24075
  delivered() {
24085
- if (!isCurrent(runGeneration) || terminal) return;
24076
+ if (!isActive() || terminal) return;
24086
24077
  terminal = true;
24087
- status.textContent = "Delivered to the verified target.";
24088
- setBusy(false);
24078
+ consume();
24079
+ notice.hidden = true;
24080
+ run = void 0;
24089
24081
  },
24090
24082
  failed(error) {
24091
- if (!isCurrent(runGeneration) || terminal) return;
24092
- terminal = true;
24093
- status.textContent = `Delivery failed: ${error instanceof Error ? error.message : String(error)}`;
24094
- setBusy(false);
24083
+ if (isActive() && !terminal) {
24084
+ terminal = true;
24085
+ showError(error);
24086
+ }
24095
24087
  }
24096
24088
  };
24097
- void Promise.resolve().then(() => onLaunch(review, progress)).then(void 0, (error) => progress.failed(error));
24089
+ void Promise.resolve().then(() => launch(link, progress)).catch(progress.failed);
24098
24090
  };
24099
- setBusy(false);
24100
- dialog.showModal();
24101
- uri.focus();
24102
- return new Promise((resolve) => {
24103
- settle6 = resolve;
24104
- });
24091
+ run();
24105
24092
  },
24106
- chooseHandler(archetype, candidates) {
24107
- if (chooseSettle) chooseSettle(void 0);
24108
- activeArchetype = archetype;
24109
- if (!dialog.open) {
24110
- trigger = document2.activeElement instanceof HTMLElement ? document2.activeElement : null;
24111
- pointer = void 0;
24112
- forceChooser = true;
24113
- uri.value = "";
24114
- payload.value = "";
24115
- setTargetCopy();
24116
- dialog.showModal();
24117
- }
24118
- handler.replaceChildren();
24119
- for (const candidate of candidates) {
24120
- const option = document2.createElement("option");
24121
- option.value = candidate.id;
24122
- option.textContent = candidate.title ?? candidate.id;
24123
- handler.append(option);
24124
- }
24125
- handler.disabled = candidates.length === 0;
24126
- useHandler.disabled = candidates.length === 0;
24127
- saveDefault.checked = false;
24128
- status.textContent = `Choose a compatible handler for ${archetype}.`;
24129
- return new Promise((resolve) => {
24130
- chooseSettle = resolve;
24131
- });
24132
- },
24133
- consumeSaveDefault() {
24134
- const selected = saveDefault.checked;
24135
- saveDefault.checked = false;
24136
- return selected;
24093
+ chooseHandler(role, candidates) {
24094
+ archetype = role;
24095
+ return choose2(
24096
+ "Choose an app",
24097
+ "Open this shared content with:",
24098
+ candidates.map((candidate) => ({ id: candidate.id, title: candidate.title ?? "Untitled app" }))
24099
+ );
24137
24100
  },
24101
+ showError,
24138
24102
  dispose() {
24139
- cancel.removeEventListener("click", cancelReview);
24140
- launch.removeEventListener("click", launchReview);
24141
- retry.removeEventListener("click", retryReview);
24142
- choose2.removeEventListener("click", chooseAnother);
24143
- useHandler.removeEventListener("click", useSelectedHandler);
24144
- clearDefaultButton?.removeEventListener("click", clearSavedDefault);
24145
- dialog.removeEventListener("cancel", cancelReview);
24146
- finish();
24103
+ dismissNotice();
24104
+ cancel.removeEventListener("click", cancelChoice);
24105
+ dialog.removeEventListener("cancel", cancelChoice);
24106
+ dismiss.removeEventListener("click", dismissNotice);
24107
+ retry.removeEventListener("click", retryLink);
24147
24108
  }
24148
24109
  };
24149
24110
  }
@@ -24456,6 +24417,7 @@ Routing: ${targetDescription}`;
24456
24417
  appendIntentLinkBuilderOption(routing.control, "default", "Recipient default");
24457
24418
  appendIntentLinkBuilderOption(routing.control, "recommend", "Recommend this app", nextTarget.event.kind !== 35129 || !nextTarget.manifest.dTag);
24458
24419
  appendIntentLinkBuilderOption(routing.control, "exact", "Exact target pointer");
24420
+ routing.control.value = nextTarget.event.kind === 35129 && nextTarget.manifest.dTag ? "recommend" : "exact";
24459
24421
  appendIntentLinkBuilderOption(payloadMode.control, "text", "Text fields");
24460
24422
  appendIntentLinkBuilderOption(payloadMode.control, "json", "JSON payload");
24461
24423
  json.value = "";
@@ -24542,6 +24504,61 @@ Routing: ${targetDescription}`;
24542
24504
  };
24543
24505
  }
24544
24506
 
24507
+ // src/browser-intent-link-target.ts
24508
+ function supports(target, link) {
24509
+ const contracts = "manifest" in target ? target.manifest.archetypes : target.archetypes;
24510
+ return contracts.some((entry) => entry.slug === link.request.archetype && entry.convention === link.request.convention);
24511
+ }
24512
+ function permitted(context, target) {
24513
+ const identity = "manifest" in target ? { dTag: target.dTag, aggregateHash: target.aggregateHash, requires: target.manifest.requires, optional: target.manifest.optional } : target;
24514
+ return resolveShellEnvironment(context.adapter, identity).capabilities.domains.includes("intent");
24515
+ }
24516
+ async function recommended(context, link) {
24517
+ const hint2 = link.request.handlerHint;
24518
+ if (!hint2) return null;
24519
+ const installed = context.runtime.catalog.get(`nip5d:${hint2.address}`);
24520
+ if (installed && supports(installed, link)) return installed;
24521
+ const match = /^35129:([a-f0-9]{64}):(.+)$/.exec(hint2.address);
24522
+ if (!match) return null;
24523
+ const [, pubkey2, identifier] = match;
24524
+ try {
24525
+ const pointer = naddrEncode({ identifier, pubkey: pubkey2, kind: 35129, relays: [...hint2.relays ?? []] });
24526
+ const target = await resolvePajaPointer(pointer, pajaPointerResolverOptions(context));
24527
+ return target.event.kind === 35129 && target.event.pubkey === pubkey2 && target.manifest.dTag === identifier && supports(target, link) ? target : null;
24528
+ } catch {
24529
+ return null;
24530
+ }
24531
+ }
24532
+ async function resolvePajaSharedLinkHandler(context, link, progress) {
24533
+ if (link.request.handler === "choose") return void 0;
24534
+ const shared = link.pointer ? await resolvePajaPointer(link.pointer, pajaPointerResolverOptions(context)) : await recommended(context, link);
24535
+ if (!progress.isActive() || !shared) return void 0;
24536
+ if (!supports(shared, link) || !permitted(context, shared)) {
24537
+ if (link.pointer) throw new Error("The named app cannot open this shared content");
24538
+ return void 0;
24539
+ }
24540
+ const id = "manifest" in shared ? shared.manifest.catalogId : shared.id;
24541
+ const title = ("manifest" in shared ? shared.manifest.title : shared.title) ?? shared.dTag;
24542
+ const savedId = context.runtime.intentDefaults.get(link.request.archetype);
24543
+ const saved = savedId ? context.runtime.catalog.get(savedId) : void 0;
24544
+ let selected = id;
24545
+ let explicit = link.pointer !== void 0;
24546
+ if (!link.pointer && saved && saved.id !== id && supports(saved, link) && permitted(context, saved)) {
24547
+ const choice = await progress.chooseConflict(
24548
+ { id: saved.id, title: saved.title ?? saved.dTag },
24549
+ { id, title }
24550
+ );
24551
+ if (!progress.isActive()) return void 0;
24552
+ if (choice !== id && choice !== saved.id) throw new Error("App choice cancelled");
24553
+ selected = choice;
24554
+ explicit = true;
24555
+ }
24556
+ if (!progress.isActive()) return void 0;
24557
+ if (selected === id && "manifest" in shared) context.runtime.catalog.install(shared);
24558
+ progress.selectedTarget(selected === id ? title : saved.title ?? saved.dTag);
24559
+ return explicit ? selected : void 0;
24560
+ }
24561
+
24545
24562
  // src/intent-launcher.ts
24546
24563
  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>`;
24547
24564
  var LAUNCHER_EVENT = {
@@ -24585,27 +24602,10 @@ function postPajaIntentLauncherMessage(frame, message2) {
24585
24602
  }
24586
24603
 
24587
24604
  // src/browser-intent-launcher-host.ts
24588
- async function launchPajaReviewedIntent(state, context, intent, progress, setPointerStatus2) {
24605
+ async function launchPajaSharedIntent(state, context, intent, progress, setPointerStatus2) {
24589
24606
  const { runtime } = context;
24590
- let explicitHandler;
24591
24607
  if (!progress.isActive()) return;
24592
- if (intent.pointer) {
24593
- const target = await resolvePajaPointer(intent.pointer, pajaPointerResolverOptions(context));
24594
- if (!progress.isActive()) return;
24595
- const exact = target.manifest.archetypes.some((contract) => contract.slug === intent.request.archetype && contract.convention === intent.request.convention);
24596
- if (!exact) throw new Error("The explicitly named napplet does not advertise this exact intent convention");
24597
- runtime.catalog.install(target);
24598
- progress.selectedTarget(target.manifest.title ?? target.manifest.dTag);
24599
- explicitHandler = target.manifest.catalogId;
24600
- }
24601
- if (intent.request.handlerHint && shouldResolveRecommendation(intent, runtime)) {
24602
- const target = await resolveRecommendedIntentTarget(context, intent.request.handlerHint, intent.request.archetype, intent.request.convention);
24603
- if (!progress.isActive()) return;
24604
- if (target) {
24605
- runtime.catalog.install(target);
24606
- progress.selectedTarget(target.manifest.title ?? target.manifest.dTag);
24607
- }
24608
- }
24608
+ const explicitHandler = await resolvePajaSharedLinkHandler(context, intent, progress);
24609
24609
  if (!progress.isActive()) return;
24610
24610
  const launcher = await resolvePajaIntentLauncher();
24611
24611
  if (!progress.isActive()) return;
@@ -24690,32 +24690,6 @@ async function launchPajaReviewedIntent(state, context, intent, progress, setPoi
24690
24690
  teardown();
24691
24691
  }
24692
24692
  }
24693
- function shouldResolveRecommendation(intent, runtime) {
24694
- if (intent.request.handler !== void 0) return false;
24695
- const defaultId = runtime.intentDefaults.get(intent.request.archetype);
24696
- if (!defaultId) return true;
24697
- const record2 = runtime.catalog.get(defaultId);
24698
- return !record2 || !record2.archetypes.some((entry) => entry.slug === intent.request.archetype && entry.convention === intent.request.convention);
24699
- }
24700
- async function resolveRecommendedIntentTarget(context, hint2, archetype, convention) {
24701
- const match = /^35129:([a-f0-9]{64}):(.+)$/.exec(hint2.address);
24702
- if (!match) return null;
24703
- const [, pubkey2, identifier] = match;
24704
- const existing = context.runtime.catalog.installed().find((record2) => record2.id === `nip5d:${hint2.address}` && record2.archetypes.some((entry) => entry.slug === archetype && entry.convention === convention));
24705
- if (existing) return null;
24706
- const pointer = naddrEncode({ identifier, pubkey: pubkey2, kind: 35129, relays: [...hint2.relays ?? []] });
24707
- let target;
24708
- try {
24709
- target = await resolvePajaPointer(pointer, pajaPointerResolverOptions(context));
24710
- } catch {
24711
- return null;
24712
- }
24713
- const exactCoordinate = target.event.kind === 35129 && target.event.pubkey === pubkey2 && target.manifest.dTag === identifier;
24714
- const exactContract = target.manifest.archetypes.some((entry) => entry.slug === archetype && entry.convention === convention);
24715
- if (!exactCoordinate || !exactContract) return null;
24716
- if (!window.confirm(`Install and use recommended handler ${target.manifest.title ?? target.manifest.dTag}?`)) return null;
24717
- return target;
24718
- }
24719
24693
 
24720
24694
  // src/browser-target-diagnostics.ts
24721
24695
  async function reportTargetCorsDiagnostic(state) {
@@ -25369,8 +25343,8 @@ async function restorePersistedRuntimeTabs(state, context, snapshot) {
25369
25343
  persistRuntimeTabs2(state);
25370
25344
  }
25371
25345
  async function hydratePersistedIntentCatalog(state, context, snapshot) {
25372
- if (!snapshot) return;
25373
- await Promise.all(snapshot.pointers.map(async (pointer) => {
25346
+ const pointers = /* @__PURE__ */ new Set([...readPajaIntentCatalogPointers(), ...snapshot?.pointers ?? []]);
25347
+ await Promise.all([...pointers].map(async (pointer) => {
25374
25348
  try {
25375
25349
  const resolved = await resolvePajaPointer(pointer, pajaPointerResolverOptions(context));
25376
25350
  context.runtime.catalog.install(resolved);
@@ -25528,7 +25502,7 @@ async function installPajaHost() {
25528
25502
  const themeBroadcast = createPajaThemeBroadcastLink();
25529
25503
  let stateRef = null;
25530
25504
  let contextRef = null;
25531
- let intentReview = null;
25505
+ let intentLinks = null;
25532
25506
  const intentController = new BrowserIntentController(createPajaIntentTargetOptions(
25533
25507
  () => stateRef,
25534
25508
  () => contextRef,
@@ -25594,12 +25568,7 @@ async function installPajaHost() {
25594
25568
  return runtime.intentDefaults.get(archetype);
25595
25569
  },
25596
25570
  async chooseHandler(archetype, candidates) {
25597
- const selected = await intentReview?.chooseHandler(archetype, candidates);
25598
- if (selected && intentReview?.consumeSaveDefault()) {
25599
- runtime.intentDefaults.set(archetype, selected);
25600
- writePajaIntentDefaults(runtime.intentDefaults);
25601
- }
25602
- return selected;
25571
+ return intentLinks?.chooseHandler(archetype, candidates);
25603
25572
  },
25604
25573
  resolveHandlerHint(hint2, candidates) {
25605
25574
  const id = `nip5d:${hint2.address}`;
@@ -25636,6 +25605,19 @@ async function installPajaHost() {
25636
25605
  stateRef = state;
25637
25606
  const stopIntentCatalogChanges = subscribePajaIntentCatalogChanges(state, context);
25638
25607
  window.addEventListener("pagehide", stopIntentCatalogChanges, { once: true });
25608
+ const catalogPointers = /* @__PURE__ */ new Map();
25609
+ const stopCatalogPersistence = runtime.catalog.onChanged((id) => {
25610
+ const pointers = new Set(readPajaIntentCatalogPointers());
25611
+ const previous = catalogPointers.get(id);
25612
+ if (previous) pointers.delete(previous);
25613
+ const record2 = runtime.catalog.get(id);
25614
+ if (record2) {
25615
+ pointers.add(record2.pointer.value);
25616
+ catalogPointers.set(id, record2.pointer.value);
25617
+ } else catalogPointers.delete(id);
25618
+ writePajaIntentCatalogPointers([...pointers]);
25619
+ });
25620
+ window.addEventListener("pagehide", stopCatalogPersistence, { once: true });
25639
25621
  window.addEventListener("pagehide", stopIdentityChanges, { once: true });
25640
25622
  window.addEventListener("pagehide", () => confirmationController.dispose(), { once: true });
25641
25623
  window.addEventListener("pagehide", () => notifyController?.dispose(), { once: true });
@@ -25689,24 +25671,30 @@ async function installPajaHost() {
25689
25671
  setStatus(state, "error");
25690
25672
  });
25691
25673
  installPajaControlListeners(state);
25692
- const review = createPajaIntentLinkReviewController(void 0, {
25693
- clearDefault(archetype) {
25694
- runtime.intentDefaults.delete(archetype);
25674
+ const links = createPajaIntentLinkController(void 0, {
25675
+ setDefault(archetype, id) {
25676
+ runtime.intentDefaults.set(archetype, id);
25695
25677
  writePajaIntentDefaults(runtime.intentDefaults);
25678
+ },
25679
+ onAccepted() {
25680
+ const url = new URL(window.location.href);
25681
+ if (!url.searchParams.has("intent")) return;
25682
+ url.search = "";
25683
+ window.history.replaceState(window.history.state, "", url);
25696
25684
  }
25697
25685
  });
25698
- intentReview = review;
25686
+ intentLinks = links;
25699
25687
  const builder = createPajaIntentLinkBuilder({
25700
25688
  onTest(link) {
25701
- if (!review) return;
25702
- void review.review(link, (intent, progress) => launchPajaReviewedIntent(state, context, intent, progress, setPointerStatus));
25689
+ if (!links) return;
25690
+ links.open(link, (intent, progress) => launchPajaSharedIntent(state, context, intent, progress, setPointerStatus));
25703
25691
  }
25704
25692
  });
25705
25693
  state.createIntentLink = (tab) => {
25706
25694
  if (isPajaLocalTarget(tab.resolvedTarget) || tab.resolvedTarget.manifest.archetypes.length === 0) return;
25707
25695
  builder.open(tab.resolvedTarget);
25708
25696
  };
25709
- window.addEventListener("pagehide", () => review?.dispose(), { once: true });
25697
+ window.addEventListener("pagehide", () => links?.dispose(), { once: true });
25710
25698
  window.addEventListener("pagehide", () => builder.dispose(), { once: true });
25711
25699
  window.addEventListener("pagehide", () => {
25712
25700
  for (const [source, launcher] of runtime.launcherFrames) {
@@ -25728,18 +25716,19 @@ async function installPajaHost() {
25728
25716
  } catch (error) {
25729
25717
  setStatus(state, "error");
25730
25718
  setPointerStatus(state, error instanceof Error ? error.message : String(error));
25719
+ links?.showError(error);
25731
25720
  return;
25732
25721
  }
25733
25722
  const persistedTabs = readPersistedRuntimeTabs(config);
25734
25723
  const input = document.getElementById("runtime-pointer-input");
25735
25724
  if (input instanceof HTMLInputElement) input.value = state.pointerValue;
25736
- if (incomingIntent && review) {
25725
+ if (incomingIntent && links) {
25737
25726
  void hydratePersistedIntentCatalog(state, context, persistedTabs).then(() => {
25738
25727
  setStatus(state, "ready");
25739
25728
  setEmptyStageVisible(true);
25740
25729
  renderRuntimeTabs(state);
25741
- void review.review(incomingIntent, (intent, progress) => launchPajaReviewedIntent(state, context, intent, progress, setPointerStatus));
25742
- });
25730
+ links.open(incomingIntent, (intent, progress) => launchPajaSharedIntent(state, context, intent, progress, setPointerStatus));
25731
+ }).catch((error) => links.showError(error));
25743
25732
  } else if (state.pointerValue) void state.loadPointer(state.pointerValue);
25744
25733
  else if (persistedTabs) void restorePersistedRuntimeTabs(state, context, persistedTabs);
25745
25734
  else {