@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.js CHANGED
@@ -107,7 +107,7 @@ function isRetryable(status) {
107
107
  }
108
108
 
109
109
  // src/http-client.ts
110
- var SDK_VERSION = "2.5.0";
110
+ var SDK_VERSION = "2.7.0";
111
111
  var HttpClient = class {
112
112
  constructor(opts) {
113
113
  this.opts = opts;
@@ -774,6 +774,50 @@ async function canViaChain(artifact, keyCerts, rootPublicKeyPem, facts, action,
774
774
  if (data?.decision === "MALFORMED") throw new NomosValidationError(message, data, requestId);
775
775
  mapHttpError(res.status, data, requestId);
776
776
  }
777
+ function normalizeResolutionResult(data) {
778
+ return {
779
+ mode: "resolution",
780
+ allowed: data.verdict === "AUTHORIZED",
781
+ verdict: data.verdict,
782
+ reason: data.reason ?? "",
783
+ resolution: data.resolution ?? { status: "INCOMPLETE", proof: null },
784
+ members: Array.isArray(data.members) ? data.members : [],
785
+ deciding_authorities: Array.isArray(data.deciding_authorities) ? data.deciding_authorities : [],
786
+ escalations: Array.isArray(data.escalations) ? data.escalations : [],
787
+ open_facts: Array.isArray(data.open_facts) ? data.open_facts : [],
788
+ ...Array.isArray(data.answered_under) ? { answered_under: data.answered_under } : {},
789
+ ...typeof data.coverage === "string" ? { coverage: data.coverage } : {}
790
+ };
791
+ }
792
+ function resolutionBody(params) {
793
+ const { authority, artifact_id, artifact, key_certs, root_public_key_pem, action, inputs, context, ...flat } = params;
794
+ void authority;
795
+ void artifact_id;
796
+ void artifact;
797
+ void key_certs;
798
+ void root_public_key_pem;
799
+ const facts = inputs && typeof inputs === "object" ? inputs : flat;
800
+ return { context, ...action ? { action } : {}, ...facts };
801
+ }
802
+ async function canViaResolution(params, opts) {
803
+ const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
804
+ let res;
805
+ try {
806
+ res = await fetch(`${base}/api/v1/can`, {
807
+ method: "POST",
808
+ headers: { "Content-Type": "application/json", "User-Agent": `nomos-node/${SDK_VERSION}`, ...callerHeaders(opts?.caller) },
809
+ body: JSON.stringify(resolutionBody(params)),
810
+ signal: opts?.signal
811
+ });
812
+ } catch (err) {
813
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
814
+ throw new NomosNetworkError(err?.message ?? "Network error");
815
+ }
816
+ const requestId = res.headers.get("x-request-id") ?? "";
817
+ const data = await res.json().catch(() => null);
818
+ if (!res.ok) mapHttpError(res.status, data, requestId);
819
+ return normalizeResolutionResult(data);
820
+ }
777
821
  async function can(params, opts) {
778
822
  if (params.artifact !== void 0) {
779
823
  if (!params.key_certs || !params.root_public_key_pem) {
@@ -785,8 +829,11 @@ async function can(params, opts) {
785
829
  if (params.artifact_id) {
786
830
  throw new Error("can(): `artifact_id` requires an API key \u2014 use `client.can(...)` on an authenticated Nomos instance, not the free `can()` export.");
787
831
  }
832
+ if (!params.authority && params.context && typeof params.context === "object" && !Array.isArray(params.context)) {
833
+ return canViaResolution(params, opts);
834
+ }
788
835
  if (!params.authority || typeof params.authority !== "string") {
789
- throw new Error("can(): `authority` (an Exchange artifact id or slug), `artifact_id` (via client.can()), or `artifact` (chain-of-trust) is required.");
836
+ 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.");
790
837
  }
791
838
  const { facts, action } = splitFacts(params);
792
839
  return canViaExchange(params.authority, facts, action, opts);
@@ -801,7 +848,11 @@ async function canAuthenticated(http, params, opts) {
801
848
  }
802
849
  if (params.artifact_id) return canViaArtifact(http, params.artifact_id, facts, action);
803
850
  if (params.authority) return canViaExchangeAuthenticated(http, params.authority, facts, action, opts);
804
- throw new Error("can(): `authority` (a public Exchange artifact), `artifact_id` (your own artifact), or `artifact` (chain-of-trust) is required.");
851
+ if (params.context && typeof params.context === "object" && !Array.isArray(params.context)) {
852
+ const data = (await http.post("/api/v1/can", resolutionBody(params), { headers: callerHeaders(opts?.caller) })).data;
853
+ return normalizeResolutionResult(data);
854
+ }
855
+ 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.");
805
856
  }
806
857
  var nomos = { can };
807
858
 
@@ -5123,7 +5174,7 @@ function verifyUnsafe(a, o) {
5123
5174
  const now = (o.now ?? /* @__PURE__ */ new Date()).getTime();
5124
5175
  const nowDate = new Date(now);
5125
5176
  const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
5126
- const localFacts = o.localFacts ?? {}, inForce = o.inForce ?? {};
5177
+ const localFacts = o.localFacts ?? {}, inForce2 = o.inForce ?? {};
5127
5178
  const need = ["act_version", "act_id", "nonce", "issued_at", "expires_at", "relying_party", "action", "authority"];
5128
5179
  if (!isObject(a) || need.some((k) => !(k in a)) || a.act_version !== "1" || !isObject(a.authority))
5129
5180
  return { decision: "MALFORMED", reason_code: "bad_envelope" };
@@ -5167,9 +5218,9 @@ function verifyUnsafe(a, o) {
5167
5218
  if (o.revokedArtifacts?.has(seal2.hash))
5168
5219
  return { decision: "ARTIFACT_REVOKED", artifact_id: artId };
5169
5220
  let inForceSource;
5170
- if (typeof artId === "string" && Object.prototype.hasOwnProperty.call(inForce, artId)) {
5171
- if (inForce[artId] !== seal2.hash)
5172
- return { decision: "SUPERSEDED", artifact_id: artId, in_force_seal_hash: inForce[artId] };
5221
+ if (typeof artId === "string" && Object.prototype.hasOwnProperty.call(inForce2, artId)) {
5222
+ if (inForce2[artId] !== seal2.hash)
5223
+ return { decision: "SUPERSEDED", artifact_id: artId, in_force_seal_hash: inForce2[artId] };
5173
5224
  inForceSource = "record";
5174
5225
  } else if (o.currencyPolicy === "record") {
5175
5226
  return { decision: "VERSION_UNVERIFIED", artifact_id: artId, reason_code: "not_in_record" };
@@ -5386,6 +5437,288 @@ function verifyRevocationList(list, o) {
5386
5437
  return out;
5387
5438
  }
5388
5439
 
5440
+ // node_modules/@nomosprotocol/verify/dist/resolution.js
5441
+ var DEFAULT_CLOSURE_MAX_AGE_DAYS = 90;
5442
+ function evaluateScope(condition, context) {
5443
+ if (condition === "always" || condition === "all")
5444
+ return { value: "TRUE", openFacts: [] };
5445
+ let out;
5446
+ try {
5447
+ out = evaluate({
5448
+ logic: {
5449
+ resolution: { conflict_policy: "highest_priority", tie_breaker: "deny_wins", default_outcome: "escalate" },
5450
+ decisions: [
5451
+ { id: "scope-when-true", when: condition, priority: 1, then: [{ type: "allow" }] },
5452
+ { id: "scope", when: condition, priority: 2, then: [{ type: "block" }] }
5453
+ ]
5454
+ }
5455
+ }, context);
5456
+ } catch {
5457
+ return { value: "UNDECIDED", openFacts: ["__scope_unparseable__"] };
5458
+ }
5459
+ if (out.outcome === "block")
5460
+ return { value: "TRUE", openFacts: [] };
5461
+ if (out.outcome === "undecided")
5462
+ return { value: "UNDECIDED", openFacts: [...out.open_facts] };
5463
+ return { value: "FALSE", openFacts: [] };
5464
+ }
5465
+ var inForce = (c, at) => (!c.from || c.from <= at) && (!c.until || c.until > at);
5466
+ var closureFresh = (c, at) => {
5467
+ const ageMs = Date.parse(at) - Date.parse(c.at);
5468
+ if (!Number.isFinite(ageMs) || ageMs < 0)
5469
+ return false;
5470
+ return ageMs <= (c.max_age_days ?? DEFAULT_CLOSURE_MAX_AGE_DAYS) * 864e5;
5471
+ };
5472
+ var closureReplaced = (c, all, at) => {
5473
+ const t = Date.parse(c.at), now = Date.parse(at);
5474
+ 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));
5475
+ };
5476
+ function resolveApplicableSet(world, query, opts = {}) {
5477
+ const at = query.at;
5478
+ const ctx = query.context ?? {};
5479
+ const proof = { at, pins: [...world.pins].sort(), heads: {}, closures: [], conflicts: [], dropped: [] };
5480
+ if (opts.ablate === "C5") {
5481
+ const applicable2 = (opts.presented ?? []).map((a) => ({ authority: a, version: null, registrar: "carrier", scope: "always" })).sort((x, y) => x.authority < y.authority ? -1 : 1);
5482
+ return { status: "RESOLVED", applicable: applicable2, proof };
5483
+ }
5484
+ const openFacts = /* @__PURE__ */ new Set();
5485
+ const unreachable = [];
5486
+ const staleClosures = [];
5487
+ const disagreements = [];
5488
+ const staleHeads = [];
5489
+ const memberAt = opts.ablate === "C7" && opts.snapshot ? opts.snapshot : at;
5490
+ const candidates2 = [];
5491
+ const visited = /* @__PURE__ */ new Set();
5492
+ const queue = [...world.pins].sort();
5493
+ while (queue.length > 0) {
5494
+ const id = queue.shift();
5495
+ if (visited.has(id))
5496
+ continue;
5497
+ visited.add(id);
5498
+ const reg = world.registrars[id];
5499
+ if (!reg) {
5500
+ if (opts.ablate !== "C8")
5501
+ unreachable.push(id);
5502
+ continue;
5503
+ }
5504
+ proof.heads[id] = reg.head ?? null;
5505
+ if (opts.ablate !== "C8" && typeof world.max_head_age_seconds === "number") {
5506
+ const age = reg.head_at ? Date.parse(at) - Date.parse(reg.head_at) : NaN;
5507
+ if (!Number.isFinite(age) || age > world.max_head_age_seconds * 1e3)
5508
+ staleHeads.push(id);
5509
+ }
5510
+ const cert = evaluateScope(reg.certified_scope, ctx);
5511
+ for (const f of cert.openFacts)
5512
+ openFacts.add(f);
5513
+ for (const e of [...reg.recognitions ?? []].sort((a, b) => a.to < b.to ? -1 : 1)) {
5514
+ const s = evaluateScope(e.scope, ctx);
5515
+ if (s.value === "TRUE")
5516
+ queue.push(e.to);
5517
+ for (const f of s.openFacts)
5518
+ openFacts.add(f);
5519
+ }
5520
+ const closedLists = [];
5521
+ if (opts.ablate !== "C8") {
5522
+ let covered = false;
5523
+ for (const c of reg.closures) {
5524
+ if (closureReplaced(c, reg.closures, at))
5525
+ continue;
5526
+ const s = evaluateScope(c.scope, ctx);
5527
+ for (const f of s.openFacts)
5528
+ openFacts.add(f);
5529
+ if (s.value !== "TRUE")
5530
+ continue;
5531
+ const fresh = closureFresh(c, at);
5532
+ proof.closures.push({ registrar: id, scope: c.scope, at: c.at, fresh });
5533
+ if (fresh) {
5534
+ covered = true;
5535
+ closedLists.push(c.authorities);
5536
+ }
5537
+ }
5538
+ if (!covered)
5539
+ staleClosures.push(id);
5540
+ }
5541
+ const supersededNow = new Set((reg.supersessions ?? []).filter((s) => s.from <= memberAt).map((s) => s.superseded));
5542
+ const revoked = new Set(reg.revoked ?? []);
5543
+ const liveAuthorities = /* @__PURE__ */ new Set();
5544
+ const trueAuthorities = /* @__PURE__ */ new Set();
5545
+ for (const claim of reg.claims) {
5546
+ if (opts.ablate !== "C7" || opts.snapshot) {
5547
+ if (!inForce(claim, memberAt)) {
5548
+ proof.dropped.push({ authority: claim.authority, registrar: id, reason: "not_in_force" });
5549
+ continue;
5550
+ }
5551
+ if (supersededNow.has(claim.authority)) {
5552
+ proof.dropped.push({ authority: claim.authority, registrar: id, reason: "superseded" });
5553
+ continue;
5554
+ }
5555
+ if (revoked.has(claim.authority)) {
5556
+ proof.dropped.push({ authority: claim.authority, registrar: id, reason: "revoked" });
5557
+ continue;
5558
+ }
5559
+ }
5560
+ liveAuthorities.add(claim.authority);
5561
+ const s = evaluateScope(claim.scope, ctx);
5562
+ for (const f of s.openFacts)
5563
+ openFacts.add(f);
5564
+ if (s.value === "TRUE")
5565
+ trueAuthorities.add(claim.authority);
5566
+ if (s.value === "FALSE")
5567
+ continue;
5568
+ const within = opts.ablate === "C6" ? { value: "TRUE", openFacts: [] } : cert;
5569
+ if (s.value === "UNDECIDED" || within.value === "UNDECIDED")
5570
+ continue;
5571
+ if (within.value === "FALSE")
5572
+ continue;
5573
+ candidates2.push({ authority: claim.authority, version: claim.version ?? null, registrar: id, scope: claim.scope, exclusive: claim.exclusive === true });
5574
+ }
5575
+ if (opts.ablate !== "C8" && closedLists.length > 0) {
5576
+ for (const a of new Set(closedLists.flat())) {
5577
+ if (!liveAuthorities.has(a))
5578
+ disagreements.push({ registrar: id, authority: a, reason: "closed_but_not_claimed" });
5579
+ }
5580
+ for (const a of trueAuthorities) {
5581
+ if (!closedLists.some((l) => l.includes(a)))
5582
+ disagreements.push({ registrar: id, authority: a, reason: "claimed_but_not_closed" });
5583
+ }
5584
+ }
5585
+ }
5586
+ proof.dropped.sort((a, b) => a.authority + a.registrar < b.authority + b.registrar ? -1 : 1);
5587
+ proof.closures.sort((a, b) => a.registrar + a.scope + a.at < b.registrar + b.scope + b.at ? -1 : 1);
5588
+ if (openFacts.size > 0 || unreachable.length > 0 || staleClosures.length > 0 || disagreements.length > 0 || staleHeads.length > 0) {
5589
+ return {
5590
+ status: "INCOMPLETE",
5591
+ missing: {
5592
+ context_facts: [...openFacts].sort(),
5593
+ unreachable_registrars: unreachable.sort(),
5594
+ stale_closures: staleClosures.sort(),
5595
+ stale_heads: staleHeads.sort(),
5596
+ closure_disagreements: disagreements.sort((a, b) => a.registrar + "\0" + a.authority < b.registrar + "\0" + b.authority ? -1 : 1)
5597
+ },
5598
+ proof
5599
+ };
5600
+ }
5601
+ const exclusives = candidates2.filter((c) => c.exclusive);
5602
+ const conflictPairs = exclusives.filter((c) => exclusives.some((o) => o.registrar !== c.registrar));
5603
+ if (conflictPairs.length > 0) {
5604
+ proof.conflicts.push({
5605
+ 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)
5606
+ });
5607
+ return { status: "CONFLICT", conflicts: proof.conflicts, proof };
5608
+ }
5609
+ const seen = /* @__PURE__ */ new Set();
5610
+ const applicable = [];
5611
+ for (const c of [...candidates2].sort((a, b) => a.authority + "\0" + a.registrar < b.authority + "\0" + b.registrar ? -1 : 1)) {
5612
+ const key = c.authority + "\0" + c.registrar;
5613
+ if (seen.has(key))
5614
+ continue;
5615
+ seen.add(key);
5616
+ applicable.push({ authority: c.authority, version: c.version, registrar: c.registrar, scope: c.scope });
5617
+ }
5618
+ return { status: "RESOLVED", applicable, proof };
5619
+ }
5620
+
5621
+ // node_modules/@nomosprotocol/verify/dist/registrar.js
5622
+ var LOG_GENESIS = "0".repeat(64);
5623
+ var registrarHeadPayload = (h, log = "nomos-exchange-log") => jcs({ log, size: h.size, hash: h.hash, at: h.at });
5624
+ function verifyLogChain(entries) {
5625
+ let prev = LOG_GENESIS;
5626
+ const hashAt = /* @__PURE__ */ new Map([[0, LOG_GENESIS]]);
5627
+ for (const [i, e] of entries.entries()) {
5628
+ if (e.seq !== i + 1)
5629
+ return { ok: false, detail: `entry ${i + 1}: its seq is ${e.seq}` };
5630
+ if (e.prev_hash !== prev)
5631
+ return { ok: false, detail: `entry ${e.seq}: prev_hash does not match entry ${e.seq - 1}` };
5632
+ const { hash, ...body } = e;
5633
+ if (sha256Hex(jcs(body)) !== hash)
5634
+ return { ok: false, detail: `entry ${e.seq}: its hash does not match its contents` };
5635
+ prev = hash;
5636
+ hashAt.set(e.seq, hash);
5637
+ }
5638
+ return { ok: true, hashAt };
5639
+ }
5640
+ function verifyRegistrarLog(o) {
5641
+ const fail2 = (reason, detail) => ({ ok: false, reason, detail });
5642
+ const chain = verifyLogChain(o.entries);
5643
+ if (!chain.ok)
5644
+ return fail2("chain_broken", chain.detail);
5645
+ const h = o.head;
5646
+ if (!h || typeof h.size !== "number" || h.size > o.entries.length || chain.hashAt.get(h.size) !== h.hash) {
5647
+ return fail2("head_mismatch", `the head (size ${h?.size}) does not name an entry of this chain`);
5648
+ }
5649
+ if (!h.signature || !h.kid)
5650
+ return fail2("head_unsigned", "the head is not signed");
5651
+ const pinnedKid = kidOf(o.publicKeyPem);
5652
+ if (!pinnedKid || h.kid !== pinnedKid)
5653
+ return fail2("wrong_key", `the head is signed by ${h.kid}, not the pinned key ${pinnedKid ?? "(unreadable)"}`);
5654
+ if (!verifySuite("Ed25519", o.publicKeyPem, registrarHeadPayload(h, o.log), { signature: h.signature }).ok)
5655
+ return fail2("bad_signature", "the head signature does not verify under the pinned key");
5656
+ const age = o.now.getTime() - Date.parse(h.at);
5657
+ if (!Number.isFinite(age))
5658
+ return fail2("head_stale", "the head carries no time");
5659
+ if (age < -6e4)
5660
+ return fail2("head_in_future", `the head is dated ${h.at}, after now`);
5661
+ if (age > o.maxHeadAgeSeconds * 1e3)
5662
+ return fail2("head_stale", `the head is ${Math.round(age / 1e3)} s old; the bound is ${o.maxHeadAgeSeconds} s`);
5663
+ if (o.previous && chain.hashAt.get(o.previous.size) !== o.previous.hash) {
5664
+ return fail2("history_rewritten", `the head kept earlier (${o.previous.size}:${o.previous.hash.slice(0, 12)}\u2026) is not part of this log`);
5665
+ }
5666
+ return { ok: true, size: o.entries.length, head: { size: h.size, hash: h.hash, at: h.at, kid: h.kid } };
5667
+ }
5668
+ function foldRegistrarLog(entries, o) {
5669
+ const open = /* @__PURE__ */ new Map();
5670
+ const closedClaims = [];
5671
+ const closures = [];
5672
+ const recognitions = [];
5673
+ const supersessions = [];
5674
+ const revoked = /* @__PURE__ */ new Set();
5675
+ let head = null;
5676
+ for (const e of entries) {
5677
+ head = e.hash;
5678
+ if (e.kind === "applies") {
5679
+ const prior = open.get(e.name);
5680
+ if (prior)
5681
+ closedClaims.push({ ...prior, until: e.at });
5682
+ open.set(e.name, {
5683
+ authority: e.name,
5684
+ version: e.version ?? null,
5685
+ scope: typeof e.entry?.scope === "string" ? e.entry.scope : "always",
5686
+ exclusive: e.entry?.exclusive === true,
5687
+ from: e.at,
5688
+ until: typeof e.entry?.until === "string" ? e.entry.until : null,
5689
+ ...typeof e.entry?.basis === "string" ? { basis: e.entry.basis } : {}
5690
+ });
5691
+ } else if (e.kind === "withdrawn") {
5692
+ const prior = open.get(e.name);
5693
+ if (prior) {
5694
+ closedClaims.push({ ...prior, until: e.at });
5695
+ open.delete(e.name);
5696
+ }
5697
+ } else if (e.kind === "revoked") {
5698
+ revoked.add(e.name);
5699
+ } else if (e.kind === "closed") {
5700
+ const scope = typeof e.entry?.scope === "string" ? e.entry.scope : "always";
5701
+ const authorities = Array.isArray(e.entry?.authorities) ? e.entry.authorities.filter((a) => typeof a === "string") : [];
5702
+ 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 } : {} });
5703
+ } else if (e.kind === "recognizes") {
5704
+ recognitions.push({ to: e.name, scope: typeof e.entry?.scope === "string" ? e.entry.scope : "always" });
5705
+ } else if (e.kind === "superseded") {
5706
+ if (typeof e.entry?.by === "string")
5707
+ supersessions.push({ superseded: e.name, by: e.entry.by, from: e.at });
5708
+ }
5709
+ }
5710
+ return {
5711
+ id: o.id,
5712
+ certified_scope: o.certifiedScope ?? "always",
5713
+ claims: [...closedClaims, ...open.values()],
5714
+ closures,
5715
+ recognitions,
5716
+ supersessions,
5717
+ revoked: [...revoked].sort(),
5718
+ head
5719
+ };
5720
+ }
5721
+
5389
5722
  // src/verify-chain.ts
5390
5723
  var CHAIN_PROTOCOL_VERSION2 = "1";
5391
5724
  var DEFAULT_BASE = "https://www.nomosprotocol.com";
@@ -5562,6 +5895,68 @@ ${tool}`);
5562
5895
  return lines.join("\n");
5563
5896
  }
5564
5897
 
5898
+ // src/act/registrars.ts
5899
+ var RegistrarUnavailable = class extends Error {
5900
+ constructor(message, retryAfterSeconds, registrar) {
5901
+ super(message);
5902
+ this.retryAfterSeconds = retryAfterSeconds;
5903
+ this.registrar = registrar;
5904
+ this.name = "RegistrarUnavailable";
5905
+ }
5906
+ };
5907
+ var PAGE = 1e3;
5908
+ function registrarSource(o) {
5909
+ const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
5910
+ const base = o.url.replace(/\/$/, "");
5911
+ const maxHeadAge = o.maxHeadAgeSeconds ?? 600, refresh = (o.refreshSeconds ?? 60) * 1e3;
5912
+ let view = null, kept = o.initialHead ?? null;
5913
+ let fetchedAt = 0, inFlight = null, lastError = "";
5914
+ async function getJson2(path) {
5915
+ const res = await f(`${base}${path}`, { headers: { accept: "application/json" } });
5916
+ if (!res.ok) throw new Error(`${base}${path} answered ${res.status}`);
5917
+ return res.json();
5918
+ }
5919
+ async function load() {
5920
+ fetchedAt = now().getTime();
5921
+ try {
5922
+ const head = await getJson2("/.well-known/nomos-exchange-log/head");
5923
+ const entries = [];
5924
+ for (let after = 0; ; ) {
5925
+ const page2 = await getJson2(`/.well-known/nomos-exchange-log?after=${after}&limit=${PAGE}`);
5926
+ const got = Array.isArray(page2?.entries) ? page2.entries : [];
5927
+ entries.push(...got);
5928
+ if (got.length < PAGE || !page2.next) break;
5929
+ after = got[got.length - 1].seq;
5930
+ }
5931
+ const vouched = entries.slice(0, Math.min(entries.length, Number(head?.size) || 0));
5932
+ const check = verifyRegistrarLog({ entries: vouched, head, publicKeyPem: o.publicKeyPem, now: now(), maxHeadAgeSeconds: maxHeadAge, previous: kept });
5933
+ if (!check.ok) {
5934
+ lastError = `${check.reason}: ${check.detail}`;
5935
+ if (check.reason === "history_rewritten" || check.reason === "wrong_key" || check.reason === "bad_signature" || check.reason === "chain_broken") view = null;
5936
+ return;
5937
+ }
5938
+ view = { record: foldRegistrarLog(vouched, { id: o.id, certifiedScope: o.certifiedScope }), head: check.head };
5939
+ kept = { size: check.head.size, hash: check.head.hash };
5940
+ lastError = "";
5941
+ } catch (e) {
5942
+ lastError = String(e?.message ?? e);
5943
+ }
5944
+ }
5945
+ const source = (async () => {
5946
+ const t = now().getTime();
5947
+ if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
5948
+ inFlight = null;
5949
+ });
5950
+ if (inFlight) await inFlight;
5951
+ if (!view || t - Date.parse(view.head.at) > maxHeadAge * 1e3) {
5952
+ 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);
5953
+ }
5954
+ return view;
5955
+ });
5956
+ source.id = o.id;
5957
+ return source;
5958
+ }
5959
+
5565
5960
  // src/act/bytes.ts
5566
5961
  var utf82 = (s) => new TextEncoder().encode(s);
5567
5962
  var fromUtf82 = (b) => new TextDecoder().decode(b);
@@ -5842,7 +6237,7 @@ var STATUS = { AUTHORIZED: 201, ESCALATED: 202, INCOMPLETE: 422, DENIED: 403, RE
5842
6237
  var statusFor = (decision) => STATUS[decision] ?? 400;
5843
6238
  var ACT_MEDIA_TYPE = "application/nomos-act+json";
5844
6239
  var HINTS = {
5845
- 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.`,
6240
+ 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.`,
5846
6241
  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}.`,
5847
6242
  DENIED: (v) => `Rule ${v.rule ?? "(default)"} refuses this action. No approval can change a denial.`,
5848
6243
  WRONG_RELYING_PARTY: () => "The Act is addressed to another system. Build it with this relying_party_id.",
@@ -5955,6 +6350,24 @@ function accept(o) {
5955
6350
  maxWindowSeconds
5956
6351
  });
5957
6352
  }
6353
+ async function resolveFor(act2, authority, facts, at, reuse) {
6354
+ if (!o.resolution) return null;
6355
+ const views = reuse ?? await Promise.all(o.resolution.registrars.map(async (src) => [src.id, await src()]));
6356
+ const context = { ...await o.resolution.context(act2, facts) };
6357
+ const r = resolveApplicableSet(
6358
+ { pins: views.map(([id]) => id), registrars: Object.fromEntries(views.map(([id, v]) => [id, v.record])) },
6359
+ { context, at: at.toISOString() }
6360
+ );
6361
+ const heads = Object.fromEntries(views.map(([id, v]) => [id, v.head.hash]));
6362
+ const applicable = r.status === "RESOLVED" ? [...new Set(r.applicable.map((m) => m.authority))].sort() : [];
6363
+ const info = { status: r.status, context, applicable, heads };
6364
+ if (r.status === "INCOMPLETE") return { ok: false, info, views, verdict: { decision: "INCOMPLETE", reason_code: "resolution_incomplete", missing: r.missing } };
6365
+ if (r.status === "CONFLICT") return { ok: false, info, views, verdict: { decision: "INCOMPLETE", reason_code: "resolution_conflict", conflicts: r.conflicts } };
6366
+ const carried = new Set([authority.registrarName, authority.artifactId].filter((x) => !!x));
6367
+ const notCarried = applicable.filter((a) => !carried.has(a));
6368
+ if (notCarried.length) return { ok: false, info: { ...info, laws_not_presented: notCarried }, views, verdict: { decision: "INCOMPLETE", reason_code: "laws_not_presented", laws: notCarried } };
6369
+ return { ok: true, info, views };
6370
+ }
5958
6371
  async function handle(req) {
5959
6372
  const ctype = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
5960
6373
  if ((req.method ?? "POST").toUpperCase() !== "POST") return { status: 405, headers: { allow: "POST", "content-type": "application/json" }, body: { error: "POST an Act here." } };
@@ -6001,8 +6414,31 @@ function accept(o) {
6001
6414
  if (verdict.decision === "REPLAYED") {
6002
6415
  return answer(verdict, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
6003
6416
  }
6417
+ let resolved = null;
6418
+ if (verdict.decision === "AUTHORIZED" && o.resolution) {
6419
+ try {
6420
+ resolved = await resolveFor(act2, authority, facts, verifiedAt);
6421
+ } catch (e) {
6422
+ if (!(e instanceof RegistrarUnavailable)) throw e;
6423
+ return {
6424
+ status: 503,
6425
+ headers: { "content-type": "application/json", "retry-after": String(e.retryAfterSeconds) },
6426
+ 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 }
6427
+ };
6428
+ }
6429
+ if (resolved && !resolved.ok) {
6430
+ const held = { ...resolved.verdict, act_id: act2.act_id, act_digest: verdict.act_digest, resolution: resolved.info };
6431
+ await o.onVerdict?.(held, { ...receiptOf(held, facts), resolution: resolved.info });
6432
+ if (o.mode === "shadow") {
6433
+ const a = answer(held);
6434
+ return { ...a, status: 200, body: { ...a.body, shadow: true } };
6435
+ }
6436
+ return answer(held);
6437
+ }
6438
+ }
6439
+ const withResolution = (r) => resolved?.ok ? { ...r, resolution: resolved.info } : r;
6004
6440
  if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
6005
- await o.onVerdict?.(verdict, receiptOf(verdict, facts));
6441
+ await o.onVerdict?.(verdict, withResolution(receiptOf(verdict, facts)));
6006
6442
  if (verdict.decision === "ESCALATED") {
6007
6443
  const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
6008
6444
  return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
@@ -6020,17 +6456,23 @@ function accept(o) {
6020
6456
  finalFacts = fresh;
6021
6457
  final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
6022
6458
  if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
6459
+ if (o.resolution) {
6460
+ const again = await resolveFor(act2, authority, fresh, verifiedAt, resolved?.views);
6461
+ if (again && !again.ok) throw new Rollback(answer({ ...again.verdict, act_id: act2.act_id, resolution: again.info }));
6462
+ resolved = again;
6463
+ }
6023
6464
  }
6024
6465
  }
6025
6466
  const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
6026
6467
  const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
6027
- const receipt = receiptOf(final, finalFacts, now());
6468
+ const receipt = withResolution(receiptOf(final, finalFacts, now()));
6028
6469
  await t.putReceipt(act2.nonce, receipt, act2);
6029
6470
  await o.onVerdict?.(final, receipt);
6030
6471
  return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
6031
6472
  });
6032
6473
  } catch (e) {
6033
6474
  if (e instanceof Rollback) return e.answer;
6475
+ 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 } };
6034
6476
  if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
6035
6477
  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) } };
6036
6478
  }
@@ -6795,6 +7237,7 @@ var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js)
6795
7237
  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'`;
6796
7238
 
6797
7239
  // src/act/carrier.ts
7240
+ var RESOLUTION_HOLDS = /* @__PURE__ */ new Set(["laws_not_presented", "resolution_incomplete", "resolution_conflict"]);
6798
7241
  var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
6799
7242
  var NomosActError = class extends Error {
6800
7243
  constructor(message) {
@@ -6814,9 +7257,9 @@ async function act(o) {
6814
7257
  const entry = issuer2.authorities.find((a) => a.artifact_id === accepted.artifact_id);
6815
7258
  if (!entry) throw new NomosActError(`The issuer at ${accepted.issuer} does not publish ${accepted.artifact_id}.`);
6816
7259
  const artifact = await getJson(f, entry.artifact_url, "the authority");
6817
- let inForce;
7260
+ let inForce2;
6818
7261
  if (accepted.currency_policy === "record_or_statement" && !accepted.in_force_seal_hash && entry.in_force_url) {
6819
- inForce = await getJson(f, entry.in_force_url, "the in-force statement").catch(() => void 0);
7262
+ inForce2 = await getJson(f, entry.in_force_url, "the in-force statement").catch(() => void 0);
6820
7263
  }
6821
7264
  const inputs = artifact?.data_contract?.inputs ?? {};
6822
7265
  const witnessed = Object.entries(inputs).filter(([, i]) => i?.source === "testimony").map(([n]) => n);
@@ -6856,7 +7299,7 @@ async function act(o) {
6856
7299
  testimony.push(t);
6857
7300
  if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
6858
7301
  }
6859
- const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
7302
+ const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce: inForce2, windowSeconds });
6860
7303
  present(theAct, available);
6861
7304
  let { status, body } = await submit(f, rp.act_endpoint, theAct);
6862
7305
  let approvalUrl;
@@ -6884,6 +7327,10 @@ async function act(o) {
6884
7327
  }
6885
7328
  }
6886
7329
  const kind = STATUS_OF[body.decision] ?? "refused";
7330
+ if (kind === "incomplete" && RESOLUTION_HOLDS.has(body.reason_code)) {
7331
+ const laws = Array.isArray(body.laws) ? body.laws : [];
7332
+ 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}).`}` });
7333
+ }
6887
7334
  if (kind === "incomplete") {
6888
7335
  const missing = (body.open_facts ?? []).map((fact) => {
6889
7336
  const why = unavailable.find((u) => u.fact === fact || fact.startsWith(u.fact + "."));
@@ -6915,6 +7362,7 @@ async function getJson(f, url, what) {
6915
7362
  async function submit(f, endpoint, theAct) {
6916
7363
  const res = await f(endpoint, { method: "POST", headers: { "content-type": ACT_MEDIA_TYPE }, body: JSON.stringify(theAct) });
6917
7364
  const body = await res.json().catch(() => ({ decision: "MALFORMED", reason_code: "unreadable_response" }));
7365
+ 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.` : ""}`);
6918
7366
  if (res.status >= 500) throw new NomosActError(`The relying party failed to commit (${res.status}): ${body?.error ?? ""}`);
6919
7367
  return { status: res.status, body };
6920
7368
  }
@@ -7117,6 +7565,7 @@ export {
7117
7565
  NomosRateLimitError,
7118
7566
  NomosValidationError,
7119
7567
  NomosWebhookSignatureError,
7568
+ RegistrarUnavailable,
7120
7569
  RegistryError,
7121
7570
  RevocationUnavailable,
7122
7571
  WELL_KNOWN,
@@ -7147,6 +7596,7 @@ export {
7147
7596
  postgresStore,
7148
7597
  presentedCertificates,
7149
7598
  readAttestation,
7599
+ registrarSource,
7150
7600
  reliedKids,
7151
7601
  revocationListCertificates,
7152
7602
  revocationSource,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomosprotocol/sdk",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "NOMOS for TypeScript: accept() for the system that makes a change, act() for the agent that asks, the issuer, witness and approver roles, and the Exchange client. Verification is local (@nomosprotocol/verify).",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -83,6 +83,6 @@
83
83
  "dependencies": {
84
84
  "@noble/curves": "^2.4.0",
85
85
  "@noble/hashes": "^2.4.0",
86
- "@nomosprotocol/verify": "^1.3.0"
86
+ "@nomosprotocol/verify": "^1.4.0"
87
87
  }
88
88
  }