@nomosprotocol/sdk 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  NomosRateLimitError: () => NomosRateLimitError,
37
37
  NomosValidationError: () => NomosValidationError,
38
38
  NomosWebhookSignatureError: () => NomosWebhookSignatureError,
39
+ RegistrarUnavailable: () => RegistrarUnavailable,
39
40
  RegistryError: () => RegistryError,
40
41
  RevocationUnavailable: () => RevocationUnavailable,
41
42
  WELL_KNOWN: () => WELL_KNOWN,
@@ -66,6 +67,7 @@ __export(index_exports, {
66
67
  postgresStore: () => postgresStore,
67
68
  presentedCertificates: () => presentedCertificates,
68
69
  readAttestation: () => readAttestation,
70
+ registrarSource: () => registrarSource,
69
71
  reliedKids: () => reliedKids,
70
72
  revocationListCertificates: () => revocationListCertificates,
71
73
  revocationSource: () => revocationSource,
@@ -191,7 +193,7 @@ function isRetryable(status) {
191
193
  }
192
194
 
193
195
  // src/http-client.ts
194
- var SDK_VERSION = "2.5.0";
196
+ var SDK_VERSION = "2.7.0";
195
197
  var HttpClient = class {
196
198
  constructor(opts) {
197
199
  this.opts = opts;
@@ -858,6 +860,50 @@ async function canViaChain(artifact, keyCerts, rootPublicKeyPem, facts, action,
858
860
  if (data?.decision === "MALFORMED") throw new NomosValidationError(message, data, requestId);
859
861
  mapHttpError(res.status, data, requestId);
860
862
  }
863
+ function normalizeResolutionResult(data) {
864
+ return {
865
+ mode: "resolution",
866
+ allowed: data.verdict === "AUTHORIZED",
867
+ verdict: data.verdict,
868
+ reason: data.reason ?? "",
869
+ resolution: data.resolution ?? { status: "INCOMPLETE", proof: null },
870
+ members: Array.isArray(data.members) ? data.members : [],
871
+ deciding_authorities: Array.isArray(data.deciding_authorities) ? data.deciding_authorities : [],
872
+ escalations: Array.isArray(data.escalations) ? data.escalations : [],
873
+ open_facts: Array.isArray(data.open_facts) ? data.open_facts : [],
874
+ ...Array.isArray(data.answered_under) ? { answered_under: data.answered_under } : {},
875
+ ...typeof data.coverage === "string" ? { coverage: data.coverage } : {}
876
+ };
877
+ }
878
+ function resolutionBody(params) {
879
+ const { authority, artifact_id, artifact, key_certs, root_public_key_pem, action, inputs, context, ...flat } = params;
880
+ void authority;
881
+ void artifact_id;
882
+ void artifact;
883
+ void key_certs;
884
+ void root_public_key_pem;
885
+ const facts = inputs && typeof inputs === "object" ? inputs : flat;
886
+ return { context, ...action ? { action } : {}, ...facts };
887
+ }
888
+ async function canViaResolution(params, opts) {
889
+ const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
890
+ let res;
891
+ try {
892
+ res = await fetch(`${base}/api/v1/can`, {
893
+ method: "POST",
894
+ headers: { "Content-Type": "application/json", "User-Agent": `nomos-node/${SDK_VERSION}`, ...callerHeaders(opts?.caller) },
895
+ body: JSON.stringify(resolutionBody(params)),
896
+ signal: opts?.signal
897
+ });
898
+ } catch (err) {
899
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
900
+ throw new NomosNetworkError(err?.message ?? "Network error");
901
+ }
902
+ const requestId = res.headers.get("x-request-id") ?? "";
903
+ const data = await res.json().catch(() => null);
904
+ if (!res.ok) mapHttpError(res.status, data, requestId);
905
+ return normalizeResolutionResult(data);
906
+ }
861
907
  async function can(params, opts) {
862
908
  if (params.artifact !== void 0) {
863
909
  if (!params.key_certs || !params.root_public_key_pem) {
@@ -869,8 +915,11 @@ async function can(params, opts) {
869
915
  if (params.artifact_id) {
870
916
  throw new Error("can(): `artifact_id` requires an API key \u2014 use `client.can(...)` on an authenticated Nomos instance, not the free `can()` export.");
871
917
  }
918
+ if (!params.authority && params.context && typeof params.context === "object" && !Array.isArray(params.context)) {
919
+ return canViaResolution(params, opts);
920
+ }
872
921
  if (!params.authority || typeof params.authority !== "string") {
873
- throw new Error("can(): `authority` (an Exchange artifact id or slug), `artifact_id` (via client.can()), or `artifact` (chain-of-trust) is required.");
922
+ throw new Error("can(): `authority` (an Exchange artifact id or slug), `artifact_id` (via client.can()), `artifact` (chain-of-trust), or `context` (resolution, NOMOS-SPEC-014) is required.");
874
923
  }
875
924
  const { facts, action } = splitFacts(params);
876
925
  return canViaExchange(params.authority, facts, action, opts);
@@ -885,7 +934,11 @@ async function canAuthenticated(http, params, opts) {
885
934
  }
886
935
  if (params.artifact_id) return canViaArtifact(http, params.artifact_id, facts, action);
887
936
  if (params.authority) return canViaExchangeAuthenticated(http, params.authority, facts, action, opts);
888
- throw new Error("can(): `authority` (a public Exchange artifact), `artifact_id` (your own artifact), or `artifact` (chain-of-trust) is required.");
937
+ if (params.context && typeof params.context === "object" && !Array.isArray(params.context)) {
938
+ const data = (await http.post("/api/v1/can", resolutionBody(params), { headers: callerHeaders(opts?.caller) })).data;
939
+ return normalizeResolutionResult(data);
940
+ }
941
+ throw new Error("can(): `authority` (a public Exchange artifact), `artifact_id` (your own artifact), `artifact` (chain-of-trust), or `context` (resolution, NOMOS-SPEC-014) is required.");
889
942
  }
890
943
  var nomos = { can };
891
944
 
@@ -5207,7 +5260,7 @@ function verifyUnsafe(a, o) {
5207
5260
  const now = (o.now ?? /* @__PURE__ */ new Date()).getTime();
5208
5261
  const nowDate = new Date(now);
5209
5262
  const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
5210
- const localFacts = o.localFacts ?? {}, inForce = o.inForce ?? {};
5263
+ const localFacts = o.localFacts ?? {}, inForce2 = o.inForce ?? {};
5211
5264
  const need = ["act_version", "act_id", "nonce", "issued_at", "expires_at", "relying_party", "action", "authority"];
5212
5265
  if (!isObject(a) || need.some((k) => !(k in a)) || a.act_version !== "1" || !isObject(a.authority))
5213
5266
  return { decision: "MALFORMED", reason_code: "bad_envelope" };
@@ -5251,9 +5304,9 @@ function verifyUnsafe(a, o) {
5251
5304
  if (o.revokedArtifacts?.has(seal2.hash))
5252
5305
  return { decision: "ARTIFACT_REVOKED", artifact_id: artId };
5253
5306
  let inForceSource;
5254
- if (typeof artId === "string" && Object.prototype.hasOwnProperty.call(inForce, artId)) {
5255
- if (inForce[artId] !== seal2.hash)
5256
- return { decision: "SUPERSEDED", artifact_id: artId, in_force_seal_hash: inForce[artId] };
5307
+ if (typeof artId === "string" && Object.prototype.hasOwnProperty.call(inForce2, artId)) {
5308
+ if (inForce2[artId] !== seal2.hash)
5309
+ return { decision: "SUPERSEDED", artifact_id: artId, in_force_seal_hash: inForce2[artId] };
5257
5310
  inForceSource = "record";
5258
5311
  } else if (o.currencyPolicy === "record") {
5259
5312
  return { decision: "VERSION_UNVERIFIED", artifact_id: artId, reason_code: "not_in_record" };
@@ -5470,6 +5523,288 @@ function verifyRevocationList(list, o) {
5470
5523
  return out;
5471
5524
  }
5472
5525
 
5526
+ // node_modules/@nomosprotocol/verify/dist/resolution.js
5527
+ var DEFAULT_CLOSURE_MAX_AGE_DAYS = 90;
5528
+ function evaluateScope(condition, context) {
5529
+ if (condition === "always" || condition === "all")
5530
+ return { value: "TRUE", openFacts: [] };
5531
+ let out;
5532
+ try {
5533
+ out = evaluate({
5534
+ logic: {
5535
+ resolution: { conflict_policy: "highest_priority", tie_breaker: "deny_wins", default_outcome: "escalate" },
5536
+ decisions: [
5537
+ { id: "scope-when-true", when: condition, priority: 1, then: [{ type: "allow" }] },
5538
+ { id: "scope", when: condition, priority: 2, then: [{ type: "block" }] }
5539
+ ]
5540
+ }
5541
+ }, context);
5542
+ } catch {
5543
+ return { value: "UNDECIDED", openFacts: ["__scope_unparseable__"] };
5544
+ }
5545
+ if (out.outcome === "block")
5546
+ return { value: "TRUE", openFacts: [] };
5547
+ if (out.outcome === "undecided")
5548
+ return { value: "UNDECIDED", openFacts: [...out.open_facts] };
5549
+ return { value: "FALSE", openFacts: [] };
5550
+ }
5551
+ var inForce = (c, at) => (!c.from || c.from <= at) && (!c.until || c.until > at);
5552
+ var closureFresh = (c, at) => {
5553
+ const ageMs = Date.parse(at) - Date.parse(c.at);
5554
+ if (!Number.isFinite(ageMs) || ageMs < 0)
5555
+ return false;
5556
+ return ageMs <= (c.max_age_days ?? DEFAULT_CLOSURE_MAX_AGE_DAYS) * 864e5;
5557
+ };
5558
+ var closureReplaced = (c, all, at) => {
5559
+ const t = Date.parse(c.at), now = Date.parse(at);
5560
+ return all.some((d) => d !== c && Date.parse(d.at) > t && Date.parse(d.at) <= now && (d.scope === c.scope || !!c.label && d.label === c.label));
5561
+ };
5562
+ function resolveApplicableSet(world, query, opts = {}) {
5563
+ const at = query.at;
5564
+ const ctx = query.context ?? {};
5565
+ const proof = { at, pins: [...world.pins].sort(), heads: {}, closures: [], conflicts: [], dropped: [] };
5566
+ if (opts.ablate === "C5") {
5567
+ const applicable2 = (opts.presented ?? []).map((a) => ({ authority: a, version: null, registrar: "carrier", scope: "always" })).sort((x, y) => x.authority < y.authority ? -1 : 1);
5568
+ return { status: "RESOLVED", applicable: applicable2, proof };
5569
+ }
5570
+ const openFacts = /* @__PURE__ */ new Set();
5571
+ const unreachable = [];
5572
+ const staleClosures = [];
5573
+ const disagreements = [];
5574
+ const staleHeads = [];
5575
+ const memberAt = opts.ablate === "C7" && opts.snapshot ? opts.snapshot : at;
5576
+ const candidates2 = [];
5577
+ const visited = /* @__PURE__ */ new Set();
5578
+ const queue = [...world.pins].sort();
5579
+ while (queue.length > 0) {
5580
+ const id = queue.shift();
5581
+ if (visited.has(id))
5582
+ continue;
5583
+ visited.add(id);
5584
+ const reg = world.registrars[id];
5585
+ if (!reg) {
5586
+ if (opts.ablate !== "C8")
5587
+ unreachable.push(id);
5588
+ continue;
5589
+ }
5590
+ proof.heads[id] = reg.head ?? null;
5591
+ if (opts.ablate !== "C8" && typeof world.max_head_age_seconds === "number") {
5592
+ const age = reg.head_at ? Date.parse(at) - Date.parse(reg.head_at) : NaN;
5593
+ if (!Number.isFinite(age) || age > world.max_head_age_seconds * 1e3)
5594
+ staleHeads.push(id);
5595
+ }
5596
+ const cert = evaluateScope(reg.certified_scope, ctx);
5597
+ for (const f of cert.openFacts)
5598
+ openFacts.add(f);
5599
+ for (const e of [...reg.recognitions ?? []].sort((a, b) => a.to < b.to ? -1 : 1)) {
5600
+ const s = evaluateScope(e.scope, ctx);
5601
+ if (s.value === "TRUE")
5602
+ queue.push(e.to);
5603
+ for (const f of s.openFacts)
5604
+ openFacts.add(f);
5605
+ }
5606
+ const closedLists = [];
5607
+ if (opts.ablate !== "C8") {
5608
+ let covered = false;
5609
+ for (const c of reg.closures) {
5610
+ if (closureReplaced(c, reg.closures, at))
5611
+ continue;
5612
+ const s = evaluateScope(c.scope, ctx);
5613
+ for (const f of s.openFacts)
5614
+ openFacts.add(f);
5615
+ if (s.value !== "TRUE")
5616
+ continue;
5617
+ const fresh = closureFresh(c, at);
5618
+ proof.closures.push({ registrar: id, scope: c.scope, at: c.at, fresh });
5619
+ if (fresh) {
5620
+ covered = true;
5621
+ closedLists.push(c.authorities);
5622
+ }
5623
+ }
5624
+ if (!covered)
5625
+ staleClosures.push(id);
5626
+ }
5627
+ const supersededNow = new Set((reg.supersessions ?? []).filter((s) => s.from <= memberAt).map((s) => s.superseded));
5628
+ const revoked = new Set(reg.revoked ?? []);
5629
+ const liveAuthorities = /* @__PURE__ */ new Set();
5630
+ const trueAuthorities = /* @__PURE__ */ new Set();
5631
+ for (const claim of reg.claims) {
5632
+ if (opts.ablate !== "C7" || opts.snapshot) {
5633
+ if (!inForce(claim, memberAt)) {
5634
+ proof.dropped.push({ authority: claim.authority, registrar: id, reason: "not_in_force" });
5635
+ continue;
5636
+ }
5637
+ if (supersededNow.has(claim.authority)) {
5638
+ proof.dropped.push({ authority: claim.authority, registrar: id, reason: "superseded" });
5639
+ continue;
5640
+ }
5641
+ if (revoked.has(claim.authority)) {
5642
+ proof.dropped.push({ authority: claim.authority, registrar: id, reason: "revoked" });
5643
+ continue;
5644
+ }
5645
+ }
5646
+ liveAuthorities.add(claim.authority);
5647
+ const s = evaluateScope(claim.scope, ctx);
5648
+ for (const f of s.openFacts)
5649
+ openFacts.add(f);
5650
+ if (s.value === "TRUE")
5651
+ trueAuthorities.add(claim.authority);
5652
+ if (s.value === "FALSE")
5653
+ continue;
5654
+ const within = opts.ablate === "C6" ? { value: "TRUE", openFacts: [] } : cert;
5655
+ if (s.value === "UNDECIDED" || within.value === "UNDECIDED")
5656
+ continue;
5657
+ if (within.value === "FALSE")
5658
+ continue;
5659
+ candidates2.push({ authority: claim.authority, version: claim.version ?? null, registrar: id, scope: claim.scope, exclusive: claim.exclusive === true });
5660
+ }
5661
+ if (opts.ablate !== "C8" && closedLists.length > 0) {
5662
+ for (const a of new Set(closedLists.flat())) {
5663
+ if (!liveAuthorities.has(a))
5664
+ disagreements.push({ registrar: id, authority: a, reason: "closed_but_not_claimed" });
5665
+ }
5666
+ for (const a of trueAuthorities) {
5667
+ if (!closedLists.some((l) => l.includes(a)))
5668
+ disagreements.push({ registrar: id, authority: a, reason: "claimed_but_not_closed" });
5669
+ }
5670
+ }
5671
+ }
5672
+ proof.dropped.sort((a, b) => a.authority + a.registrar < b.authority + b.registrar ? -1 : 1);
5673
+ proof.closures.sort((a, b) => a.registrar + a.scope + a.at < b.registrar + b.scope + b.at ? -1 : 1);
5674
+ if (openFacts.size > 0 || unreachable.length > 0 || staleClosures.length > 0 || disagreements.length > 0 || staleHeads.length > 0) {
5675
+ return {
5676
+ status: "INCOMPLETE",
5677
+ missing: {
5678
+ context_facts: [...openFacts].sort(),
5679
+ unreachable_registrars: unreachable.sort(),
5680
+ stale_closures: staleClosures.sort(),
5681
+ stale_heads: staleHeads.sort(),
5682
+ closure_disagreements: disagreements.sort((a, b) => a.registrar + "\0" + a.authority < b.registrar + "\0" + b.authority ? -1 : 1)
5683
+ },
5684
+ proof
5685
+ };
5686
+ }
5687
+ const exclusives = candidates2.filter((c) => c.exclusive);
5688
+ const conflictPairs = exclusives.filter((c) => exclusives.some((o) => o.registrar !== c.registrar));
5689
+ if (conflictPairs.length > 0) {
5690
+ proof.conflicts.push({
5691
+ context_covered_by: conflictPairs.map((c) => ({ registrar: c.registrar, authority: c.authority, scope: c.scope })).sort((a, b) => a.registrar + a.authority < b.registrar + b.authority ? -1 : 1)
5692
+ });
5693
+ return { status: "CONFLICT", conflicts: proof.conflicts, proof };
5694
+ }
5695
+ const seen = /* @__PURE__ */ new Set();
5696
+ const applicable = [];
5697
+ for (const c of [...candidates2].sort((a, b) => a.authority + "\0" + a.registrar < b.authority + "\0" + b.registrar ? -1 : 1)) {
5698
+ const key = c.authority + "\0" + c.registrar;
5699
+ if (seen.has(key))
5700
+ continue;
5701
+ seen.add(key);
5702
+ applicable.push({ authority: c.authority, version: c.version, registrar: c.registrar, scope: c.scope });
5703
+ }
5704
+ return { status: "RESOLVED", applicable, proof };
5705
+ }
5706
+
5707
+ // node_modules/@nomosprotocol/verify/dist/registrar.js
5708
+ var LOG_GENESIS = "0".repeat(64);
5709
+ var registrarHeadPayload = (h, log = "nomos-exchange-log") => jcs({ log, size: h.size, hash: h.hash, at: h.at });
5710
+ function verifyLogChain(entries) {
5711
+ let prev = LOG_GENESIS;
5712
+ const hashAt = /* @__PURE__ */ new Map([[0, LOG_GENESIS]]);
5713
+ for (const [i, e] of entries.entries()) {
5714
+ if (e.seq !== i + 1)
5715
+ return { ok: false, detail: `entry ${i + 1}: its seq is ${e.seq}` };
5716
+ if (e.prev_hash !== prev)
5717
+ return { ok: false, detail: `entry ${e.seq}: prev_hash does not match entry ${e.seq - 1}` };
5718
+ const { hash, ...body } = e;
5719
+ if (sha256Hex(jcs(body)) !== hash)
5720
+ return { ok: false, detail: `entry ${e.seq}: its hash does not match its contents` };
5721
+ prev = hash;
5722
+ hashAt.set(e.seq, hash);
5723
+ }
5724
+ return { ok: true, hashAt };
5725
+ }
5726
+ function verifyRegistrarLog(o) {
5727
+ const fail2 = (reason, detail) => ({ ok: false, reason, detail });
5728
+ const chain = verifyLogChain(o.entries);
5729
+ if (!chain.ok)
5730
+ return fail2("chain_broken", chain.detail);
5731
+ const h = o.head;
5732
+ if (!h || typeof h.size !== "number" || h.size > o.entries.length || chain.hashAt.get(h.size) !== h.hash) {
5733
+ return fail2("head_mismatch", `the head (size ${h?.size}) does not name an entry of this chain`);
5734
+ }
5735
+ if (!h.signature || !h.kid)
5736
+ return fail2("head_unsigned", "the head is not signed");
5737
+ const pinnedKid = kidOf(o.publicKeyPem);
5738
+ if (!pinnedKid || h.kid !== pinnedKid)
5739
+ return fail2("wrong_key", `the head is signed by ${h.kid}, not the pinned key ${pinnedKid ?? "(unreadable)"}`);
5740
+ if (!verifySuite("Ed25519", o.publicKeyPem, registrarHeadPayload(h, o.log), { signature: h.signature }).ok)
5741
+ return fail2("bad_signature", "the head signature does not verify under the pinned key");
5742
+ const age = o.now.getTime() - Date.parse(h.at);
5743
+ if (!Number.isFinite(age))
5744
+ return fail2("head_stale", "the head carries no time");
5745
+ if (age < -6e4)
5746
+ return fail2("head_in_future", `the head is dated ${h.at}, after now`);
5747
+ if (age > o.maxHeadAgeSeconds * 1e3)
5748
+ return fail2("head_stale", `the head is ${Math.round(age / 1e3)} s old; the bound is ${o.maxHeadAgeSeconds} s`);
5749
+ if (o.previous && chain.hashAt.get(o.previous.size) !== o.previous.hash) {
5750
+ return fail2("history_rewritten", `the head kept earlier (${o.previous.size}:${o.previous.hash.slice(0, 12)}\u2026) is not part of this log`);
5751
+ }
5752
+ return { ok: true, size: o.entries.length, head: { size: h.size, hash: h.hash, at: h.at, kid: h.kid } };
5753
+ }
5754
+ function foldRegistrarLog(entries, o) {
5755
+ const open = /* @__PURE__ */ new Map();
5756
+ const closedClaims = [];
5757
+ const closures = [];
5758
+ const recognitions = [];
5759
+ const supersessions = [];
5760
+ const revoked = /* @__PURE__ */ new Set();
5761
+ let head = null;
5762
+ for (const e of entries) {
5763
+ head = e.hash;
5764
+ if (e.kind === "applies") {
5765
+ const prior = open.get(e.name);
5766
+ if (prior)
5767
+ closedClaims.push({ ...prior, until: e.at });
5768
+ open.set(e.name, {
5769
+ authority: e.name,
5770
+ version: e.version ?? null,
5771
+ scope: typeof e.entry?.scope === "string" ? e.entry.scope : "always",
5772
+ exclusive: e.entry?.exclusive === true,
5773
+ from: e.at,
5774
+ until: typeof e.entry?.until === "string" ? e.entry.until : null,
5775
+ ...typeof e.entry?.basis === "string" ? { basis: e.entry.basis } : {}
5776
+ });
5777
+ } else if (e.kind === "withdrawn") {
5778
+ const prior = open.get(e.name);
5779
+ if (prior) {
5780
+ closedClaims.push({ ...prior, until: e.at });
5781
+ open.delete(e.name);
5782
+ }
5783
+ } else if (e.kind === "revoked") {
5784
+ revoked.add(e.name);
5785
+ } else if (e.kind === "closed") {
5786
+ const scope = typeof e.entry?.scope === "string" ? e.entry.scope : "always";
5787
+ const authorities = Array.isArray(e.entry?.authorities) ? e.entry.authorities.filter((a) => typeof a === "string") : [];
5788
+ closures.push({ scope, authorities, at: e.at, label: e.name, ...typeof e.entry?.max_age_days === "number" ? { max_age_days: e.entry.max_age_days } : {} });
5789
+ } else if (e.kind === "recognizes") {
5790
+ recognitions.push({ to: e.name, scope: typeof e.entry?.scope === "string" ? e.entry.scope : "always" });
5791
+ } else if (e.kind === "superseded") {
5792
+ if (typeof e.entry?.by === "string")
5793
+ supersessions.push({ superseded: e.name, by: e.entry.by, from: e.at });
5794
+ }
5795
+ }
5796
+ return {
5797
+ id: o.id,
5798
+ certified_scope: o.certifiedScope ?? "always",
5799
+ claims: [...closedClaims, ...open.values()],
5800
+ closures,
5801
+ recognitions,
5802
+ supersessions,
5803
+ revoked: [...revoked].sort(),
5804
+ head
5805
+ };
5806
+ }
5807
+
5473
5808
  // src/verify-chain.ts
5474
5809
  var CHAIN_PROTOCOL_VERSION2 = "1";
5475
5810
  var DEFAULT_BASE = "https://www.nomosprotocol.com";
@@ -5646,6 +5981,68 @@ ${tool}`);
5646
5981
  return lines.join("\n");
5647
5982
  }
5648
5983
 
5984
+ // src/act/registrars.ts
5985
+ var RegistrarUnavailable = class extends Error {
5986
+ constructor(message, retryAfterSeconds, registrar) {
5987
+ super(message);
5988
+ this.retryAfterSeconds = retryAfterSeconds;
5989
+ this.registrar = registrar;
5990
+ this.name = "RegistrarUnavailable";
5991
+ }
5992
+ };
5993
+ var PAGE = 1e3;
5994
+ function registrarSource(o) {
5995
+ const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
5996
+ const base = o.url.replace(/\/$/, "");
5997
+ const maxHeadAge = o.maxHeadAgeSeconds ?? 600, refresh = (o.refreshSeconds ?? 60) * 1e3;
5998
+ let view = null, kept = o.initialHead ?? null;
5999
+ let fetchedAt = 0, inFlight = null, lastError = "";
6000
+ async function getJson2(path) {
6001
+ const res = await f(`${base}${path}`, { headers: { accept: "application/json" } });
6002
+ if (!res.ok) throw new Error(`${base}${path} answered ${res.status}`);
6003
+ return res.json();
6004
+ }
6005
+ async function load() {
6006
+ fetchedAt = now().getTime();
6007
+ try {
6008
+ const head = await getJson2("/.well-known/nomos-exchange-log/head");
6009
+ const entries = [];
6010
+ for (let after = 0; ; ) {
6011
+ const page2 = await getJson2(`/.well-known/nomos-exchange-log?after=${after}&limit=${PAGE}`);
6012
+ const got = Array.isArray(page2?.entries) ? page2.entries : [];
6013
+ entries.push(...got);
6014
+ if (got.length < PAGE || !page2.next) break;
6015
+ after = got[got.length - 1].seq;
6016
+ }
6017
+ const vouched = entries.slice(0, Math.min(entries.length, Number(head?.size) || 0));
6018
+ const check = verifyRegistrarLog({ entries: vouched, head, publicKeyPem: o.publicKeyPem, now: now(), maxHeadAgeSeconds: maxHeadAge, previous: kept });
6019
+ if (!check.ok) {
6020
+ lastError = `${check.reason}: ${check.detail}`;
6021
+ if (check.reason === "history_rewritten" || check.reason === "wrong_key" || check.reason === "bad_signature" || check.reason === "chain_broken") view = null;
6022
+ return;
6023
+ }
6024
+ view = { record: foldRegistrarLog(vouched, { id: o.id, certifiedScope: o.certifiedScope }), head: check.head };
6025
+ kept = { size: check.head.size, hash: check.head.hash };
6026
+ lastError = "";
6027
+ } catch (e) {
6028
+ lastError = String(e?.message ?? e);
6029
+ }
6030
+ }
6031
+ const source = (async () => {
6032
+ const t = now().getTime();
6033
+ if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
6034
+ inFlight = null;
6035
+ });
6036
+ if (inFlight) await inFlight;
6037
+ if (!view || t - Date.parse(view.head.at) > maxHeadAge * 1e3) {
6038
+ throw new RegistrarUnavailable(`Registrar ${o.id} (${base}) has no current, verified log${lastError ? `: ${lastError}` : ""}. Not committing until the law that applies can be read.`, Math.max(1, Math.ceil(refresh / 1e3)), o.id);
6039
+ }
6040
+ return view;
6041
+ });
6042
+ source.id = o.id;
6043
+ return source;
6044
+ }
6045
+
5649
6046
  // src/act/bytes.ts
5650
6047
  var utf82 = (s) => new TextEncoder().encode(s);
5651
6048
  var fromUtf82 = (b) => new TextDecoder().decode(b);
@@ -5926,7 +6323,7 @@ var STATUS = { AUTHORIZED: 201, ESCALATED: 202, INCOMPLETE: 422, DENIED: 403, RE
5926
6323
  var statusFor = (decision) => STATUS[decision] ?? 400;
5927
6324
  var ACT_MEDIA_TYPE = "application/nomos-act+json";
5928
6325
  var HINTS = {
5929
- INCOMPLETE: (v) => `Missing facts that could change the outcome: ${(v.open_facts ?? []).join(", ")}. Bring them as testimony from a witness certified for each claim, or as the channel the authority declares.`,
6326
+ INCOMPLETE: (v) => v.reason_code === "laws_not_presented" ? `Other law also governs this act here: ${(v.laws ?? []).join(", ")}. This system commits only when every governing law is carried; this Act carries one.` : v.reason_code === "resolution_incomplete" ? `This system cannot yet establish which law governs this act${v.missing?.context_facts?.length ? `: its own knowledge of the act lacks ${v.missing.context_facts.join(", ")}` : v.missing?.stale_closures?.length ? ": a registrar it honours does not cover this case" : ""}. Nothing the carrier sends can supply this; nothing is committed while the governing law is unknown.` : v.reason_code === "resolution_conflict" ? "Two registrars each claim exclusive precedence over this act. A person must settle which law governs before it can be committed." : `Missing facts that could change the outcome: ${(v.open_facts ?? []).join(", ")}. Bring them as testimony from a witness certified for each claim, or as the channel the authority declares.`,
5930
6327
  ESCALATED: (v) => `Needs ${v.consents_required} approval(s) from role ${v.role}. Request consent over act_digest ${v.act_digest} and resubmit this same Act before ${v.retry_until}.`,
5931
6328
  DENIED: (v) => `Rule ${v.rule ?? "(default)"} refuses this action. No approval can change a denial.`,
5932
6329
  WRONG_RELYING_PARTY: () => "The Act is addressed to another system. Build it with this relying_party_id.",
@@ -6039,6 +6436,24 @@ function accept(o) {
6039
6436
  maxWindowSeconds
6040
6437
  });
6041
6438
  }
6439
+ async function resolveFor(act2, authority, facts, at, reuse) {
6440
+ if (!o.resolution) return null;
6441
+ const views = reuse ?? await Promise.all(o.resolution.registrars.map(async (src) => [src.id, await src()]));
6442
+ const context = { ...await o.resolution.context(act2, facts) };
6443
+ const r = resolveApplicableSet(
6444
+ { pins: views.map(([id]) => id), registrars: Object.fromEntries(views.map(([id, v]) => [id, v.record])) },
6445
+ { context, at: at.toISOString() }
6446
+ );
6447
+ const heads = Object.fromEntries(views.map(([id, v]) => [id, v.head.hash]));
6448
+ const applicable = r.status === "RESOLVED" ? [...new Set(r.applicable.map((m) => m.authority))].sort() : [];
6449
+ const info = { status: r.status, context, applicable, heads };
6450
+ if (r.status === "INCOMPLETE") return { ok: false, info, views, verdict: { decision: "INCOMPLETE", reason_code: "resolution_incomplete", missing: r.missing } };
6451
+ if (r.status === "CONFLICT") return { ok: false, info, views, verdict: { decision: "INCOMPLETE", reason_code: "resolution_conflict", conflicts: r.conflicts } };
6452
+ const carried = new Set([authority.registrarName, authority.artifactId].filter((x) => !!x));
6453
+ const notCarried = applicable.filter((a) => !carried.has(a));
6454
+ if (notCarried.length) return { ok: false, info: { ...info, laws_not_presented: notCarried }, views, verdict: { decision: "INCOMPLETE", reason_code: "laws_not_presented", laws: notCarried } };
6455
+ return { ok: true, info, views };
6456
+ }
6042
6457
  async function handle(req) {
6043
6458
  const ctype = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
6044
6459
  if ((req.method ?? "POST").toUpperCase() !== "POST") return { status: 405, headers: { allow: "POST", "content-type": "application/json" }, body: { error: "POST an Act here." } };
@@ -6085,8 +6500,31 @@ function accept(o) {
6085
6500
  if (verdict.decision === "REPLAYED") {
6086
6501
  return answer(verdict, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
6087
6502
  }
6503
+ let resolved = null;
6504
+ if (verdict.decision === "AUTHORIZED" && o.resolution) {
6505
+ try {
6506
+ resolved = await resolveFor(act2, authority, facts, verifiedAt);
6507
+ } catch (e) {
6508
+ if (!(e instanceof RegistrarUnavailable)) throw e;
6509
+ return {
6510
+ status: 503,
6511
+ headers: { "content-type": "application/json", "retry-after": String(e.retryAfterSeconds) },
6512
+ body: { error: "The law that governs this act cannot be read right now, so nothing is committed. Resubmit this same Act later.", detail: e.message, registrar: e.registrar, retry_after: e.retryAfterSeconds }
6513
+ };
6514
+ }
6515
+ if (resolved && !resolved.ok) {
6516
+ const held = { ...resolved.verdict, act_id: act2.act_id, act_digest: verdict.act_digest, resolution: resolved.info };
6517
+ await o.onVerdict?.(held, { ...receiptOf(held, facts), resolution: resolved.info });
6518
+ if (o.mode === "shadow") {
6519
+ const a = answer(held);
6520
+ return { ...a, status: 200, body: { ...a.body, shadow: true } };
6521
+ }
6522
+ return answer(held);
6523
+ }
6524
+ }
6525
+ const withResolution = (r) => resolved?.ok ? { ...r, resolution: resolved.info } : r;
6088
6526
  if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
6089
- await o.onVerdict?.(verdict, receiptOf(verdict, facts));
6527
+ await o.onVerdict?.(verdict, withResolution(receiptOf(verdict, facts)));
6090
6528
  if (verdict.decision === "ESCALATED") {
6091
6529
  const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
6092
6530
  return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
@@ -6104,17 +6542,23 @@ function accept(o) {
6104
6542
  finalFacts = fresh;
6105
6543
  final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
6106
6544
  if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
6545
+ if (o.resolution) {
6546
+ const again = await resolveFor(act2, authority, fresh, verifiedAt, resolved?.views);
6547
+ if (again && !again.ok) throw new Rollback(answer({ ...again.verdict, act_id: act2.act_id, resolution: again.info }));
6548
+ resolved = again;
6549
+ }
6107
6550
  }
6108
6551
  }
6109
6552
  const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
6110
6553
  const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
6111
- const receipt = receiptOf(final, finalFacts, now());
6554
+ const receipt = withResolution(receiptOf(final, finalFacts, now()));
6112
6555
  await t.putReceipt(act2.nonce, receipt, act2);
6113
6556
  await o.onVerdict?.(final, receipt);
6114
6557
  return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
6115
6558
  });
6116
6559
  } catch (e) {
6117
6560
  if (e instanceof Rollback) return e.answer;
6561
+ if (e instanceof RegistrarUnavailable) return { status: 503, headers: { "content-type": "application/json", "retry-after": String(e.retryAfterSeconds) }, body: { error: "The law that governs this act cannot be read right now, so nothing is committed. Resubmit this same Act later.", detail: e.message, registrar: e.registrar, retry_after: e.retryAfterSeconds } };
6118
6562
  if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
6119
6563
  return { status: 500, headers: { "content-type": "application/json" }, body: { error: "The commit failed and was rolled back; nothing was applied.", detail: String(e?.message ?? e) } };
6120
6564
  }
@@ -6879,6 +7323,7 @@ var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js)
6879
7323
  var CSP = () => `default-src 'none'; script-src ${scriptHash(APPROVE_JS)} ${scriptHash(ENROL_JS)}; style-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'`;
6880
7324
 
6881
7325
  // src/act/carrier.ts
7326
+ var RESOLUTION_HOLDS = /* @__PURE__ */ new Set(["laws_not_presented", "resolution_incomplete", "resolution_conflict"]);
6882
7327
  var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
6883
7328
  var NomosActError = class extends Error {
6884
7329
  constructor(message) {
@@ -6898,9 +7343,9 @@ async function act(o) {
6898
7343
  const entry = issuer2.authorities.find((a) => a.artifact_id === accepted.artifact_id);
6899
7344
  if (!entry) throw new NomosActError(`The issuer at ${accepted.issuer} does not publish ${accepted.artifact_id}.`);
6900
7345
  const artifact = await getJson(f, entry.artifact_url, "the authority");
6901
- let inForce;
7346
+ let inForce2;
6902
7347
  if (accepted.currency_policy === "record_or_statement" && !accepted.in_force_seal_hash && entry.in_force_url) {
6903
- inForce = await getJson(f, entry.in_force_url, "the in-force statement").catch(() => void 0);
7348
+ inForce2 = await getJson(f, entry.in_force_url, "the in-force statement").catch(() => void 0);
6904
7349
  }
6905
7350
  const inputs = artifact?.data_contract?.inputs ?? {};
6906
7351
  const witnessed = Object.entries(inputs).filter(([, i]) => i?.source === "testimony").map(([n]) => n);
@@ -6940,7 +7385,7 @@ async function act(o) {
6940
7385
  testimony.push(t);
6941
7386
  if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
6942
7387
  }
6943
- const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
7388
+ const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce: inForce2, windowSeconds });
6944
7389
  present(theAct, available);
6945
7390
  let { status, body } = await submit(f, rp.act_endpoint, theAct);
6946
7391
  let approvalUrl;
@@ -6968,6 +7413,10 @@ async function act(o) {
6968
7413
  }
6969
7414
  }
6970
7415
  const kind = STATUS_OF[body.decision] ?? "refused";
7416
+ if (kind === "incomplete" && RESOLUTION_HOLDS.has(body.reason_code)) {
7417
+ const laws = Array.isArray(body.laws) ? body.laws : [];
7418
+ return done("incomplete", body, theAct, { missing: [], summary: body.reason_code === "laws_not_presented" ? `Not committed: other law also governs this act at ${rp.relying_party_id} (${laws.join(", ")}), and this Act carries only ${theAct.authority?.artifact?.meta?.artifact_id ?? "one authority"}.` : `Not committed: ${body.hint ?? `${rp.relying_party_id} cannot yet settle which law governs this act (${body.reason_code}).`}` });
7419
+ }
6971
7420
  if (kind === "incomplete") {
6972
7421
  const missing = (body.open_facts ?? []).map((fact) => {
6973
7422
  const why = unavailable.find((u) => u.fact === fact || fact.startsWith(u.fact + "."));
@@ -6999,6 +7448,7 @@ async function getJson(f, url, what) {
6999
7448
  async function submit(f, endpoint, theAct) {
7000
7449
  const res = await f(endpoint, { method: "POST", headers: { "content-type": ACT_MEDIA_TYPE }, body: JSON.stringify(theAct) });
7001
7450
  const body = await res.json().catch(() => ({ decision: "MALFORMED", reason_code: "unreadable_response" }));
7451
+ if (res.status === 503) throw new NomosActError(`The relying party is not committing right now (503): ${body?.error ?? ""}${body?.retry_after ? ` Resubmit this same Act after ${body.retry_after} s.` : ""}`);
7002
7452
  if (res.status >= 500) throw new NomosActError(`The relying party failed to commit (${res.status}): ${body?.error ?? ""}`);
7003
7453
  return { status: res.status, body };
7004
7454
  }
@@ -7202,6 +7652,7 @@ ${buttons}</body>`;
7202
7652
  NomosRateLimitError,
7203
7653
  NomosValidationError,
7204
7654
  NomosWebhookSignatureError,
7655
+ RegistrarUnavailable,
7205
7656
  RegistryError,
7206
7657
  RevocationUnavailable,
7207
7658
  WELL_KNOWN,
@@ -7232,6 +7683,7 @@ ${buttons}</body>`;
7232
7683
  postgresStore,
7233
7684
  presentedCertificates,
7234
7685
  readAttestation,
7686
+ registrarSource,
7235
7687
  reliedKids,
7236
7688
  revocationListCertificates,
7237
7689
  revocationSource,