@nomosprotocol/sdk 2.6.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/README.md CHANGED
@@ -97,6 +97,39 @@ can return their own chains with what they sign (`keyCerts`). On the relying par
97
97
  (NOMOS-SPEC-007 rev. 1.10). Revocations are never forgotten, and without a current list
98
98
  `accept()` answers 503 instead of committing.
99
99
 
100
+ **Which laws govern the act (2.7.0).** An Act carries one authority, so on its own `accept()` checks
101
+ the law the carrier chose. Pin registrars and `accept()` works out every law that governs the act here,
102
+ now, and commits only if the Act carries every one (NOMOS-SPEC-014, Draft):
103
+
104
+ ```ts
105
+ import { accept, registrarSource } from '@nomosprotocol/sdk';
106
+
107
+ const platform = registrarSource({
108
+ id: 'platform',
109
+ url: 'https://www.nomosprotocol.com',
110
+ publicKeyPem: PLATFORM_KEY_PEM, // pinned by you; heads signed by any other key are refused
111
+ });
112
+
113
+ const rp = accept({
114
+ // ...relyingPartyId, root, authorities (with registrarName), commit, as above
115
+ resolution: {
116
+ registrars: [platform],
117
+ // From YOUR knowledge and state, never from the Act: a carrier that stated its own
118
+ // jurisdiction would choose the law again.
119
+ context: (act, localFacts) => ({ jurisdiction: 'AE', established_in: 'DIFC', subjects_in: 'AE', output_used_in: 'AE' }),
120
+ },
121
+ });
122
+ ```
123
+
124
+ If another law also governs the act, the Act is held (`422 INCOMPLETE`, `reason_code:
125
+ 'laws_not_presented'`, the laws named) and nothing is committed. A context fact you leave out holds it
126
+ too (`resolution_incomplete`, naming the fact). A registrar that cannot be read, a head older than
127
+ `maxHeadAgeSeconds`, a head signed by a key you did not pin, or a log whose history was rewritten
128
+ answers 503 with Retry-After. Receipts record the resolution (status, context, laws, log heads).
129
+ Create each `registrarSource()` once and reuse it: the check that a registrar's history was only ever
130
+ added to spans the source's lifetime (pass `initialHead` to carry the last head across restarts).
131
+ Without `resolution`, `accept()` is unchanged.
132
+
100
133
  **The other roles** are one call each:
101
134
  - `issuer()` publishes your sealed policies and certificates;
102
135
  - `witness()` turns an existing lookup into signed statements;
@@ -168,8 +201,8 @@ No. 45 of 2021 and DIFC Law No. 5 of 2020, and lists only its own laws, so other
168
201
  apply. Only want the list of laws? `POST /api/v1/resolve { context }` returns it without asking
169
202
  them, and records nothing.
170
203
 
171
- `accept()` still verifies the one authority an Act carries. Resolving inside `accept()`, from
172
- registrars the receiving system pins, is the next step of NOMOS-SPEC-014.
204
+ The system that commits the change can resolve too, from registrars it pins and a context it supplies
205
+ itself: see `accept({ resolution })` above (2.7.0).
173
206
 
174
207
  ### 1. Public authority
175
208
 
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.6.0";
196
+ var SDK_VERSION = "2.7.0";
195
197
  var HttpClient = class {
196
198
  constructor(opts) {
197
199
  this.opts = opts;
@@ -5258,7 +5260,7 @@ function verifyUnsafe(a, o) {
5258
5260
  const now = (o.now ?? /* @__PURE__ */ new Date()).getTime();
5259
5261
  const nowDate = new Date(now);
5260
5262
  const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
5261
- const localFacts = o.localFacts ?? {}, inForce = o.inForce ?? {};
5263
+ const localFacts = o.localFacts ?? {}, inForce2 = o.inForce ?? {};
5262
5264
  const need = ["act_version", "act_id", "nonce", "issued_at", "expires_at", "relying_party", "action", "authority"];
5263
5265
  if (!isObject(a) || need.some((k) => !(k in a)) || a.act_version !== "1" || !isObject(a.authority))
5264
5266
  return { decision: "MALFORMED", reason_code: "bad_envelope" };
@@ -5302,9 +5304,9 @@ function verifyUnsafe(a, o) {
5302
5304
  if (o.revokedArtifacts?.has(seal2.hash))
5303
5305
  return { decision: "ARTIFACT_REVOKED", artifact_id: artId };
5304
5306
  let inForceSource;
5305
- if (typeof artId === "string" && Object.prototype.hasOwnProperty.call(inForce, artId)) {
5306
- if (inForce[artId] !== seal2.hash)
5307
- 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] };
5308
5310
  inForceSource = "record";
5309
5311
  } else if (o.currencyPolicy === "record") {
5310
5312
  return { decision: "VERSION_UNVERIFIED", artifact_id: artId, reason_code: "not_in_record" };
@@ -5521,6 +5523,288 @@ function verifyRevocationList(list, o) {
5521
5523
  return out;
5522
5524
  }
5523
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
+
5524
5808
  // src/verify-chain.ts
5525
5809
  var CHAIN_PROTOCOL_VERSION2 = "1";
5526
5810
  var DEFAULT_BASE = "https://www.nomosprotocol.com";
@@ -5697,6 +5981,68 @@ ${tool}`);
5697
5981
  return lines.join("\n");
5698
5982
  }
5699
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
+
5700
6046
  // src/act/bytes.ts
5701
6047
  var utf82 = (s) => new TextEncoder().encode(s);
5702
6048
  var fromUtf82 = (b) => new TextDecoder().decode(b);
@@ -5977,7 +6323,7 @@ var STATUS = { AUTHORIZED: 201, ESCALATED: 202, INCOMPLETE: 422, DENIED: 403, RE
5977
6323
  var statusFor = (decision) => STATUS[decision] ?? 400;
5978
6324
  var ACT_MEDIA_TYPE = "application/nomos-act+json";
5979
6325
  var HINTS = {
5980
- 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.`,
5981
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}.`,
5982
6328
  DENIED: (v) => `Rule ${v.rule ?? "(default)"} refuses this action. No approval can change a denial.`,
5983
6329
  WRONG_RELYING_PARTY: () => "The Act is addressed to another system. Build it with this relying_party_id.",
@@ -6090,6 +6436,24 @@ function accept(o) {
6090
6436
  maxWindowSeconds
6091
6437
  });
6092
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
+ }
6093
6457
  async function handle(req) {
6094
6458
  const ctype = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
6095
6459
  if ((req.method ?? "POST").toUpperCase() !== "POST") return { status: 405, headers: { allow: "POST", "content-type": "application/json" }, body: { error: "POST an Act here." } };
@@ -6136,8 +6500,31 @@ function accept(o) {
6136
6500
  if (verdict.decision === "REPLAYED") {
6137
6501
  return answer(verdict, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
6138
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;
6139
6526
  if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
6140
- await o.onVerdict?.(verdict, receiptOf(verdict, facts));
6527
+ await o.onVerdict?.(verdict, withResolution(receiptOf(verdict, facts)));
6141
6528
  if (verdict.decision === "ESCALATED") {
6142
6529
  const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
6143
6530
  return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
@@ -6155,17 +6542,23 @@ function accept(o) {
6155
6542
  finalFacts = fresh;
6156
6543
  final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
6157
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
+ }
6158
6550
  }
6159
6551
  }
6160
6552
  const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
6161
6553
  const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
6162
- const receipt = receiptOf(final, finalFacts, now());
6554
+ const receipt = withResolution(receiptOf(final, finalFacts, now()));
6163
6555
  await t.putReceipt(act2.nonce, receipt, act2);
6164
6556
  await o.onVerdict?.(final, receipt);
6165
6557
  return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
6166
6558
  });
6167
6559
  } catch (e) {
6168
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 } };
6169
6562
  if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
6170
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) } };
6171
6564
  }
@@ -6930,6 +7323,7 @@ var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js)
6930
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'`;
6931
7324
 
6932
7325
  // src/act/carrier.ts
7326
+ var RESOLUTION_HOLDS = /* @__PURE__ */ new Set(["laws_not_presented", "resolution_incomplete", "resolution_conflict"]);
6933
7327
  var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
6934
7328
  var NomosActError = class extends Error {
6935
7329
  constructor(message) {
@@ -6949,9 +7343,9 @@ async function act(o) {
6949
7343
  const entry = issuer2.authorities.find((a) => a.artifact_id === accepted.artifact_id);
6950
7344
  if (!entry) throw new NomosActError(`The issuer at ${accepted.issuer} does not publish ${accepted.artifact_id}.`);
6951
7345
  const artifact = await getJson(f, entry.artifact_url, "the authority");
6952
- let inForce;
7346
+ let inForce2;
6953
7347
  if (accepted.currency_policy === "record_or_statement" && !accepted.in_force_seal_hash && entry.in_force_url) {
6954
- 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);
6955
7349
  }
6956
7350
  const inputs = artifact?.data_contract?.inputs ?? {};
6957
7351
  const witnessed = Object.entries(inputs).filter(([, i]) => i?.source === "testimony").map(([n]) => n);
@@ -6991,7 +7385,7 @@ async function act(o) {
6991
7385
  testimony.push(t);
6992
7386
  if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
6993
7387
  }
6994
- 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 });
6995
7389
  present(theAct, available);
6996
7390
  let { status, body } = await submit(f, rp.act_endpoint, theAct);
6997
7391
  let approvalUrl;
@@ -7019,6 +7413,10 @@ async function act(o) {
7019
7413
  }
7020
7414
  }
7021
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
+ }
7022
7420
  if (kind === "incomplete") {
7023
7421
  const missing = (body.open_facts ?? []).map((fact) => {
7024
7422
  const why = unavailable.find((u) => u.fact === fact || fact.startsWith(u.fact + "."));
@@ -7050,6 +7448,7 @@ async function getJson(f, url, what) {
7050
7448
  async function submit(f, endpoint, theAct) {
7051
7449
  const res = await f(endpoint, { method: "POST", headers: { "content-type": ACT_MEDIA_TYPE }, body: JSON.stringify(theAct) });
7052
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.` : ""}`);
7053
7452
  if (res.status >= 500) throw new NomosActError(`The relying party failed to commit (${res.status}): ${body?.error ?? ""}`);
7054
7453
  return { status: res.status, body };
7055
7454
  }
@@ -7253,6 +7652,7 @@ ${buttons}</body>`;
7253
7652
  NomosRateLimitError,
7254
7653
  NomosValidationError,
7255
7654
  NomosWebhookSignatureError,
7655
+ RegistrarUnavailable,
7256
7656
  RegistryError,
7257
7657
  RevocationUnavailable,
7258
7658
  WELL_KNOWN,
@@ -7283,6 +7683,7 @@ ${buttons}</body>`;
7283
7683
  postgresStore,
7284
7684
  presentedCertificates,
7285
7685
  readAttestation,
7686
+ registrarSource,
7286
7687
  reliedKids,
7287
7688
  revocationListCertificates,
7288
7689
  revocationSource,