@nomosprotocol/sdk 2.0.0 → 2.3.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
@@ -21,6 +21,7 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ACT_MEDIA_TYPE: () => ACT_MEDIA_TYPE,
24
+ MAX_PRESENTED: () => MAX_PRESENTED,
24
25
  Nomos: () => Nomos,
25
26
  NomosAPIError: () => NomosAPIError,
26
27
  NomosActError: () => NomosActError,
@@ -35,6 +36,8 @@ __export(index_exports, {
35
36
  NomosRateLimitError: () => NomosRateLimitError,
36
37
  NomosValidationError: () => NomosValidationError,
37
38
  NomosWebhookSignatureError: () => NomosWebhookSignatureError,
39
+ RegistryError: () => RegistryError,
40
+ RevocationUnavailable: () => RevocationUnavailable,
38
41
  WELL_KNOWN: () => WELL_KNOWN,
39
42
  accept: () => accept,
40
43
  act: () => act,
@@ -42,24 +45,40 @@ __export(index_exports, {
42
45
  bindingDigest: () => bindingDigest,
43
46
  buildAct: () => buildAct,
44
47
  can: () => can,
48
+ certificateRegistry: () => certificateRegistry,
45
49
  certify: () => certify,
46
50
  fetchDocument: () => fetchDocument,
47
51
  generateSigner: () => generateSigner,
48
52
  issuer: () => issuer,
49
53
  kidOf: () => kidOf2,
50
54
  localSigner: () => localSigner,
55
+ memoryPasskeyStore: () => memoryPasskeyStore,
56
+ memoryRegistryStore: () => memoryRegistryStore,
51
57
  memoryStore: () => memoryStore,
52
58
  nomos: () => nomos,
53
59
  originOf: () => originOf,
60
+ passkeyApprover: () => passkeyApprover,
61
+ postgresPasskeyStore: () => postgresPasskeyStore,
62
+ postgresRegistryStore: () => postgresRegistryStore,
63
+ postgresStore: () => postgresStore,
64
+ presentedCertificates: () => presentedCertificates,
65
+ readAttestation: () => readAttestation,
66
+ reliedKids: () => reliedKids,
67
+ revocationListCertificates: () => revocationListCertificates,
68
+ revocationSource: () => revocationSource,
54
69
  seal: () => seal,
55
70
  seededSigner: () => seededSigner,
56
71
  signConsent: () => signConsent,
72
+ signEnrolmentToken: () => signEnrolmentToken,
57
73
  signInForce: () => signInForce,
74
+ signRevocation: () => signRevocation,
75
+ signRevocationList: () => signRevocationList,
58
76
  signTestimony: () => signTestimony,
59
77
  statusFor: () => statusFor,
60
78
  verifyAct: () => verifyAct,
61
79
  verifyArtifact: () => verifyArtifact,
62
80
  verifyChain: () => verifyChain,
81
+ verifyEnrolmentToken: () => verifyEnrolmentToken,
63
82
  verifySeal: () => verifySeal,
64
83
  witness: () => witness
65
84
  });
@@ -873,7 +892,7 @@ var Nomos = class {
873
892
  }
874
893
  };
875
894
 
876
- // ../../AI_Navigator-repro/node_modules/@noble/hashes/_u64.js
895
+ // node_modules/@noble/hashes/_u64.js
877
896
  var U32_MASK64 = /* @__PURE__ */ (() => BigInt(2 ** 32 - 1))();
878
897
  var _32n = /* @__PURE__ */ BigInt(32);
879
898
  function fromBig(n, le = false) {
@@ -916,7 +935,7 @@ var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0
916
935
  var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
917
936
  var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
918
937
 
919
- // ../../AI_Navigator-repro/node_modules/@noble/hashes/utils.js
938
+ // node_modules/@noble/hashes/utils.js
920
939
  function isBytes(a) {
921
940
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
922
941
  }
@@ -1081,7 +1100,7 @@ var oidNist = (suffix) => ({
1081
1100
  oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
1082
1101
  });
1083
1102
 
1084
- // ../../AI_Navigator-repro/node_modules/@noble/hashes/_md.js
1103
+ // node_modules/@noble/hashes/_md.js
1085
1104
  function Chi(a, b, c) {
1086
1105
  return a & b ^ ~a & c;
1087
1106
  }
@@ -1212,7 +1231,7 @@ var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
1212
1231
  327033209
1213
1232
  ]);
1214
1233
 
1215
- // ../../AI_Navigator-repro/node_modules/@noble/hashes/sha2.js
1234
+ // node_modules/@noble/hashes/sha2.js
1216
1235
  var SHA256_K = /* @__PURE__ */ Uint32Array.from([
1217
1236
  1116352408,
1218
1237
  1899447441,
@@ -1609,7 +1628,7 @@ var sha512 = /* @__PURE__ */ createHasher(
1609
1628
  /* @__PURE__ */ oidNist(3)
1610
1629
  );
1611
1630
 
1612
- // ../packages/verify/dist/bytes.js
1631
+ // node_modules/@nomosprotocol/verify/dist/bytes.js
1613
1632
  var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1614
1633
  var REV = new Map([...B64].map((c, i) => [c, i]));
1615
1634
  var utf8 = (s) => new TextEncoder().encode(s);
@@ -1671,7 +1690,7 @@ function fromBase64url(s) {
1671
1690
  return raw && toBase64url(raw) === s ? raw : null;
1672
1691
  }
1673
1692
 
1674
- // ../packages/verify/dist/jcs.js
1693
+ // node_modules/@nomosprotocol/verify/dist/jcs.js
1675
1694
  function value(v) {
1676
1695
  if (v === null)
1677
1696
  return "null";
@@ -1705,7 +1724,7 @@ function codePointCompare(a, b) {
1705
1724
  return A.length - B.length;
1706
1725
  }
1707
1726
 
1708
- // ../../AI_Navigator-repro/node_modules/@noble/curves/utils.js
1727
+ // node_modules/@noble/curves/utils.js
1709
1728
  function aarray(item, title, inner = () => {
1710
1729
  }) {
1711
1730
  if (!Array.isArray(item))
@@ -1887,7 +1906,7 @@ function validateObject(object, fields = {}, optFields = {}, title = "object") {
1887
1906
  iter(optFields, true);
1888
1907
  }
1889
1908
 
1890
- // ../../AI_Navigator-repro/node_modules/@noble/curves/abstract/modular.js
1909
+ // node_modules/@noble/curves/abstract/modular.js
1891
1910
  var _0n2 = /* @__PURE__ */ BigInt(0);
1892
1911
  var _1n2 = /* @__PURE__ */ BigInt(1);
1893
1912
  var _2n = /* @__PURE__ */ BigInt(2);
@@ -2350,7 +2369,7 @@ function mapHashToField(key, fieldOrder, isLE = false) {
2350
2369
  return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
2351
2370
  }
2352
2371
 
2353
- // ../../AI_Navigator-repro/node_modules/@noble/curves/abstract/curve.js
2372
+ // node_modules/@noble/curves/abstract/curve.js
2354
2373
  var _0n3 = /* @__PURE__ */ BigInt(0);
2355
2374
  var _1n3 = /* @__PURE__ */ BigInt(1);
2356
2375
  var _4n2 = /* @__PURE__ */ BigInt(4);
@@ -2737,7 +2756,7 @@ function createKeygen(randomSecretKey, getPublicKey) {
2737
2756
  };
2738
2757
  }
2739
2758
 
2740
- // ../../AI_Navigator-repro/node_modules/@noble/curves/abstract/edwards.js
2759
+ // node_modules/@noble/curves/abstract/edwards.js
2741
2760
  var _0n4 = /* @__PURE__ */ BigInt(0);
2742
2761
  var _1n4 = /* @__PURE__ */ BigInt(1);
2743
2762
  var _2n2 = /* @__PURE__ */ BigInt(2);
@@ -3188,7 +3207,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
3188
3207
  });
3189
3208
  }
3190
3209
 
3191
- // ../../AI_Navigator-repro/node_modules/@noble/curves/ed25519.js
3210
+ // node_modules/@noble/curves/ed25519.js
3192
3211
  var _1n5 = /* @__PURE__ */ BigInt(1);
3193
3212
  var _2n3 = /* @__PURE__ */ BigInt(2);
3194
3213
  var _5n2 = /* @__PURE__ */ BigInt(5);
@@ -3263,7 +3282,7 @@ function ed(opts) {
3263
3282
  }
3264
3283
  var ed25519 = /* @__PURE__ */ ed({});
3265
3284
 
3266
- // ../../AI_Navigator-repro/node_modules/@noble/hashes/hmac.js
3285
+ // node_modules/@noble/hashes/hmac.js
3267
3286
  var _HMAC = class {
3268
3287
  oHash;
3269
3288
  iHash;
@@ -3340,7 +3359,7 @@ var hmac = /* @__PURE__ */ (() => {
3340
3359
  return hmac_;
3341
3360
  })();
3342
3361
 
3343
- // ../../AI_Navigator-repro/node_modules/@noble/curves/abstract/der.js
3362
+ // node_modules/@noble/curves/abstract/der.js
3344
3363
  var _0n5 = /* @__PURE__ */ BigInt(0);
3345
3364
  var DERErr = class extends Error {
3346
3365
  constructor(m = "") {
@@ -3434,18 +3453,18 @@ var _DER = {
3434
3453
  }
3435
3454
  },
3436
3455
  toSig(bytes, maxScalarBytes) {
3437
- const { Err: E, _int: int, _tlv: tlv } = _DER;
3456
+ const { Err: E, _int: int, _tlv: tlv2 } = _DER;
3438
3457
  if (maxScalarBytes !== void 0) {
3439
3458
  asafenumber(maxScalarBytes, "maxScalarBytes");
3440
3459
  if (maxScalarBytes < 1)
3441
3460
  throw new E("invalid signature: maxScalarBytes must be positive");
3442
3461
  }
3443
3462
  const data = abytes2(bytes, void 0, "signature");
3444
- const { v: seqBytes, l: seqLeftBytes } = tlv.decode(48, data);
3463
+ const { v: seqBytes, l: seqLeftBytes } = tlv2.decode(48, data);
3445
3464
  if (seqLeftBytes.length)
3446
3465
  throw new E("invalid signature: left bytes after parsing");
3447
- const { v: rBytes, l: rLeftBytes } = tlv.decode(2, seqBytes);
3448
- const { v: sBytes, l: sLeftBytes } = tlv.decode(2, rLeftBytes);
3466
+ const { v: rBytes, l: rLeftBytes } = tlv2.decode(2, seqBytes);
3467
+ const { v: sBytes, l: sLeftBytes } = tlv2.decode(2, rLeftBytes);
3449
3468
  if (sLeftBytes.length)
3450
3469
  throw new E("invalid signature: left bytes after parsing");
3451
3470
  if (maxScalarBytes !== void 0 && (rBytes.length > maxScalarBytes || sBytes.length > maxScalarBytes))
@@ -3453,12 +3472,12 @@ var _DER = {
3453
3472
  return { r: int.decode(rBytes), s: int.decode(sBytes) };
3454
3473
  },
3455
3474
  hexFromSig(sig) {
3456
- const { _tlv: tlv, _int: int } = _DER;
3475
+ const { _tlv: tlv2, _int: int } = _DER;
3457
3476
  validateObject(sig, { r: "bigint", s: "bigint" }, {}, "sig");
3458
- const rs = tlv.encode(2, int.encode(sig.r));
3459
- const ss = tlv.encode(2, int.encode(sig.s));
3477
+ const rs = tlv2.encode(2, int.encode(sig.r));
3478
+ const ss = tlv2.encode(2, int.encode(sig.s));
3460
3479
  const seq = rs + ss;
3461
- return tlv.encode(48, seq);
3480
+ return tlv2.encode(48, seq);
3462
3481
  }
3463
3482
  };
3464
3483
  var DER = /* @__PURE__ */ (() => {
@@ -3467,7 +3486,7 @@ var DER = /* @__PURE__ */ (() => {
3467
3486
  return Object.freeze(_DER);
3468
3487
  })();
3469
3488
 
3470
- // ../../AI_Navigator-repro/node_modules/@noble/curves/abstract/weierstrass.js
3489
+ // node_modules/@noble/curves/abstract/weierstrass.js
3471
3490
  var divNearest = (num, den) => (num + (num >= 0 ? den : -den) / _2n4) / den;
3472
3491
  function _splitEndoScalar(k, basis, n) {
3473
3492
  aInRange("scalar", k, _0n6, n);
@@ -4299,7 +4318,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
4299
4318
  });
4300
4319
  }
4301
4320
 
4302
- // ../../AI_Navigator-repro/node_modules/@noble/curves/nist.js
4321
+ // node_modules/@noble/curves/nist.js
4303
4322
  var p256_CURVE = /* @__PURE__ */ (() => ({
4304
4323
  p: BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),
4305
4324
  n: BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),
@@ -4312,21 +4331,21 @@ var p256_CURVE = /* @__PURE__ */ (() => ({
4312
4331
  var p256_Point = /* @__PURE__ */ weierstrass(p256_CURVE);
4313
4332
  var p256 = /* @__PURE__ */ ecdsa(p256_Point, sha256);
4314
4333
 
4315
- // ../packages/verify/dist/suites.js
4334
+ // node_modules/@nomosprotocol/verify/dist/suites.js
4316
4335
  var SUITES = ["Ed25519", "ES256", "WebAuthn-ES256"];
4317
4336
  var N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
4318
4337
  var ED25519_PREFIX = Uint8Array.from([48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]);
4319
4338
  var P256_PREFIX = Uint8Array.from([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4]);
4320
4339
  var SUITES_FOR = { "Ed25519": ["Ed25519"], "P-256": ["ES256", "WebAuthn-ES256"] };
4321
4340
  var isSuite = (s) => typeof s === "string" && SUITES.includes(s);
4322
- function pemDer(pem) {
4323
- if (typeof pem !== "string")
4341
+ function pemDer(pem2) {
4342
+ if (typeof pem2 !== "string")
4324
4343
  return null;
4325
- const m = pem.match(/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=\s]+?)\s*-----END PUBLIC KEY-----\s*$/);
4344
+ const m = pem2.match(/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=\s]+?)\s*-----END PUBLIC KEY-----\s*$/);
4326
4345
  return m ? fromBase64(m[1].replace(/\s+/g, "")) : null;
4327
4346
  }
4328
- function loadKey(pem) {
4329
- const der = pemDer(pem);
4347
+ function loadKey(pem2) {
4348
+ const der = pemDer(pem2);
4330
4349
  if (!der)
4331
4350
  return { reason: "invalid_key" };
4332
4351
  if (der.length === 44 && equal(der.subarray(0, 12), ED25519_PREFIX)) {
@@ -4350,16 +4369,16 @@ function loadKey(pem) {
4350
4369
  }
4351
4370
  return { reason: "invalid_key" };
4352
4371
  }
4353
- function kidOf(pem) {
4354
- const k = loadKey(pem);
4372
+ function kidOf(pem2) {
4373
+ const k = loadKey(pem2);
4355
4374
  return "reason" in k ? null : toBase64url(sha256(k.der)).slice(0, 16);
4356
4375
  }
4357
4376
  var toBigInt = (b) => b.reduce((n, x) => n << 8n | BigInt(x), 0n);
4358
- function verifySuite(suite, pem, payload, proof) {
4377
+ function verifySuite(suite, pem2, payload, proof) {
4359
4378
  const fail2 = (reason) => ({ ok: false, reason });
4360
4379
  if (!isSuite(suite))
4361
4380
  return fail2("unknown_suite");
4362
- const k = loadKey(pem);
4381
+ const k = loadKey(pem2);
4363
4382
  if ("reason" in k)
4364
4383
  return fail2("invalid_key");
4365
4384
  if (!SUITES_FOR[k.type].includes(suite))
@@ -4421,7 +4440,7 @@ function verifySuite(suite, pem, payload, proof) {
4421
4440
  return ok ? { ok: true } : fail2("bad_signature");
4422
4441
  }
4423
4442
 
4424
- // ../packages/verify/dist/scope.js
4443
+ // node_modules/@nomosprotocol/verify/dist/scope.js
4425
4444
  var ARTIFACT_DIMENSIONS = ["artifact", "industry", "jurisdiction"];
4426
4445
  var SET_DIMENSIONS = ["claim", "consent", "suite"];
4427
4446
  var UNRESTRICTED = { terms: {} };
@@ -4540,7 +4559,7 @@ function formatScope(scope) {
4540
4559
  return Object.entries(scope.terms).map(([d, v]) => `${d}:${v}`).join(" ");
4541
4560
  }
4542
4561
 
4543
- // ../packages/verify/dist/chain.js
4562
+ // node_modules/@nomosprotocol/verify/dist/chain.js
4544
4563
  var MAX_CHAIN = 20;
4545
4564
  var CERT_SUITES = ["Ed25519", "ES256"];
4546
4565
  var SEAL_SUITES = ["Ed25519", "ES256"];
@@ -4632,7 +4651,7 @@ var Memo = class {
4632
4651
  }
4633
4652
  };
4634
4653
  function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo) {
4635
- let pem = rootPem, scope = UNRESTRICTED;
4654
+ let pem2 = rootPem, scope = UNRESTRICTED;
4636
4655
  const hops = [];
4637
4656
  for (const cert of path) {
4638
4657
  const f = memo.of(cert);
@@ -4642,7 +4661,7 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
4642
4661
  return fail("ISSUER_NOT_RECOGNIZED", { reason_code: "kid_mismatch" });
4643
4662
  if (!suitePermitted(scope, cert.algorithm))
4644
4663
  return fail("OUT_OF_SCOPE", { dimension: "suite", reason_code: "suite_not_permitted" });
4645
- const sig = memo.sig(cert, pem);
4664
+ const sig = memo.sig(cert, pem2);
4646
4665
  if (!sig.ok)
4647
4666
  return fail("ISSUER_NOT_RECOGNIZED", { reason_code: sig.reason });
4648
4667
  const issued = parseTime(cert.issued_at), expires = parseTime(cert.expires_at);
@@ -4662,14 +4681,14 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
4662
4681
  const narrowing = isNarrowerOrEqual(f.scope.scope, scope);
4663
4682
  if (!narrowing.ok)
4664
4683
  return fail("OUT_OF_SCOPE", { dimension: narrowing.dimension, reason_code: "widening" });
4665
- hops.push({ cert, parentPem: pem, parentScope: scope });
4666
- pem = cert.child_public_key_pem;
4684
+ hops.push({ cert, parentPem: pem2, parentScope: scope });
4685
+ pem2 = cert.child_public_key_pem;
4667
4686
  scope = mergeScopes(scope, f.scope.scope);
4668
4687
  }
4669
4688
  const admitted = purpose(scope);
4670
4689
  if (!admitted.ok)
4671
4690
  return fail("OUT_OF_SCOPE", { dimension: admitted.dimension });
4672
- return { decision: "RESOLVED", pem, path: [], effective: scope, hops };
4691
+ return { decision: "RESOLVED", pem: pem2, path: [], effective: scope, hops };
4673
4692
  }
4674
4693
  function resolveKey(o) {
4675
4694
  const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
@@ -4755,7 +4774,7 @@ function verifyArtifact(o) {
4755
4774
  }
4756
4775
  }
4757
4776
 
4758
- // ../packages/verify/dist/evaluate.js
4777
+ // node_modules/@nomosprotocol/verify/dist/evaluate.js
4759
4778
  var jcsString = (v) => canonicalize(v);
4760
4779
  var FIELD = /* @__PURE__ */ Symbol("field");
4761
4780
  var ABSENT = /* @__PURE__ */ Symbol("absent");
@@ -5084,7 +5103,7 @@ function evaluate(artifact, facts) {
5084
5103
  return ev;
5085
5104
  }
5086
5105
 
5087
- // ../packages/verify/dist/act.js
5106
+ // node_modules/@nomosprotocol/verify/dist/act.js
5088
5107
  var MAX_WINDOW_SECONDS = 300;
5089
5108
  var MAX_IN_FORCE_SECONDS = 86400;
5090
5109
  var TESTIMONY_SUITES = ["Ed25519", "ES256"];
@@ -5122,7 +5141,7 @@ function hasAmbiguousEscalation(artifact) {
5122
5141
  return [...named.values()].some((ids) => ids.size > 1);
5123
5142
  }
5124
5143
  var proofOf = (st) => st.algorithm === "WebAuthn-ES256" ? pick(st, WEBAUTHN_FIELDS) : { signature: st.signature };
5125
- var statementSig = (pem, st, payload) => verifySuite(st.algorithm, pem, jcs(payload), proofOf(st));
5144
+ var statementSig = (pem2, st, payload) => verifySuite(st.algorithm, pem2, jcs(payload), proofOf(st));
5126
5145
  var time = (s) => typeof s === "string" ? Date.parse(s) : NaN;
5127
5146
  var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
5128
5147
  function statementFailure(r, invalid, outOfScope, who) {
@@ -5287,11 +5306,11 @@ function verifyUnsafe(a, o) {
5287
5306
  return { decision: "AUTHORIZED", commit: true, ...base };
5288
5307
  if (ev.outcome === "block")
5289
5308
  return { decision: "DENIED", commit: false, ...base };
5290
- const esc2 = (art.governance?.escalations ?? []).find((e) => e?.id === ev.escalation_id);
5291
- if (!esc2)
5309
+ const esc3 = (art.governance?.escalations ?? []).find((e) => e?.id === ev.escalation_id);
5310
+ if (!esc3)
5292
5311
  return { decision: "ESCALATED", commit: false, escalation_id: ev.escalation_id, consents_valid: 0, consents_required: null, ...base };
5293
- const role = esc2.role_required;
5294
- const needN = Math.trunc(Number(esc2.required_consents ?? 1));
5312
+ const role = esc3.role_required;
5313
+ const needN = Math.trunc(Number(esc3.required_consents ?? 1));
5295
5314
  const approvers = /* @__PURE__ */ new Set();
5296
5315
  for (const c of Array.isArray(act2.consents) ? act2.consents : []) {
5297
5316
  if (!isObject(c) || !["binding_digest", "role", "decision", "signed_at", "kid", "signature"].every((k2) => typeof c[k2] === "string") || !CONSENT_SUITES.includes(c.algorithm) || c.algorithm === "WebAuthn-ES256" && !WEBAUTHN_FIELDS.every((k2) => typeof c[k2] === "string")) {
@@ -5317,7 +5336,7 @@ function verifyUnsafe(a, o) {
5317
5336
  return { decision: "ESCALATED", commit: false, ...counted };
5318
5337
  }
5319
5338
 
5320
- // ../packages/verify/dist/seal.js
5339
+ // node_modules/@nomosprotocol/verify/dist/seal.js
5321
5340
  function verifySeal(artifact, publicKeyPem) {
5322
5341
  const bad = checkSealShape(artifact);
5323
5342
  if (bad)
@@ -5331,6 +5350,83 @@ function verifySeal(artifact, publicKeyPem) {
5331
5350
  return sealFailure(artifact, publicKeyPem) ?? { decision: "VALID", kid };
5332
5351
  }
5333
5352
 
5353
+ // node_modules/@nomosprotocol/verify/dist/revocation.js
5354
+ var MAX_LIST_CERTS = 1e3;
5355
+ var REVOCATION_SUITES = ["Ed25519", "ES256"];
5356
+ var CERT_FIELDS2 = ["parent_kid", "child_kid", "child_public_key_pem", "issued_at", "expires_at", "algorithm", "signature"];
5357
+ var FP = /^[0-9a-f]{64}$/;
5358
+ var revocationPayload = (st) => "revoked_fingerprint" in st ? { revoked_fingerprint: st.revoked_fingerprint, reason: st.reason, issued_at: st.issued_at } : { revoked_kid: st.revoked_kid, reason: st.reason, issued_at: st.issued_at };
5359
+ var revocationListPayload = (l) => ({ generated_at: l.generated_at, statements: l.statements });
5360
+ var forSigning = (suite) => (scope) => suitePermitted(scope, suite) ? { ok: true } : { ok: false, dimension: "suite" };
5361
+ var str = (v) => typeof v === "string" && v !== "";
5362
+ var time2 = (s) => typeof s === "string" ? Date.parse(s) : NaN;
5363
+ function signerPem(kid, pool, root, at, suite, revokedKids, revokedCerts) {
5364
+ const r = resolveKey({ targetKid: kid, pool, rootPublicKeyPem: root, purpose: forSigning(suite), now: new Date(at), revokedKids, revokedCerts });
5365
+ return r.decision === "RESOLVED" ? r.pem : null;
5366
+ }
5367
+ function evaluateStatement(st, pool, rootPublicKeyPem, now) {
5368
+ if (st === null || typeof st !== "object" || Array.isArray(st))
5369
+ return "bad_statement";
5370
+ const hasKid = "revoked_kid" in st, hasFp = "revoked_fingerprint" in st;
5371
+ if (hasKid === hasFp)
5372
+ return "bad_statement";
5373
+ const target = hasKid ? st.revoked_kid : st.revoked_fingerprint;
5374
+ if (!str(target) || hasFp && !FP.test(target))
5375
+ return "bad_statement";
5376
+ if (typeof st.reason !== "string" || !["issued_at", "kid", "algorithm", "signature"].every((f) => str(st[f])))
5377
+ return "bad_statement";
5378
+ if (!REVOCATION_SUITES.includes(st.algorithm))
5379
+ return "bad_statement";
5380
+ const issued = time2(st.issued_at);
5381
+ if (Number.isNaN(issued) || issued > now.getTime())
5382
+ return "bad_statement";
5383
+ const certs = pool.filter((c) => c.parent_kid === st.kid && (hasKid ? c.child_kid === target : fingerprint(c) === target));
5384
+ if (certs.length === 0)
5385
+ return "no_certificate";
5386
+ const parent = signerPem(st.kid, pool, rootPublicKeyPem, issued, st.algorithm, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
5387
+ if (!parent)
5388
+ return "signer_not_recognized";
5389
+ if (!certs.some((c) => verifySuite(c.algorithm, parent, jcs(certPayload(c)), { signature: c.signature }).ok))
5390
+ return "bad_certificate_signature";
5391
+ return verifySuite(st.algorithm, parent, jcs(revocationPayload(st)), { signature: st.signature }).ok ? null : "bad_signature";
5392
+ }
5393
+ function verifyRevocationList(list, o) {
5394
+ const L = list;
5395
+ if (L === null || typeof L !== "object" || !["generated_at", "kid", "algorithm", "signature"].every((f) => typeof L[f] === "string") || !Array.isArray(L.statements)) {
5396
+ return { decision: "MALFORMED", reason_code: "bad_list" };
5397
+ }
5398
+ const extra = L.key_certs ?? [];
5399
+ if (!Array.isArray(extra) || extra.length > MAX_LIST_CERTS || !extra.every((c) => c && typeof c === "object" && CERT_FIELDS2.every((f) => str(c[f])) && ["Ed25519", "ES256"].includes(c.algorithm) && (c.scope === void 0 || c.scope === null || typeof c.scope === "string"))) {
5400
+ return { decision: "MALFORMED", reason_code: "bad_list_certs" };
5401
+ }
5402
+ const seen = /* @__PURE__ */ new Set(), pool = [];
5403
+ for (const c of [...extra, ...o.ownCerts ?? []]) {
5404
+ const fp = fingerprint(c);
5405
+ if (!seen.has(fp)) {
5406
+ seen.add(fp);
5407
+ pool.push(c);
5408
+ }
5409
+ }
5410
+ const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set(), ignored = [];
5411
+ L.statements.forEach((st, index) => {
5412
+ const why = evaluateStatement(st, pool, o.rootPublicKeyPem, o.now);
5413
+ if (why)
5414
+ ignored.push({ index, reason: why });
5415
+ else if ("revoked_kid" in st)
5416
+ kids.add(st.revoked_kid);
5417
+ else
5418
+ certs.add(st.revoked_fingerprint);
5419
+ });
5420
+ const out = { decision: "OK", revoked_kids: [...kids].sort(), revoked_certs: [...certs].sort(), ignored };
5421
+ const generated = time2(L.generated_at);
5422
+ if (Number.isNaN(generated) || generated > o.now.getTime() || !REVOCATION_SUITES.includes(L.algorithm))
5423
+ return out;
5424
+ const signer = signerPem(L.kid, pool, o.rootPublicKeyPem, generated, L.algorithm, kids, certs);
5425
+ if (signer && verifySuite(L.algorithm, signer, jcs(revocationListPayload(L)), { signature: L.signature }).ok)
5426
+ out.as_of = L.generated_at;
5427
+ return out;
5428
+ }
5429
+
5334
5430
  // src/verify-chain.ts
5335
5431
  var CHAIN_PROTOCOL_VERSION2 = "1";
5336
5432
  var DEFAULT_BASE = "https://www.nomosprotocol.com";
@@ -5377,43 +5473,241 @@ async function verifyChainRemote(params, opts) {
5377
5473
  throw new NomosAPIError(message, res.status, requestId);
5378
5474
  }
5379
5475
 
5476
+ // src/act/bytes.ts
5477
+ var utf82 = (s) => new TextEncoder().encode(s);
5478
+ var fromUtf82 = (b) => new TextDecoder().decode(b);
5479
+ function concat2(...parts) {
5480
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
5481
+ let o = 0;
5482
+ for (const p of parts) {
5483
+ out.set(p, o);
5484
+ o += p.length;
5485
+ }
5486
+ return out;
5487
+ }
5488
+ function equal2(a, b) {
5489
+ if (a.length !== b.length) return false;
5490
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
5491
+ return true;
5492
+ }
5493
+ function fromHex(h) {
5494
+ const out = new Uint8Array(h.length / 2);
5495
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
5496
+ return out;
5497
+ }
5498
+ function toBase642(b) {
5499
+ let s = "";
5500
+ for (let i = 0; i < b.length; i += 32768) s += String.fromCharCode(...b.subarray(i, i + 32768));
5501
+ return btoa(s);
5502
+ }
5503
+ function fromBase642(s) {
5504
+ const bin = atob(s);
5505
+ const out = new Uint8Array(bin.length);
5506
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
5507
+ return out;
5508
+ }
5509
+ var toBase64url2 = (b) => toBase642(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5510
+ var fromBase64url2 = (s) => {
5511
+ const t = s.replace(/-/g, "+").replace(/_/g, "/");
5512
+ return fromBase642(t + "=".repeat((4 - t.length % 4) % 4));
5513
+ };
5514
+ function randomBytes3(n) {
5515
+ return globalThis.crypto.getRandomValues(new Uint8Array(n));
5516
+ }
5517
+ var randomUUID = () => globalThis.crypto.randomUUID();
5518
+
5380
5519
  // src/act/signer.ts
5381
- var import_node_crypto = require("crypto");
5382
- var P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551n;
5383
5520
  function kidOf2(publicKeyPem) {
5384
- const der = (0, import_node_crypto.createPublicKey)(publicKeyPem).export({ type: "spki", format: "der" });
5385
- return (0, import_node_crypto.createHash)("sha256").update(der).digest("base64url").slice(0, 16);
5521
+ const kid = kidOf(publicKeyPem);
5522
+ if (!kid) throw new Error("kidOf: not a canonical Ed25519 or P-256 public key (NOMOS-SPEC-010 \xA73.1)");
5523
+ return kid;
5524
+ }
5525
+ var ED25519_SPKI = fromHex("302a300506032b6570032100");
5526
+ var P256_SPKI = fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");
5527
+ var ED25519_PKCS8 = fromHex("302e020100300506032b657004220420");
5528
+ var P256_PKCS8_HEAD = fromHex("308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420");
5529
+ var P256_PKCS8_PUB = fromHex("a144034200");
5530
+ var OID_ED25519 = fromHex("2b6570");
5531
+ var OID_EC = fromHex("2a8648ce3d0201");
5532
+ var OID_P256 = fromHex("2a8648ce3d030107");
5533
+ function pem(label, der) {
5534
+ const b64 = toBase642(der);
5535
+ return `-----BEGIN ${label}-----
5536
+ ${b64.match(/.{1,64}/g).join("\n")}
5537
+ -----END ${label}-----
5538
+ `;
5539
+ }
5540
+ function pemBody(text) {
5541
+ const m = text.trim().match(/^-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----$/);
5542
+ if (!m) throw new Error("Signer: expected a PEM private key");
5543
+ return { label: m[1], der: fromBase642(m[2].replace(/\s+/g, "")) };
5544
+ }
5545
+ function tlv(b, at) {
5546
+ const tag = b[at];
5547
+ let len = b[at + 1], start = at + 2;
5548
+ if (len & 128) {
5549
+ const n = len & 127;
5550
+ if (n < 1 || n > 2) throw new Error("Signer: unsupported DER length");
5551
+ len = 0;
5552
+ for (let i = 0; i < n; i++) len = len << 8 | b[start + i];
5553
+ start += n;
5554
+ }
5555
+ if (start + len > b.length) throw new Error("Signer: truncated DER");
5556
+ return { tag, start, end: start + len };
5557
+ }
5558
+ function children(b, outer) {
5559
+ const out = [];
5560
+ for (let at = outer.start; at < outer.end; ) {
5561
+ const t = tlv(b, at);
5562
+ out.push(t);
5563
+ at = t.end;
5564
+ }
5565
+ return out;
5386
5566
  }
5387
- function fromKeyObject(privateKey) {
5388
- const publicKeyPem = (0, import_node_crypto.createPublicKey)(privateKey).export({ type: "spki", format: "pem" }).toString();
5389
- const isEd = privateKey.asymmetricKeyType === "ed25519";
5390
- if (!isEd && !(privateKey.asymmetricKeyType === "ec" && privateKey.asymmetricKeyDetails?.namedCurve === "prime256v1")) {
5391
- throw new Error("Signer: only Ed25519 and P-256 keys are NOMOS signature suites (NOMOS-SPEC-010)");
5567
+ function sec1Scalar(b, seq) {
5568
+ const [version, d, ...rest] = children(b, seq);
5569
+ if (version.tag !== 2 || d.tag !== 4 || d.end - d.start !== 32) throw new Error("Signer: malformed EC private key");
5570
+ for (const r of rest) {
5571
+ if (r.tag === 160) {
5572
+ const oid = tlv(b, r.start);
5573
+ if (!equal2(b.subarray(oid.start, oid.end), OID_P256)) throw new Error("Signer: only P-256 EC keys are a NOMOS suite (NOMOS-SPEC-010)");
5574
+ }
5392
5575
  }
5576
+ return b.slice(d.start, d.end);
5577
+ }
5578
+ function readPrivateKey(text) {
5579
+ const { label, der } = pemBody(text);
5580
+ const top = tlv(der, 0);
5581
+ if (label === "EC PRIVATE KEY") return { type: "ES256", key: sec1Scalar(der, top) };
5582
+ if (label !== "PRIVATE KEY") throw new Error(`Signer: unsupported key type "${label}" (PKCS#8 "PRIVATE KEY" or SEC1 "EC PRIVATE KEY")`);
5583
+ const [, alg, key] = children(der, top);
5584
+ const [oid, params] = children(der, alg);
5585
+ const algOid = der.subarray(oid.start, oid.end);
5586
+ if (equal2(algOid, OID_ED25519)) {
5587
+ const inner = tlv(der, key.start);
5588
+ if (inner.tag !== 4 || inner.end - inner.start !== 32) throw new Error("Signer: malformed Ed25519 private key");
5589
+ return { type: "Ed25519", key: der.slice(inner.start, inner.end) };
5590
+ }
5591
+ if (equal2(algOid, OID_EC)) {
5592
+ if (!params || !equal2(der.subarray(params.start, params.end), OID_P256)) throw new Error("Signer: only P-256 EC keys are a NOMOS suite (NOMOS-SPEC-010)");
5593
+ return { type: "ES256", key: sec1Scalar(der, tlv(der, key.start)) };
5594
+ }
5595
+ throw new Error("Signer: only Ed25519 and P-256 keys are NOMOS signature suites (NOMOS-SPEC-010)");
5596
+ }
5597
+ function publicPem(s) {
5598
+ return s.type === "Ed25519" ? pem("PUBLIC KEY", concat2(ED25519_SPKI, ed25519.getPublicKey(s.key))) : pem("PUBLIC KEY", concat2(P256_SPKI, p256.getPublicKey(s.key, false)));
5599
+ }
5600
+ function privatePem(s) {
5601
+ return s.type === "Ed25519" ? pem("PRIVATE KEY", concat2(ED25519_PKCS8, s.key)) : pem("PRIVATE KEY", concat2(P256_PKCS8_HEAD, s.key, P256_PKCS8_PUB, p256.getPublicKey(s.key, false)));
5602
+ }
5603
+ function fromSecret(s) {
5604
+ const publicKeyPem = publicPem(s);
5393
5605
  return {
5394
5606
  kid: kidOf2(publicKeyPem),
5395
5607
  publicKeyPem,
5396
- algorithm: isEd ? "Ed25519" : "ES256",
5608
+ algorithm: s.type,
5397
5609
  async sign(payload) {
5398
- if (isEd) return (0, import_node_crypto.sign)(null, Buffer.from(payload), privateKey).toString("base64");
5399
- const raw = (0, import_node_crypto.sign)("sha256", Buffer.from(payload), { key: privateKey, dsaEncoding: "ieee-p1363" });
5400
- let s = BigInt("0x" + raw.subarray(32).toString("hex"));
5401
- if (s > P256_N / 2n) s = P256_N - s;
5402
- return Buffer.concat([raw.subarray(0, 32), Buffer.from(s.toString(16).padStart(64, "0"), "hex")]).toString("base64");
5610
+ return toBase642(s.type === "Ed25519" ? ed25519.sign(payload, s.key) : p256.sign(payload, s.key, { prehash: true, lowS: true, format: "compact" }));
5403
5611
  }
5404
5612
  };
5405
5613
  }
5406
5614
  function localSigner(privateKeyPem) {
5407
- return fromKeyObject((0, import_node_crypto.createPrivateKey)(privateKeyPem));
5615
+ return fromSecret(readPrivateKey(privateKeyPem));
5408
5616
  }
5409
5617
  function generateSigner(algorithm = "Ed25519") {
5410
- const { privateKey } = algorithm === "Ed25519" ? (0, import_node_crypto.generateKeyPairSync)("ed25519") : (0, import_node_crypto.generateKeyPairSync)("ec", { namedCurve: "prime256v1" });
5411
- return { ...fromKeyObject(privateKey), privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString() };
5618
+ const s = algorithm === "Ed25519" ? { type: "Ed25519", key: ed25519.utils.randomSecretKey() } : { type: "ES256", key: p256.utils.randomSecretKey() };
5619
+ return { ...fromSecret(s), privateKeyPem: privatePem(s) };
5412
5620
  }
5413
5621
  function seededSigner(seed) {
5414
- const raw = (0, import_node_crypto.createHash)("sha256").update(seed).digest();
5415
- const der = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), raw]);
5416
- return fromKeyObject((0, import_node_crypto.createPrivateKey)({ key: der, format: "der", type: "pkcs8" }));
5622
+ return fromSecret({ type: "Ed25519", key: sha2562(seed) });
5623
+ }
5624
+ var sha2562 = (s) => sha256(new TextEncoder().encode(s));
5625
+
5626
+ // src/act/certificates.ts
5627
+ var MAX_PRESENTED = 20;
5628
+ function reliedKids(act2) {
5629
+ const kids = [act2?.authority?.artifact?.seal?.kid, ...(act2?.testimony ?? []).map((t) => t?.kid), ...(act2?.consents ?? []).map((c) => c?.kid)];
5630
+ return [...new Set(kids.filter((k) => typeof k === "string" && k !== ""))];
5631
+ }
5632
+ function presentedCertificates(available, act2) {
5633
+ const unique = /* @__PURE__ */ new Map();
5634
+ for (const c of available) unique.set(fingerprint(c), c);
5635
+ const kids = new Set(reliedKids(act2)), chosen = /* @__PURE__ */ new Map();
5636
+ for (let grew = true; grew; ) {
5637
+ grew = false;
5638
+ for (const [fp, c] of unique) {
5639
+ if (!chosen.has(fp) && kids.has(c.child_kid)) {
5640
+ chosen.set(fp, c);
5641
+ kids.add(c.parent_kid);
5642
+ grew = true;
5643
+ }
5644
+ }
5645
+ }
5646
+ return [...chosen.values()];
5647
+ }
5648
+ async function signRevocation(parent, target, reason, issuedAt = /* @__PURE__ */ new Date()) {
5649
+ const st = "kid" in target ? { revoked_kid: target.kid, reason, issued_at: issuedAt.toISOString() } : { revoked_fingerprint: fingerprint(target.certificate), reason, issued_at: issuedAt.toISOString() };
5650
+ if ("certificate" in target && target.certificate.parent_kid !== parent.kid) throw new Error("signRevocation: only the parent that issued a certificate can revoke it");
5651
+ return { ...st, kid: parent.kid, algorithm: parent.algorithm, signature: await parent.sign(jcs(revocationPayload(st))) };
5652
+ }
5653
+ async function signRevocationList(signer, statements, keyCerts = [], generatedAt = /* @__PURE__ */ new Date()) {
5654
+ const L = { generated_at: generatedAt.toISOString(), kid: signer.kid, algorithm: signer.algorithm, statements, ...keyCerts.length ? { key_certs: keyCerts } : {} };
5655
+ L.signature = await signer.sign(jcs(revocationListPayload(L)));
5656
+ return L;
5657
+ }
5658
+ function revocationListCertificates(available, statements, listSignerKid) {
5659
+ const byFp = new Map(available.map((c) => [fingerprint(c), c]));
5660
+ const revokedCerts = statements.map((s) => s.revoked_fingerprint && byFp.get(s.revoked_fingerprint)).filter((c) => !!c);
5661
+ const kids = [listSignerKid, ...statements.flatMap((s) => [s.kid, s.revoked_kid].filter((k) => !!k)), ...revokedCerts.map((c) => c.parent_kid)];
5662
+ const closure = presentedCertificates(available, { authority: { artifact: { seal: {} } }, testimony: kids.map((kid) => ({ kid })) });
5663
+ const out = new Map(closure.map((c) => [fingerprint(c), c]));
5664
+ for (const c of revokedCerts) out.set(fingerprint(c), c);
5665
+ return [...out.values()];
5666
+ }
5667
+ var RevocationUnavailable = class extends Error {
5668
+ constructor(message, retryAfterSeconds) {
5669
+ super(message);
5670
+ this.retryAfterSeconds = retryAfterSeconds;
5671
+ this.name = "RevocationUnavailable";
5672
+ }
5673
+ };
5674
+ function revocationSource(o) {
5675
+ const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
5676
+ const maxAge = (o.maxAgeSeconds ?? 3600) * 1e3, refresh = (o.refreshSeconds ?? 60) * 1e3;
5677
+ const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
5678
+ let asOf = 0, fetchedAt = 0, inFlight = null, lastError = "";
5679
+ async function load() {
5680
+ fetchedAt = now().getTime();
5681
+ try {
5682
+ const res = await f(o.url, { headers: { accept: "application/json" } });
5683
+ if (!res.ok) {
5684
+ lastError = `${o.url} answered ${res.status}`;
5685
+ return;
5686
+ }
5687
+ const r = verifyRevocationList(await res.json(), { rootPublicKeyPem: o.root, now: now(), ownCerts: o.ownCerts });
5688
+ if (r.decision !== "OK") {
5689
+ lastError = `${o.url} served a malformed list (${r.reason_code})`;
5690
+ return;
5691
+ }
5692
+ for (const k of r.revoked_kids) kids.add(k);
5693
+ for (const c of r.revoked_certs) certs.add(c);
5694
+ if (r.as_of) asOf = Math.max(asOf, Date.parse(r.as_of));
5695
+ else lastError = `${o.url}'s list is not signed by a key your root reaches, so it does not show it is current`;
5696
+ } catch (e) {
5697
+ lastError = `${o.url}: ${e?.message ?? e}`;
5698
+ }
5699
+ }
5700
+ return async () => {
5701
+ const t = now().getTime();
5702
+ if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
5703
+ inFlight = null;
5704
+ });
5705
+ if (inFlight) await inFlight;
5706
+ if (!asOf || t - asOf > maxAge) {
5707
+ throw new RevocationUnavailable(`No current revocation list${asOf ? `: the newest is from ${new Date(asOf).toISOString()}` : ""}${lastError ? ` (${lastError})` : ""}. Not committing until revocations can be checked.`, Math.ceil(refresh / 1e3));
5708
+ }
5709
+ return { kids: new Set(kids), certs: new Set(certs) };
5710
+ };
5417
5711
  }
5418
5712
 
5419
5713
  // src/act/documents.ts
@@ -5518,6 +5812,11 @@ var Rollback = class extends Error {
5518
5812
  this.answer = answer2;
5519
5813
  }
5520
5814
  };
5815
+ var LostRace = class extends Error {
5816
+ constructor() {
5817
+ super("replayed");
5818
+ }
5819
+ };
5521
5820
  function accept(o) {
5522
5821
  const kid = kidOf2(o.root);
5523
5822
  for (const a of o.authorities) {
@@ -5525,7 +5824,7 @@ function accept(o) {
5525
5824
  throw new Error(`accept(): authority ${a.artifactId} uses currencyPolicy "record" but has no inForceSealHash`);
5526
5825
  }
5527
5826
  }
5528
- if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only.");
5827
+ if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only; use postgresStore() in production.");
5529
5828
  const store = o.store ?? memoryStore();
5530
5829
  const maxWindowSeconds = o.maxWindowSeconds ?? 900;
5531
5830
  const maxActBytes = o.maxActBytes ?? 1024 * 1024;
@@ -5551,8 +5850,7 @@ function accept(o) {
5551
5850
  valid_until: new Date(now().getTime() + (o.documentTtlSeconds ?? 86400) * 1e3).toISOString()
5552
5851
  };
5553
5852
  }
5554
- async function verifyWith(act2, authority, facts, at, seen) {
5555
- const rev = await o.revocations?.() ?? {};
5853
+ function verifyWith(act2, authority, facts, at, seen, rev) {
5556
5854
  return verifyAct(act2, {
5557
5855
  relyingPartyId: o.relyingPartyId,
5558
5856
  rootPublicKeyPem: o.root,
@@ -5584,9 +5882,20 @@ function accept(o) {
5584
5882
  const authority = o.authorities.find((a) => a.artifactId === artifactId);
5585
5883
  if (!authority) return answer({ decision: "NOT_ACCEPTED", reason_code: "authority_not_accepted", artifact_id: artifactId ?? null });
5586
5884
  if (!authority.actions.includes(act2?.action?.type)) return answer({ decision: "NOT_ACCEPTED", reason_code: "action_not_accepted", action_type: act2?.action?.type ?? null });
5885
+ let rev;
5886
+ try {
5887
+ rev = await o.revocations?.() ?? {};
5888
+ } catch (e) {
5889
+ const retry = e instanceof RevocationUnavailable ? e.retryAfterSeconds : 60;
5890
+ return {
5891
+ status: 503,
5892
+ headers: { "content-type": "application/json", "retry-after": String(retry) },
5893
+ body: { error: "Revocation status is not current, so nothing is committed. Resubmit this same Act later.", detail: String(e?.message ?? e), retry_after: retry }
5894
+ };
5895
+ }
5587
5896
  const verifiedAt = now();
5588
5897
  const facts = await o.localFacts?.(act2) ?? {};
5589
- const verdict = await verifyWith(act2, authority, facts, verifiedAt, await store.hasNonce(act2.nonce));
5898
+ const verdict = verifyWith(act2, authority, facts, verifiedAt, await store.hasNonce(act2.nonce), rev);
5590
5899
  const receiptOf = (v, f, committedAt) => ({
5591
5900
  act_id: act2.act_id,
5592
5901
  act_digest: String(v.act_digest ?? ""),
@@ -5606,35 +5915,34 @@ function accept(o) {
5606
5915
  if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
5607
5916
  await o.onVerdict?.(verdict, receiptOf(verdict, facts));
5608
5917
  if (verdict.decision === "ESCALATED") {
5609
- const esc2 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
5610
- return answer(verdict, { role: esc2?.role_required ?? null, retry_until: act2.expires_at });
5918
+ const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
5919
+ return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
5611
5920
  }
5612
5921
  if (o.mode === "shadow") return { ...answer(verdict), status: 200, body: { ...answer(verdict).body, shadow: true } };
5613
5922
  return answer(verdict);
5614
5923
  }
5615
5924
  try {
5616
5925
  return await store.transaction(async (t) => {
5617
- if (!await t.claimNonce(act2.nonce)) {
5618
- throw new Rollback(answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) }));
5619
- }
5926
+ if (!await t.claimNonce(act2.nonce)) throw new LostRace();
5620
5927
  let final = verdict, finalFacts = facts;
5621
5928
  if (o.localFacts) {
5622
5929
  const fresh = await o.localFacts(act2, t.tx);
5623
5930
  if (!sameFacts(fresh, facts)) {
5624
5931
  finalFacts = fresh;
5625
- final = await verifyWith(act2, authority, fresh, verifiedAt, false);
5932
+ final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
5626
5933
  if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
5627
5934
  }
5628
5935
  }
5629
5936
  const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
5630
5937
  const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
5631
5938
  const receipt = receiptOf(final, finalFacts, now());
5632
- await t.putReceipt(act2.nonce, receipt);
5939
+ await t.putReceipt(act2.nonce, receipt, act2);
5633
5940
  await o.onVerdict?.(final, receipt);
5634
5941
  return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
5635
5942
  });
5636
5943
  } catch (e) {
5637
5944
  if (e instanceof Rollback) return e.answer;
5945
+ if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
5638
5946
  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) } };
5639
5947
  }
5640
5948
  }
@@ -5673,7 +5981,7 @@ function accept(o) {
5673
5981
  if (size > maxActBytes) break;
5674
5982
  chunks.push(c);
5675
5983
  }
5676
- body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) : Buffer.concat(chunks);
5984
+ body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) : concat2(...chunks);
5677
5985
  }
5678
5986
  const a = await handle({ method: req.method, headers: req.headers, body });
5679
5987
  send(a.status, a.headers, JSON.stringify(a.body));
@@ -5684,7 +5992,7 @@ function accept(o) {
5684
5992
  function consentOrigins(act2) {
5685
5993
  const origins = (Array.isArray(act2?.consents) ? act2.consents : []).filter((c) => c?.algorithm === "WebAuthn-ES256" && typeof c.client_data_json === "string").map((c) => {
5686
5994
  try {
5687
- return JSON.parse(Buffer.from(c.client_data_json, "base64url").toString("utf8")).origin;
5995
+ return JSON.parse(fromUtf82(fromBase64url2(c.client_data_json))).origin;
5688
5996
  } catch {
5689
5997
  return null;
5690
5998
  }
@@ -5692,8 +6000,74 @@ function consentOrigins(act2) {
5692
6000
  return origins.length ? { consent_origins: [...new Set(origins)] } : {};
5693
6001
  }
5694
6002
 
6003
+ // src/act/postgres.ts
6004
+ var IDENT = /^[a-z_][a-z0-9_]{0,62}(\.[a-z_][a-z0-9_]{0,62})?$/;
6005
+ function postgresStore(pool, opts = {}) {
6006
+ const table = opts.table ?? "nomos_acts";
6007
+ if (!IDENT.test(table)) throw new Error(`postgresStore: "${table}" is not a plain lower-case table name`);
6008
+ const schema = `CREATE TABLE IF NOT EXISTS ${table} (
6009
+ nonce text PRIMARY KEY, -- SPEC-008 \xA75.2: the nonce ledger
6010
+ act_id text NOT NULL,
6011
+ receipt json NOT NULL, -- SPEC-008 \xA79; json, not jsonb: kept byte for byte
6012
+ act json NOT NULL, -- the exact Act verified, for re-verification
6013
+ committed_at timestamptz NOT NULL DEFAULT now()
6014
+ );`;
6015
+ return {
6016
+ schema,
6017
+ async migrate() {
6018
+ await pool.query(schema);
6019
+ },
6020
+ async hasNonce(nonce) {
6021
+ const r = await pool.query(`SELECT 1 FROM ${table} WHERE nonce = $1`, [nonce]);
6022
+ return (r.rowCount ?? r.rows.length) > 0;
6023
+ },
6024
+ async receiptFor(nonce) {
6025
+ const r = await pool.query(`SELECT receipt FROM ${table} WHERE nonce = $1`, [nonce]);
6026
+ return r.rows[0]?.receipt ?? null;
6027
+ },
6028
+ async transaction(fn) {
6029
+ const client = await pool.connect();
6030
+ let claimed = null, completed = false;
6031
+ try {
6032
+ await client.query("BEGIN");
6033
+ const out = await fn({
6034
+ tx: client,
6035
+ async claimNonce(nonce) {
6036
+ const r = await client.query(
6037
+ `INSERT INTO ${table} (nonce, act_id, receipt, act) VALUES ($1, '', '{}'::json, '{}'::json)
6038
+ ON CONFLICT (nonce) DO NOTHING RETURNING nonce`,
6039
+ [nonce]
6040
+ );
6041
+ const won = (r.rowCount ?? r.rows.length) === 1;
6042
+ if (won) claimed = nonce;
6043
+ return won;
6044
+ },
6045
+ async putReceipt(nonce, receipt, act2) {
6046
+ if (nonce !== claimed) throw new Error("postgresStore: putReceipt for a nonce this transaction did not claim");
6047
+ await client.query(
6048
+ `UPDATE ${table} SET act_id = $2, receipt = $3::json, act = $4::json WHERE nonce = $1`,
6049
+ [nonce, receipt.act_id, JSON.stringify(receipt), JSON.stringify(act2 ?? {})]
6050
+ );
6051
+ completed = true;
6052
+ }
6053
+ });
6054
+ if (claimed && !completed) throw new Error("postgresStore: the transaction claimed a nonce but stored no receipt");
6055
+ await client.query("COMMIT");
6056
+ client.release();
6057
+ return out;
6058
+ } catch (e) {
6059
+ let broken;
6060
+ await client.query("ROLLBACK").catch((err) => {
6061
+ broken = err;
6062
+ });
6063
+ client.release(broken);
6064
+ throw e;
6065
+ }
6066
+ }
6067
+ };
6068
+ }
6069
+
5695
6070
  // src/act/statements.ts
5696
- var import_node_crypto2 = require("crypto");
5697
6071
  async function certify(parent, child, opts) {
5698
6072
  const unsigned = {
5699
6073
  parent_kid: parent.kid,
@@ -5747,8 +6121,8 @@ function buildAct(o) {
5747
6121
  const window = o.windowSeconds ?? 300;
5748
6122
  const act2 = {
5749
6123
  act_version: "1",
5750
- act_id: `act-${(0, import_node_crypto2.randomUUID)()}`,
5751
- nonce: (0, import_node_crypto2.randomBytes)(16).toString("base64"),
6124
+ act_id: `act-${randomUUID()}`,
6125
+ nonce: toBase642(randomBytes3(16)),
5752
6126
  // Issued a second in the past so a relying party's clock a moment behind ours still accepts it.
5753
6127
  issued_at: new Date(now.getTime() - 1e3).toISOString(),
5754
6128
  expires_at: new Date(now.getTime() - 1e3 + window * 1e3).toISOString(),
@@ -5762,6 +6136,575 @@ function buildAct(o) {
5762
6136
  return act2;
5763
6137
  }
5764
6138
 
6139
+ // src/act/registry.ts
6140
+ function memoryRegistryStore() {
6141
+ const certs = [], sts = [];
6142
+ return {
6143
+ async certificates() {
6144
+ return [...certs];
6145
+ },
6146
+ async addCertificate(c) {
6147
+ if (!certs.some((x) => fingerprint(x) === fingerprint(c))) certs.push(c);
6148
+ },
6149
+ async statements() {
6150
+ return [...sts];
6151
+ },
6152
+ async addStatement(s) {
6153
+ sts.push(s);
6154
+ }
6155
+ };
6156
+ }
6157
+ var RegistryError = class extends Error {
6158
+ constructor(message, code) {
6159
+ super(message);
6160
+ this.code = code;
6161
+ this.name = "RegistryError";
6162
+ }
6163
+ };
6164
+ var any = () => ({ ok: true });
6165
+ function certificateRegistry(o) {
6166
+ const rootKid = kidOf(o.root);
6167
+ if (!rootKid) throw new RegistryError("certificateRegistry: the root is not a canonical Ed25519 or P-256 public key", "bad_root");
6168
+ const now = o.now ?? (() => /* @__PURE__ */ new Date());
6169
+ async function revokedSets() {
6170
+ const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
6171
+ for (const s of await o.store.statements()) s.revoked_kid ? kids.add(s.revoked_kid) : s.revoked_fingerprint && certs.add(s.revoked_fingerprint);
6172
+ return { kids, certs };
6173
+ }
6174
+ async function scopesOf(kid) {
6175
+ if (kid === rootKid) return { ok: true, scopes: [{ terms: {} }] };
6176
+ const { kids, certs } = await revokedSets();
6177
+ const r = resolveKey({ targetKid: kid, pool: await o.store.certificates(), rootPublicKeyPem: o.root, purpose: any, now: now(), revokedKids: kids, revokedCerts: certs, allPaths: true });
6178
+ if (r.decision !== "RESOLVED") return { ok: false, reason: `${r.decision}${r.reason_code ? ` (${r.reason_code})` : ""}` };
6179
+ return { ok: true, scopes: r.admitted ?? [r.effective] };
6180
+ }
6181
+ async function pool(filter) {
6182
+ const { kids, certs } = await revokedSets(), t = now().getTime();
6183
+ return (await o.store.certificates()).filter((c) => !kids.has(c.child_kid) && !certs.has(fingerprint(c)) && Date.parse(c.expires_at) > t && (!filter || filter(c)));
6184
+ }
6185
+ return {
6186
+ rootKid,
6187
+ pool,
6188
+ /** NOMOS-SPEC-007 §3: certify `child` under `parent`. Refuses anything a verifier would refuse. */
6189
+ async issue(io) {
6190
+ if (!kidOf(io.child.publicKeyPem)) throw new RegistryError("The child key is not a canonical Ed25519 or P-256 key (NOMOS-SPEC-010 \xA73.1)", "invalid_key");
6191
+ const scope = parseScope(io.scope ?? null);
6192
+ if (!scope.ok) throw new RegistryError(`Malformed scope: ${scope.detail}`, "malformed_scope");
6193
+ const parentScopes = await scopesOf(io.parent.kid);
6194
+ if (!parentScopes.ok) throw new RegistryError(`The parent ${io.parent.kid} is not reached from the root: ${parentScopes.reason}`, "parent_not_recognized");
6195
+ const fits = parentScopes.scopes.filter((p) => suitePermitted(p, io.parent.algorithm) && isNarrowerOrEqual(scope.scope, p).ok);
6196
+ if (!fits.length) {
6197
+ const widened = parentScopes.scopes.map((p) => isNarrowerOrEqual(scope.scope, p)).find((x) => !x.ok);
6198
+ throw new RegistryError(widened ? `The scope widens the parent's: ${widened.detail}` : `The parent may not sign under ${io.parent.algorithm}`, widened ? "widening" : "suite_not_permitted");
6199
+ }
6200
+ const expiresAt = io.expiresAt ?? (io.days ? new Date(now().getTime() + io.days * 864e5) : void 0);
6201
+ if (!expiresAt) throw new RegistryError("Give expiresAt or days: a certificate always expires", "no_expiry");
6202
+ const c = await certify(io.parent, io.child, { scope: io.scope, issuedAt: io.issuedAt ?? now(), expiresAt });
6203
+ await o.store.addCertificate(c);
6204
+ return c;
6205
+ },
6206
+ /** rev. 1.10 §5.1: `parent` revokes a key it certified. The key is dead on every path. */
6207
+ async revokeKey(io) {
6208
+ const certs = (await o.store.certificates()).filter((c) => c.child_kid === io.kid && c.parent_kid === io.parent.kid);
6209
+ if (!certs.length) throw new RegistryError(`${io.parent.kid} never certified ${io.kid}: only a key's parent can revoke it`, "not_the_parent");
6210
+ const st = await signRevocation(io.parent, { kid: io.kid }, io.reason, now());
6211
+ await o.store.addStatement(st);
6212
+ return st;
6213
+ },
6214
+ /** rev. 1.10 §5.1: withdraw one certificate; the key stays valid under any other. */
6215
+ async revokeCertificate(io) {
6216
+ const c = (await o.store.certificates()).find((x) => fingerprint(x) === io.fingerprint);
6217
+ if (!c) throw new RegistryError(`No certificate ${io.fingerprint} in this registry`, "unknown_certificate");
6218
+ const st = await signRevocation(io.parent, { certificate: c }, io.reason, now());
6219
+ await o.store.addStatement(st);
6220
+ return st;
6221
+ },
6222
+ /** The certificates that connect `kid` to the root: what a witness or approver returns with
6223
+ * what it signs (SPEC-009 0.2.0 §4, §5). */
6224
+ async chainFor(kid) {
6225
+ return presentedCertificates(await pool(), { authority: { artifact: { seal: { kid } } } });
6226
+ },
6227
+ /** How `kid` is reached from the root, and with which effective scopes, or why it is not. */
6228
+ async check(kid) {
6229
+ return scopesOf(kid);
6230
+ },
6231
+ certificates: () => o.store.certificates(),
6232
+ statements: () => o.store.statements(),
6233
+ /** rev. 1.10 §5.3: the signed list, carrying the certificates that make it checkable. The
6234
+ * signer should be reached from the root, or the list establishes no date (§5.3). */
6235
+ async revocationList(signer) {
6236
+ const statements = await o.store.statements();
6237
+ return signRevocationList(signer, statements, revocationListCertificates(await o.store.certificates(), statements, signer.kid), now());
6238
+ }
6239
+ };
6240
+ }
6241
+ function postgresRegistryStore(pool, opts = {}) {
6242
+ const p = opts.prefix ?? "nomos_registry";
6243
+ if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new RegistryError(`"${p}" is not a plain lower-case prefix`, "bad_prefix");
6244
+ const schema = `CREATE TABLE IF NOT EXISTS ${p}_certificates (
6245
+ fingerprint text PRIMARY KEY, child_kid text NOT NULL, parent_kid text NOT NULL, certificate json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
6246
+ );
6247
+ CREATE TABLE IF NOT EXISTS ${p}_statements (
6248
+ id bigserial PRIMARY KEY, target text NOT NULL, kid text NOT NULL, statement json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
6249
+ );`;
6250
+ return {
6251
+ schema,
6252
+ async migrate() {
6253
+ await pool.query(schema);
6254
+ },
6255
+ async certificates() {
6256
+ return (await pool.query(`SELECT certificate FROM ${p}_certificates ORDER BY added_at, fingerprint`)).rows.map((r) => r.certificate);
6257
+ },
6258
+ async addCertificate(c) {
6259
+ await pool.query(
6260
+ `INSERT INTO ${p}_certificates (fingerprint, child_kid, parent_kid, certificate) VALUES ($1, $2, $3, $4::json) ON CONFLICT (fingerprint) DO NOTHING`,
6261
+ [fingerprint(c), c.child_kid, c.parent_kid, JSON.stringify(c)]
6262
+ );
6263
+ },
6264
+ async statements() {
6265
+ return (await pool.query(`SELECT statement FROM ${p}_statements ORDER BY id`)).rows.map((r) => r.statement);
6266
+ },
6267
+ async addStatement(s) {
6268
+ await pool.query(`INSERT INTO ${p}_statements (target, kid, statement) VALUES ($1, $2, $3::json)`, [s.revoked_kid ?? s.revoked_fingerprint, s.kid, JSON.stringify(s)]);
6269
+ }
6270
+ };
6271
+ }
6272
+
6273
+ // src/act/passkeys.ts
6274
+ function cbor(b) {
6275
+ let i = 0;
6276
+ const len = (info) => {
6277
+ if (info < 24) return info;
6278
+ const n = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : -1;
6279
+ if (n < 0) throw new Error("unsupported CBOR length");
6280
+ let v2 = 0;
6281
+ for (let k = 0; k < n; k++) v2 = v2 * 256 + b[i++];
6282
+ return v2;
6283
+ };
6284
+ const item = (depth) => {
6285
+ if (depth > 16 || i >= b.length) throw new Error("malformed CBOR");
6286
+ const head = b[i++], major = head >> 5, info = head & 31;
6287
+ switch (major) {
6288
+ case 0:
6289
+ return len(info);
6290
+ case 1:
6291
+ return -1 - len(info);
6292
+ case 2: {
6293
+ const n = len(info);
6294
+ const v2 = b.slice(i, i + n);
6295
+ i += n;
6296
+ if (v2.length !== n) throw new Error("truncated CBOR");
6297
+ return v2;
6298
+ }
6299
+ case 3: {
6300
+ const n = len(info);
6301
+ const v2 = fromUtf82(b.slice(i, i + n));
6302
+ i += n;
6303
+ return v2;
6304
+ }
6305
+ case 4: {
6306
+ const n = len(info), a = [];
6307
+ for (let k = 0; k < n; k++) a.push(item(depth + 1));
6308
+ return a;
6309
+ }
6310
+ case 5: {
6311
+ const n = len(info), m = /* @__PURE__ */ new Map();
6312
+ for (let k = 0; k < n; k++) m.set(item(depth + 1), item(depth + 1));
6313
+ return m;
6314
+ }
6315
+ case 7:
6316
+ if (info === 20) return false;
6317
+ if (info === 21) return true;
6318
+ if (info === 22) return null;
6319
+ throw new Error("unsupported CBOR simple value");
6320
+ default:
6321
+ throw new Error("unsupported CBOR type");
6322
+ }
6323
+ };
6324
+ const v = item(0);
6325
+ return { value: v, end: i };
6326
+ }
6327
+ function readAttestation(attestationObject) {
6328
+ const att = cbor(attestationObject).value;
6329
+ if (!(att instanceof Map) || !(att.get("authData") instanceof Uint8Array)) throw new Error("not an attestation object");
6330
+ const a = att.get("authData");
6331
+ if (a.length < 55 || !(a[32] & 64)) throw new Error("no attested credential data");
6332
+ const idLen = a[53] << 8 | a[54];
6333
+ const credentialId = a.slice(55, 55 + idLen);
6334
+ const cose = cbor(a.slice(55 + idLen)).value;
6335
+ if (!(cose instanceof Map) || cose.get(1) !== 2 || cose.get(3) !== -7 || cose.get(-1) !== 1) throw new Error("the passkey is not an ES256 P-256 key (COSE kty 2, alg -7, crv 1)");
6336
+ const x = cose.get(-2), y = cose.get(-3);
6337
+ if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) throw new Error("malformed COSE key");
6338
+ const spki = concat2(Uint8Array.from([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4]), x, y);
6339
+ const publicKeyPem = pem("PUBLIC KEY", spki);
6340
+ if (!kidOf(publicKeyPem)) throw new Error("the passkey's key is not a valid P-256 point");
6341
+ return { authData: a, flags: a[32], credentialId, publicKeyPem };
6342
+ }
6343
+ async function signEnrolmentToken(authority, c) {
6344
+ if (!/^[^,\s]+$/.test(c.approver)) throw new Error("An approver id has no commas or spaces (NOMOS-SPEC-007 rev. 1.9 \xA73.7)");
6345
+ const claims = { approver: c.approver, roles: [...c.roles], origin: c.origin.replace(/\/$/, ""), expires_at: c.expiresAt.toISOString(), nonce: toBase64url2(randomBytes3(16)) };
6346
+ const token = { ...claims, kid: authority.kid, algorithm: authority.algorithm, signature: await authority.sign(jcs(claims)) };
6347
+ return toBase64url2(utf82(JSON.stringify(token)));
6348
+ }
6349
+ function verifyEnrolmentToken(token, authorityPem, origin, now) {
6350
+ let t;
6351
+ try {
6352
+ t = JSON.parse(fromUtf82(fromBase64url2(token)));
6353
+ } catch {
6354
+ return { error: "not an enrolment link" };
6355
+ }
6356
+ const claims = { approver: t?.approver, roles: t?.roles, origin: t?.origin, expires_at: t?.expires_at, nonce: t?.nonce };
6357
+ if (typeof claims.approver !== "string" || !Array.isArray(claims.roles) || !claims.roles.every((r) => typeof r === "string") || typeof claims.nonce !== "string") return { error: "malformed enrolment link" };
6358
+ if (t.kid !== kidOf(authorityPem) || !verifySuite(t.algorithm, authorityPem, jcs(claims), { signature: t.signature }).ok) return { error: "this enrolment link was not signed by the enrolment authority" };
6359
+ if (claims.origin !== origin) return { error: "this enrolment link is for another service" };
6360
+ if (!(Date.parse(claims.expires_at) > now.getTime())) return { error: "this enrolment link has expired" };
6361
+ return claims;
6362
+ }
6363
+ function memoryPasskeyStore() {
6364
+ const used = /* @__PURE__ */ new Set(), challenges = /* @__PURE__ */ new Map(), creds = [], pend = /* @__PURE__ */ new Map();
6365
+ return {
6366
+ async claimToken(n) {
6367
+ if (used.has(n)) return false;
6368
+ used.add(n);
6369
+ return true;
6370
+ },
6371
+ async tokenUsed(n) {
6372
+ return used.has(n);
6373
+ },
6374
+ async saveEnrolmentChallenge(n, c) {
6375
+ if (!used.has(n)) challenges.set(n, c);
6376
+ },
6377
+ async enrolmentChallenge(n) {
6378
+ return used.has(n) ? null : challenges.get(n) ?? null;
6379
+ },
6380
+ async addCredential(c) {
6381
+ creds.push(structuredClone(c));
6382
+ },
6383
+ async credentials() {
6384
+ return structuredClone(creds);
6385
+ },
6386
+ async createPending(p) {
6387
+ pend.set(p.id, structuredClone(p));
6388
+ },
6389
+ async pending(id) {
6390
+ const p = pend.get(id);
6391
+ return p ? structuredClone(p) : null;
6392
+ },
6393
+ async addAttempt(id, c, a) {
6394
+ const p = pend.get(id);
6395
+ if (!p || p.status !== "pending") return false;
6396
+ p.attempts[c] = { ...a };
6397
+ return true;
6398
+ },
6399
+ async resolvePending(id, out) {
6400
+ const p = pend.get(id);
6401
+ if (!p || p.status !== "pending") return false;
6402
+ Object.assign(p, structuredClone(out));
6403
+ return true;
6404
+ }
6405
+ };
6406
+ }
6407
+ function postgresPasskeyStore(pool, opts = {}) {
6408
+ const p = opts.prefix ?? "nomos_passkeys";
6409
+ if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new Error(`"${p}" is not a plain lower-case prefix`);
6410
+ const schema = `CREATE TABLE IF NOT EXISTS ${p}_tokens (nonce text PRIMARY KEY, challenge text, used boolean NOT NULL DEFAULT false);
6411
+ CREATE TABLE IF NOT EXISTS ${p}_credentials (credential_id text PRIMARY KEY, credential json NOT NULL, added_at timestamptz NOT NULL DEFAULT now());
6412
+ CREATE TABLE IF NOT EXISTS ${p}_pending (id text PRIMARY KEY, approval jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now());`;
6413
+ return {
6414
+ schema,
6415
+ async migrate() {
6416
+ await pool.query(schema);
6417
+ },
6418
+ async claimToken(n) {
6419
+ const r = await pool.query(`INSERT INTO ${p}_tokens (nonce, used) VALUES ($1, true) ON CONFLICT (nonce) DO UPDATE SET used = true WHERE ${p}_tokens.used = false RETURNING nonce`, [n]);
6420
+ return (r.rowCount ?? r.rows.length) === 1;
6421
+ },
6422
+ async tokenUsed(n) {
6423
+ return (await pool.query(`SELECT 1 FROM ${p}_tokens WHERE nonce = $1 AND used = true`, [n])).rows.length > 0;
6424
+ },
6425
+ async saveEnrolmentChallenge(n, c) {
6426
+ await pool.query(`INSERT INTO ${p}_tokens (nonce, challenge) VALUES ($1, $2) ON CONFLICT (nonce) DO UPDATE SET challenge = $2 WHERE ${p}_tokens.used = false`, [n, c]);
6427
+ },
6428
+ async enrolmentChallenge(n) {
6429
+ return (await pool.query(`SELECT challenge FROM ${p}_tokens WHERE nonce = $1 AND used = false`, [n])).rows[0]?.challenge ?? null;
6430
+ },
6431
+ async addCredential(c) {
6432
+ await pool.query(`INSERT INTO ${p}_credentials (credential_id, credential) VALUES ($1, $2::json)`, [c.credential_id, JSON.stringify(c)]);
6433
+ },
6434
+ async credentials() {
6435
+ return (await pool.query(`SELECT credential FROM ${p}_credentials ORDER BY added_at`)).rows.map((r) => r.credential);
6436
+ },
6437
+ async createPending(a) {
6438
+ await pool.query(`INSERT INTO ${p}_pending (id, approval) VALUES ($1, $2::jsonb)`, [a.id, JSON.stringify(a)]);
6439
+ },
6440
+ async pending(id) {
6441
+ return (await pool.query(`SELECT approval FROM ${p}_pending WHERE id = $1`, [id])).rows[0]?.approval ?? null;
6442
+ },
6443
+ // Each change is one conditional statement: Postgres serializes them on the row, and none
6444
+ // rewrites the whole record, so a stale reader cannot write an old status back.
6445
+ async addAttempt(id, c, a) {
6446
+ const r = await pool.query(`UPDATE ${p}_pending SET approval = jsonb_set(approval, ARRAY['attempts', $2::text], $3::jsonb), updated_at = now() WHERE id = $1 AND approval->>'status' = 'pending' RETURNING id`, [id, c, JSON.stringify(a)]);
6447
+ return (r.rowCount ?? r.rows.length) === 1;
6448
+ },
6449
+ async resolvePending(id, out) {
6450
+ const r = await pool.query(`UPDATE ${p}_pending SET approval = approval || $2::jsonb, updated_at = now() WHERE id = $1 AND approval->>'status' = 'pending' RETURNING id`, [id, JSON.stringify(out)]);
6451
+ return (r.rowCount ?? r.rows.length) === 1;
6452
+ }
6453
+ };
6454
+ }
6455
+ var MAX_ATTEMPTS = 50;
6456
+ var esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6457
+ function passkeyApprover(o) {
6458
+ const base = o.origin.replace(/\/$/, "");
6459
+ const rpId = o.rpId ?? new URL(base).hostname;
6460
+ const store = o.store ?? memoryPasskeyStore();
6461
+ if (!o.store) console.warn("[nomos] passkeyApprover(): using the in-memory store. Enrolled passkeys are lost on restart. Development only; use postgresPasskeyStore() in production.");
6462
+ const now = o.now ?? (() => /* @__PURE__ */ new Date());
6463
+ const rpIdHash = sha256(utf82(rpId));
6464
+ const expired = (p) => p.status === "pending" && Date.parse(p.act.expires_at) <= now().getTime();
6465
+ const statusOf = (p) => expired(p) ? "expired" : p.status;
6466
+ function clientData(b64, type, challenge) {
6467
+ let c;
6468
+ try {
6469
+ c = JSON.parse(fromUtf82(fromBase64url2(b64)));
6470
+ } catch {
6471
+ return "unreadable client data";
6472
+ }
6473
+ if (c?.type !== type) return `expected a ${type} ceremony`;
6474
+ if (c?.challenge !== challenge) return "the challenge does not match";
6475
+ if (c?.origin !== base) return `the ceremony ran on ${c?.origin}, not ${base}`;
6476
+ return null;
6477
+ }
6478
+ async function route(method, path, json) {
6479
+ if (method === "POST" && path === "/nomos/approvals") {
6480
+ const { role, act: act2 } = await json();
6481
+ if (!o.roles.includes(role)) return { status: 404, body: { error: `This service does not approve as ${role}.` } };
6482
+ if (typeof act2?.expires_at !== "string" || !act2?.action) return { status: 400, body: { error: "The request needs the Act." } };
6483
+ const id = randomUUID();
6484
+ await store.createPending({ id, role, act: act2, status: "pending", created_at: now().toISOString(), attempts: {} });
6485
+ await o.onPending?.({ id, role, act: act2, approvalUrl: `${base}/approve/${id}` });
6486
+ return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
6487
+ }
6488
+ let m = path.match(/^\/nomos\/approvals\/([0-9a-f-]{36})$/);
6489
+ if (method === "GET" && m) {
6490
+ const p = await store.pending(m[1]);
6491
+ if (!p) return null;
6492
+ const s = statusOf(p);
6493
+ return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...p.key_certs?.length ? { key_certs: p.key_certs } : {} } : {} } };
6494
+ }
6495
+ m = path.match(/^\/approve\/([0-9a-f-]{36})(?:\/(devices|options|assertion|refuse))?$/);
6496
+ if (m) {
6497
+ const p = await store.pending(m[1]);
6498
+ if (!p) return null;
6499
+ if (method === "GET" && !m[2]) return { status: 200, type: "text/html", body: approvalPage(p, statusOf(p), o.listApprovers === true ? (await store.credentials()).filter((c) => c.roles.includes(p.role)) : null) };
6500
+ if (method !== "POST" || !m[2]) return null;
6501
+ if (statusOf(p) !== "pending") return { status: 409, body: { error: `This request is ${statusOf(p)}.` } };
6502
+ if (m[2] === "refuse") return await store.resolvePending(p.id, { status: "refused" }) ? { status: 200, body: { status: "refused" } } : { status: 409, body: { error: "This request is no longer pending." } };
6503
+ const req = await json();
6504
+ if (m[2] === "devices") {
6505
+ const mine = (await store.credentials()).filter((c) => c.approver === String(req?.approver ?? "") && c.roles.includes(p.role));
6506
+ return { status: 200, body: { devices: mine.map((c) => ({ credential_id: c.credential_id, label: c.label })) } };
6507
+ }
6508
+ const cred = (await store.credentials()).find((c) => c.credential_id === req?.credential_id);
6509
+ if (!cred || !cred.roles.includes(p.role)) return { status: 403, body: { error: `That passkey does not approve as ${p.role}.` } };
6510
+ if (m[2] === "options") {
6511
+ const signed_at = now().toISOString();
6512
+ const payload2 = consentPayload({ binding_digest: bindingDigest(p.act), role: p.role, decision: "approve", signed_at, kid: cred.kid });
6513
+ const challenge = toBase64url2(sha256(jcs(payload2)));
6514
+ if (Object.keys(p.attempts).length >= MAX_ATTEMPTS) return { status: 429, body: { error: "Too many approval ceremonies for this request." } };
6515
+ const expires_at = new Date(Math.min(Date.parse(p.act.expires_at), now().getTime() + 5 * 6e4)).toISOString();
6516
+ if (!await store.addAttempt(p.id, challenge, { credential_id: cred.credential_id, signed_at, expires_at })) return { status: 409, body: { error: "This request is no longer pending." } };
6517
+ return { status: 200, body: { challenge, rpId, allowCredentials: [{ type: "public-key", id: cred.credential_id }], userVerification: "required", timeout: 12e4 } };
6518
+ }
6519
+ let offered;
6520
+ try {
6521
+ offered = JSON.parse(fromUtf82(fromBase64url2(String(req.client_data_json ?? "")))).challenge;
6522
+ } catch {
6523
+ return { status: 400, body: { error: "unreadable client data" } };
6524
+ }
6525
+ const attempt = p.attempts[offered];
6526
+ if (!attempt || attempt.credential_id !== cred.credential_id) return { status: 400, body: { error: "This ceremony was not offered for this passkey." } };
6527
+ if (Date.parse(attempt.expires_at) <= now().getTime()) return { status: 400, body: { error: "This ceremony has expired; start again." } };
6528
+ const consent = {
6529
+ binding_digest: bindingDigest(p.act),
6530
+ role: p.role,
6531
+ decision: "approve",
6532
+ signed_at: attempt.signed_at,
6533
+ kid: cred.kid,
6534
+ algorithm: "WebAuthn-ES256",
6535
+ authenticator_data: String(req.authenticator_data ?? ""),
6536
+ client_data_json: String(req.client_data_json ?? ""),
6537
+ signature: String(req.signature ?? "")
6538
+ };
6539
+ const payload = jcs(consentPayload(consent));
6540
+ const cd = clientData(consent.client_data_json, "webauthn.get", toBase64url2(sha256(payload)));
6541
+ if (cd) return { status: 400, body: { error: cd } };
6542
+ const ad = fromBase64url2(consent.authenticator_data);
6543
+ if (ad.length < 37 || !equal2(ad.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The assertion is for another relying-party id." } };
6544
+ const v = verifySuite("WebAuthn-ES256", cred.public_key_pem, payload, consent);
6545
+ if (!v.ok) return { status: 400, body: { error: `The passkey's signature does not verify (${v.reason}).` } };
6546
+ const key_certs = await o.registry.chainFor(cred.kid);
6547
+ if (!key_certs.length) return { status: 409, body: { error: "This passkey is no longer certified (revoked or expired)." } };
6548
+ if (!await store.resolvePending(p.id, { status: "approved", consent, key_certs })) return { status: 409, body: { error: "This request is no longer pending." } };
6549
+ return { status: 200, body: { status: "approved" } };
6550
+ }
6551
+ if (method === "GET" && path === "/enroll") return { status: 200, type: "text/html", body: enrolPage(o.rpName ?? rpId) };
6552
+ if (method === "POST" && (path === "/nomos/enrol/begin" || path === "/nomos/enrol/finish")) {
6553
+ const req = await json();
6554
+ const claims = verifyEnrolmentToken(String(req?.token ?? ""), o.enrolmentAuthority, base, now());
6555
+ if ("error" in claims) return { status: 403, body: { error: claims.error } };
6556
+ if (!claims.roles.every((r) => o.roles.includes(r))) return { status: 403, body: { error: `This service does not enrol for ${claims.roles.join(", ")}.` } };
6557
+ if (path === "/nomos/enrol/begin") {
6558
+ if (await store.tokenUsed(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
6559
+ const challenge2 = toBase64url2(randomBytes3(32));
6560
+ await store.saveEnrolmentChallenge(claims.nonce, challenge2);
6561
+ const existing = (await store.credentials()).filter((c) => c.approver === claims.approver);
6562
+ return { status: 200, body: {
6563
+ challenge: challenge2,
6564
+ rp: { id: rpId, name: o.rpName ?? rpId },
6565
+ user: { id: toBase64url2(utf82(claims.approver)), name: claims.approver, displayName: claims.approver },
6566
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
6567
+ authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
6568
+ attestation: "none",
6569
+ timeout: 12e4,
6570
+ excludeCredentials: existing.map((c) => ({ type: "public-key", id: c.credential_id })),
6571
+ approver: claims.approver,
6572
+ roles: claims.roles
6573
+ } };
6574
+ }
6575
+ const challenge = await store.enrolmentChallenge(claims.nonce);
6576
+ if (!challenge) return { status: 403, body: { error: "Start enrolment again: this link has been used or has no ceremony in progress." } };
6577
+ const cd = clientData(String(req.client_data_json ?? ""), "webauthn.create", challenge);
6578
+ if (cd) return { status: 400, body: { error: cd } };
6579
+ let att;
6580
+ try {
6581
+ att = readAttestation(fromBase64url2(String(req.attestation_object ?? "")));
6582
+ } catch (e) {
6583
+ return { status: 400, body: { error: String(e?.message ?? e) } };
6584
+ }
6585
+ if (!equal2(att.authData.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The passkey was created for another relying-party id." } };
6586
+ if (!(att.flags & 1) || !(att.flags & 4)) return { status: 400, body: { error: "The device did not verify the person (Face ID, Touch ID or PIN is required)." } };
6587
+ if (toBase64url2(att.credentialId) !== req.credential_id) return { status: 400, body: { error: "The credential id does not match the attestation." } };
6588
+ if (!await store.claimToken(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
6589
+ const kid = kidOf(att.publicKeyPem);
6590
+ const certificate = await o.registry.issue({
6591
+ parent: o.enroller,
6592
+ child: { kid, publicKeyPem: att.publicKeyPem },
6593
+ scope: `consent:${claims.roles.join(",")} approver:${claims.approver} suite:WebAuthn-ES256`,
6594
+ days: o.certificateDays ?? 365
6595
+ });
6596
+ const label = String(req.label ?? "").slice(0, 60) || "Passkey";
6597
+ await store.addCredential({ credential_id: req.credential_id, approver: claims.approver, roles: claims.roles, kid, public_key_pem: att.publicKeyPem, label, created_at: now().toISOString() });
6598
+ return { status: 200, body: { enrolled: true, approver: claims.approver, roles: claims.roles, kid, certificate } };
6599
+ }
6600
+ return null;
6601
+ }
6602
+ async function fetchHandler(request) {
6603
+ const out = await route(request.method, new URL(request.url).pathname, () => request.json()).catch((e) => ({ status: 400, body: { error: String(e?.message ?? e) } }));
6604
+ if (!out) return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });
6605
+ const html = out.type === "text/html";
6606
+ return new Response(html ? String(out.body) : JSON.stringify(out.body), { status: out.status, headers: { "content-type": html ? "text/html; charset=utf-8" : "application/json", ...html ? { "content-security-policy": CSP() } : {} } });
6607
+ }
6608
+ function node() {
6609
+ return async (req, res, next) => {
6610
+ const path = String(req.originalUrl ?? req.url ?? "").split("?")[0];
6611
+ const json = async () => {
6612
+ if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
6613
+ const chunks = [];
6614
+ for await (const c of req) chunks.push(c);
6615
+ return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
6616
+ };
6617
+ let out;
6618
+ try {
6619
+ out = await route(req.method, path, json);
6620
+ } catch (e) {
6621
+ out = { status: 400, body: { error: String(e?.message ?? e) } };
6622
+ }
6623
+ if (!out) {
6624
+ if (next) return next();
6625
+ res.statusCode = 404;
6626
+ return res.end(JSON.stringify({ error: "Not found" }));
6627
+ }
6628
+ res.statusCode = out.status;
6629
+ const html = out.type === "text/html";
6630
+ res.setHeader("content-type", html ? "text/html; charset=utf-8" : "application/json");
6631
+ if (html) res.setHeader("content-security-policy", CSP());
6632
+ res.end(html ? String(out.body) : JSON.stringify(out.body));
6633
+ };
6634
+ }
6635
+ return { store, fetch: fetchHandler, express: node, node };
6636
+ }
6637
+ var STYLE = `body{font:15px/1.55 system-ui,-apple-system,sans-serif;max-width:560px;margin:40px auto;padding:0 16px;color:#111;background:#fff}
6638
+ @media (prefers-color-scheme:dark){body{color:#eee;background:#111}table td{border-color:#333}}
6639
+ h2{margin:0 0 12px}table{border-collapse:collapse;margin:12px 0}td{padding:6px 10px;border-bottom:1px solid #ddd}
6640
+ button{font:inherit;padding:10px 18px;border-radius:8px;border:1px solid #888;background:transparent;color:inherit;cursor:pointer;margin:4px 8px 4px 0}
6641
+ button.primary{background:#2563eb;border-color:#2563eb;color:#fff}.muted{color:#777;font-size:13px}#msg{margin-top:16px;font-weight:600}`;
6642
+ var B642 = `const b64u={enc:b=>btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,''),dec:s=>Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')+'='.repeat((4-s.length%4)%4)),c=>c.charCodeAt(0))};
6643
+ const post=(u,b)=>fetch(u,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b)}).then(async r=>({ok:r.ok,body:await r.json().catch(()=>({}))}));
6644
+ const say=t=>{document.getElementById('msg').textContent=t};`;
6645
+ function approvalPage(p, status, devices) {
6646
+ const a = p.act.action ?? {};
6647
+ const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc(k)}</td><td><b>${esc(JSON.stringify(v))}</b></td></tr>`).join("");
6648
+ const device = (d) => `<button class="primary" data-cred="${esc(d.credential_id)}">Approve${d.approver ? ` as ${esc(d.approver)}` : ""} \xB7 ${esc(d.label)}</button>`;
6649
+ const choose = devices === null ? `<p><label>Your approver id <input id="who" autocomplete="username"></label> <button id="find">Continue</button></p><div id="devices"></div>` : devices.length ? `<div id="devices">${devices.map(device).join("")}</div>` : `<p class="muted">No passkey is enrolled for ${esc(p.role)} yet.</p>`;
6650
+ const body = status === "pending" ? `${choose}<button id="refuse">Refuse</button>` : `<p><b>${esc(status)}</b></p>`;
6651
+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Approve: ${esc(a.type)}</title><style>${STYLE}</style>
6652
+ <h2>Approve as ${esc(p.role)}?</h2>
6653
+ <p><b>${esc(a.type)}</b>${a.subject ? ` for <b>${esc(a.subject)}</b>` : ""}, at <b>${esc(p.act.relying_party)}</b>, under ${esc(p.act.authority?.artifact?.meta?.artifact_id)} v${esc(p.act.authority?.artifact?.meta?.version)}.</p>
6654
+ <table>${params}</table>
6655
+ <p class="muted">Your passkey signs this exact change and nothing else. It expires at ${esc(p.act.expires_at)}.</p>
6656
+ ${body}<div id="msg" data-id="${esc(p.id)}"></div>
6657
+ <script>${APPROVE_JS}</script>`;
6658
+ }
6659
+ var APPROVE_JS = `${B642}
6660
+ const id=document.getElementById('msg').dataset.id;
6661
+ async function approve(credential_id){
6662
+ try{
6663
+ const o=await post('/approve/'+id+'/options',{credential_id});
6664
+ if(!o.ok) return say(o.body.error||'Not allowed.');
6665
+ const a=await navigator.credentials.get({publicKey:{challenge:b64u.dec(o.body.challenge),rpId:o.body.rpId,allowCredentials:o.body.allowCredentials.map(c=>({type:c.type,id:b64u.dec(c.id)})),userVerification:'required',timeout:o.body.timeout}});
6666
+ const r=await post('/approve/'+id+'/assertion',{credential_id,authenticator_data:b64u.enc(a.response.authenticatorData),client_data_json:b64u.enc(a.response.clientDataJSON),signature:b64u.enc(a.response.signature)});
6667
+ say(r.ok?'Approved. You can close this page.':(r.body.error||'Not approved.'));
6668
+ if(r.ok) document.querySelectorAll('button').forEach(b=>b.disabled=true);
6669
+ }catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
6670
+ }
6671
+ async function refuse(){const r=await post('/approve/'+id+'/refuse',{});say(r.ok?'Refused.':(r.body.error||''));if(r.ok)document.querySelectorAll('button').forEach(b=>b.disabled=true)}
6672
+ function wire(root){root.querySelectorAll('[data-cred]').forEach(b=>b.addEventListener('click',()=>approve(b.dataset.cred)))}
6673
+ wire(document);
6674
+ document.getElementById('refuse')?.addEventListener('click',refuse);
6675
+ document.getElementById('find')?.addEventListener('click',async()=>{
6676
+ const r=await post('/approve/'+id+'/devices',{approver:document.getElementById('who').value});
6677
+ const box=document.getElementById('devices');box.textContent='';
6678
+ if(!r.body.devices?.length) return say('No passkey of yours approves this.');
6679
+ for(const d of r.body.devices){const b=document.createElement('button');b.className='primary';b.dataset.cred=d.credential_id;b.textContent='Approve \xB7 '+d.label;box.appendChild(b)}
6680
+ wire(box);
6681
+ });`;
6682
+ function enrolPage(rpName) {
6683
+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Enrol a passkey</title><style>${STYLE}</style>
6684
+ <h2>Enrol a passkey for ${esc(rpName)}</h2>
6685
+ <p id="who" class="muted">Checking your enrolment link\u2026</p>
6686
+ <p><label>Name this device <input id="label" value="" placeholder="e.g. iPhone"></label></p>
6687
+ <button class="primary" id="go" disabled>Create passkey</button>
6688
+ <div id="msg"></div>
6689
+ <p class="muted">The passkey stays on your device. It can approve requests in your role and nothing else, and this link works once.</p>
6690
+ <script>${ENROL_JS}</script>`;
6691
+ }
6692
+ var ENROL_JS = `${B642}
6693
+ const token=location.hash.slice(1);let opts=null;
6694
+ (async()=>{const r=await post('/nomos/enrol/begin',{token});if(!r.ok){document.getElementById('who').textContent=r.body.error||'This link is not valid.';return}
6695
+ opts=r.body;document.getElementById('who').textContent='Enrolling '+opts.approver+' to approve as '+opts.roles.join(', ')+'.';document.getElementById('go').disabled=false})();
6696
+ async function enrol(){
6697
+ try{
6698
+ const c=await navigator.credentials.create({publicKey:{challenge:b64u.dec(opts.challenge),rp:opts.rp,user:{id:b64u.dec(opts.user.id),name:opts.user.name,displayName:opts.user.displayName},pubKeyCredParams:opts.pubKeyCredParams,authenticatorSelection:opts.authenticatorSelection,attestation:opts.attestation,timeout:opts.timeout,excludeCredentials:opts.excludeCredentials.map(x=>({type:x.type,id:b64u.dec(x.id)}))}});
6699
+ const r=await post('/nomos/enrol/finish',{token,label:document.getElementById('label').value,credential_id:b64u.enc(c.rawId),client_data_json:b64u.enc(c.response.clientDataJSON),attestation_object:b64u.enc(c.response.attestationObject)});
6700
+ say(r.ok?'Enrolled. This device can now approve as '+r.body.roles.join(', ')+'.':(r.body.error||'Not enrolled.'));
6701
+ if(r.ok) document.getElementById('go').disabled=true;
6702
+ }catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
6703
+ }
6704
+ document.getElementById('go').addEventListener('click',enrol);`;
6705
+ var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js))))}'`;
6706
+ 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'`;
6707
+
5765
6708
  // src/act/carrier.ts
5766
6709
  var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
5767
6710
  var NomosActError = class extends Error {
@@ -5798,6 +6741,7 @@ async function act(o) {
5798
6741
  }
5799
6742
  }
5800
6743
  const testimony = [];
6744
+ const available = [...issuer2.key_certs ?? []];
5801
6745
  const unavailable = [];
5802
6746
  for (const claim of witnessed) {
5803
6747
  const w = witnesses.find((x) => x.claims.includes(claim));
@@ -5821,8 +6765,10 @@ async function act(o) {
5821
6765
  continue;
5822
6766
  }
5823
6767
  testimony.push(t);
6768
+ if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
5824
6769
  }
5825
- const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: issuer2.key_certs, testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
6770
+ const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
6771
+ present(theAct, available);
5826
6772
  let { status, body } = await submit(f, rp.act_endpoint, theAct);
5827
6773
  let approvalUrl;
5828
6774
  while (status === 202 && body.decision === "ESCALATED") {
@@ -5830,16 +6776,18 @@ async function act(o) {
5830
6776
  const endpoint = (issuer2.approvals ?? []).find((a) => a.role === role)?.endpoint;
5831
6777
  if (!endpoint) return done("escalated", body, theAct, { summary: `Needs approval from ${role}, and ${issuer2.issuer_id} publishes no approval endpoint for that role.` });
5832
6778
  const deadline = Math.min(Date.parse(theAct.expires_at), Date.now() + (o.approvalTimeoutMs ?? Infinity));
5833
- const consent = await requestConsent(f, endpoint, role, theAct, deadline, o.pollMs ?? 1e3, (url) => {
6779
+ const answer2 = await requestConsent(f, endpoint, role, theAct, deadline, o.pollMs ?? 1e3, (url) => {
5834
6780
  approvalUrl = url;
5835
6781
  o.onApproval?.({ role, url, actDigest: body.act_digest });
5836
6782
  });
5837
- if (consent === "refused") return done("denied", body, theAct, { summary: `The ${role} refused this action.` });
5838
- if (!consent) {
6783
+ if (answer2 === "refused") return done("denied", body, theAct, { summary: `The ${role} refused this action.` });
6784
+ if (!answer2) {
5839
6785
  const expired = Date.now() >= Date.parse(theAct.expires_at);
5840
6786
  return done("escalated", body, theAct, { approvalUrl, summary: expired ? `Not approved by ${role} before the Act expired at ${theAct.expires_at}; build a new Act to ask again.` : `Waiting for approval from ${role}${approvalUrl ? ` at ${approvalUrl}` : ""}. Not approved yet; this Act stays valid until ${theAct.expires_at}.` });
5841
6787
  }
5842
- theAct.consents.push(consent);
6788
+ theAct.consents.push(answer2.consent);
6789
+ if (Array.isArray(answer2.keyCerts)) available.push(...answer2.keyCerts);
6790
+ present(theAct, available);
5843
6791
  const before = Number(body.consents_valid ?? 0);
5844
6792
  ({ status, body } = await submit(f, rp.act_endpoint, theAct));
5845
6793
  if (status === 202 && Number(body.consents_valid ?? 0) <= before) {
@@ -5860,6 +6808,13 @@ async function act(o) {
5860
6808
  summary: kind === "committed" ? `Committed by ${rp.relying_party_id}${body.by_consent ? ", with approval" : ""}.` : kind === "denied" ? `Denied by rule ${body.rule ?? "(default)"}.` : kind === "replayed" ? "Already committed; this is the original receipt." : `Refused: ${body.decision}${body.reason_code ? ` (${body.reason_code})` : ""}.${body.hint ? " " + body.hint : ""}`
5861
6809
  });
5862
6810
  }
6811
+ function present(theAct, available) {
6812
+ const set = presentedCertificates(available, theAct);
6813
+ if (set.length > MAX_PRESENTED) {
6814
+ throw new NomosActError(`This Act relies on ${set.length} certificates; at most ${MAX_PRESENTED} can be presented (NOMOS-SPEC-008 \xA76 step 1). The issuer's hierarchy is too deep for one Act.`);
6815
+ }
6816
+ theAct.authority.key_certs = set;
6817
+ }
5863
6818
  function done(status, verdict, theAct, extra) {
5864
6819
  return { status, verdict, act: theAct, ...extra };
5865
6820
  }
@@ -5877,13 +6832,13 @@ async function submit(f, endpoint, theAct) {
5877
6832
  async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPending) {
5878
6833
  const res = await f(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ role, act: { ...theAct, consents: [] } }) });
5879
6834
  const body = await res.json().catch(() => ({}));
5880
- if (res.status === 200 && body.status === "approved") return body.consent;
6835
+ if (res.status === 200 && body.status === "approved") return { consent: body.consent, keyCerts: body.key_certs };
5881
6836
  if (res.status !== 202 || !body.poll_url) return null;
5882
6837
  onPending(body.approval_url);
5883
6838
  while (Date.now() < deadline) {
5884
6839
  await new Promise((r) => setTimeout(r, pollMs));
5885
6840
  const p = await (await f(body.poll_url)).json().catch(() => ({}));
5886
- if (p.status === "approved") return p.consent;
6841
+ if (p.status === "approved") return { consent: p.consent, keyCerts: p.key_certs };
5887
6842
  if (p.status === "refused") return "refused";
5888
6843
  if (p.status === "expired") return null;
5889
6844
  }
@@ -5891,7 +6846,6 @@ async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPen
5891
6846
  }
5892
6847
 
5893
6848
  // src/act/roles.ts
5894
- var import_node_crypto3 = require("crypto");
5895
6849
  function serve(route) {
5896
6850
  async function fetchHandler(request) {
5897
6851
  const out = await route({ method: request.method, path: new URL(request.url).pathname, json: () => request.json() });
@@ -5906,7 +6860,7 @@ function serve(route) {
5906
6860
  if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
5907
6861
  const chunks = [];
5908
6862
  for await (const c of req) chunks.push(c);
5909
- return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
6863
+ return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
5910
6864
  };
5911
6865
  let out;
5912
6866
  try {
@@ -5930,7 +6884,9 @@ var day = 864e5;
5930
6884
  function issuer(o) {
5931
6885
  const byId = new Map(o.authorities.map((a) => [a.meta.artifact_id, a]));
5932
6886
  const base = o.origin.replace(/\/$/, "");
5933
- const document = () => ({
6887
+ const pool = async () => typeof o.keyCerts === "function" ? await o.keyCerts() : o.keyCerts;
6888
+ const revocationsUrl = o.revocations ? `${base}/nomos/revocations` : o.revocationsUrl;
6889
+ const build = (keyCerts) => ({
5934
6890
  nomos_document: "issuer",
5935
6891
  version: "1",
5936
6892
  issuer_id: o.issuerId,
@@ -5941,17 +6897,24 @@ function issuer(o) {
5941
6897
  artifact_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}.nomos`,
5942
6898
  in_force_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}/in-force`
5943
6899
  })),
5944
- key_certs: o.keyCerts,
6900
+ key_certs: keyCerts,
5945
6901
  ...o.witnesses ? { witnesses: o.witnesses } : {},
5946
6902
  ...o.approvals ? { approvals: o.approvals } : {},
5947
- ...o.revocationsUrl ? { revocations_url: o.revocationsUrl } : {},
6903
+ ...revocationsUrl ? { revocations_url: revocationsUrl } : {},
5948
6904
  valid_until: new Date(Date.now() + day).toISOString()
5949
6905
  });
6906
+ const document = () => {
6907
+ if (typeof o.keyCerts === "function") throw new Error("issuer(): keyCerts is a function, so the document must be read with `await resolveDocument()`");
6908
+ return build(o.keyCerts);
6909
+ };
6910
+ const resolveDocument = async () => build(await pool());
5950
6911
  return {
5951
6912
  document,
6913
+ resolveDocument,
5952
6914
  ...serve(async ({ method, path }) => {
5953
6915
  if (method !== "GET") return null;
5954
- if (path === WELL_KNOWN.issuer) return { status: 200, body: document() };
6916
+ if (path === WELL_KNOWN.issuer) return { status: 200, body: await resolveDocument() };
6917
+ if (path === "/nomos/revocations" && o.revocations) return { status: 200, body: await o.revocations() };
5955
6918
  const m = path.match(/^\/nomos\/authorities\/([^/]+?)(\.nomos|\/in-force)$/);
5956
6919
  const art = m && byId.get(decodeURIComponent(m[1]));
5957
6920
  if (!art) return null;
@@ -5986,13 +6949,14 @@ function witness(o) {
5986
6949
  if (value2 === void 0) return { status: 404, body: { error: `${o.witnessId} knows nothing about ${about}.` } };
5987
6950
  const max = o.maxValiditySeconds ?? 900;
5988
6951
  const seconds = Math.max(1, Math.min(max, Number(req?.valid_for_seconds) || max));
5989
- return { status: 200, body: { testimony: await signTestimony(o.signer, { claim, value: value2, about, validUntil: new Date(Date.now() + seconds * 1e3) }) } };
6952
+ return { status: 200, body: { testimony: await signTestimony(o.signer, { claim, value: value2, about, validUntil: new Date(Date.now() + seconds * 1e3) }), ...o.keyCerts?.length ? { key_certs: o.keyCerts } : {} } };
5990
6953
  })
5991
6954
  };
5992
6955
  }
5993
6956
  function approver(o) {
5994
6957
  const base = o.origin.replace(/\/$/, "");
5995
6958
  const pending = /* @__PURE__ */ new Map();
6959
+ const chain = o.keyCerts?.length ? { key_certs: o.keyCerts } : {};
5996
6960
  const statusOf = (p) => p.status === "pending" && Date.parse(p.act.expires_at) < Date.now() ? "expired" : p.status;
5997
6961
  return {
5998
6962
  pending,
@@ -6002,9 +6966,9 @@ function approver(o) {
6002
6966
  if (!o.roles.includes(role)) return { status: 404, body: { error: `This approver does not consent as ${role}.` } };
6003
6967
  if (o.decide === "auto" || typeof o.decide === "function") {
6004
6968
  const ok = o.decide === "auto" ? true : await o.decide({ role, act: act2 });
6005
- return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role) } } : { status: 200, body: { status: "refused" } };
6969
+ return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role), ...chain } } : { status: 200, body: { status: "refused" } };
6006
6970
  }
6007
- const id = (0, import_node_crypto3.randomUUID)();
6971
+ const id = randomUUID();
6008
6972
  pending.set(id, { role, act: act2, status: "pending" });
6009
6973
  return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
6010
6974
  }
@@ -6013,7 +6977,7 @@ function approver(o) {
6013
6977
  const p = pending.get(m[1]);
6014
6978
  if (!p) return null;
6015
6979
  const s = statusOf(p);
6016
- return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent } : {} } };
6980
+ return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...chain } : {} } };
6017
6981
  }
6018
6982
  m = path.match(/^\/approve\/([0-9a-f-]+)(\/(approve|refuse))?$/);
6019
6983
  if (m) {
@@ -6033,23 +6997,24 @@ function approver(o) {
6033
6997
  })
6034
6998
  };
6035
6999
  }
6036
- var esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
7000
+ var esc2 = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6037
7001
  function page(p, status, id) {
6038
7002
  const a = p.act.action ?? {};
6039
- const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc(k)}</td><td><b>${esc(JSON.stringify(v))}</b></td></tr>`).join("");
7003
+ const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc2(k)}</td><td><b>${esc2(JSON.stringify(v))}</b></td></tr>`).join("");
6040
7004
  const buttons = status === "pending" ? `<form method="post" action="/approve/${id}/approve" style="display:inline"><button style="padding:10px 18px">Approve</button></form>
6041
- <form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${esc(status)}</b></p>`;
6042
- return `<!doctype html><meta charset="utf-8"><title>Approve: ${esc(a.type)}</title>
7005
+ <form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${esc2(status)}</b></p>`;
7006
+ return `<!doctype html><meta charset="utf-8"><title>Approve: ${esc2(a.type)}</title>
6043
7007
  <body style="font:15px system-ui;max-width:560px;margin:40px auto;padding:0 16px">
6044
- <h2>Approve as ${esc(p.role)}?</h2>
6045
- <p><b>${esc(a.type)}</b>${a.subject ? ` for <b>${esc(a.subject)}</b>` : ""}, at <b>${esc(p.act.relying_party)}</b>, under ${esc(p.act.authority?.artifact?.meta?.artifact_id)} v${esc(p.act.authority?.artifact?.meta?.version)}.</p>
7008
+ <h2>Approve as ${esc2(p.role)}?</h2>
7009
+ <p><b>${esc2(a.type)}</b>${a.subject ? ` for <b>${esc2(a.subject)}</b>` : ""}, at <b>${esc2(p.act.relying_party)}</b>, under ${esc2(p.act.authority?.artifact?.meta?.artifact_id)} v${esc2(p.act.authority?.artifact?.meta?.version)}.</p>
6046
7010
  <table style="border-collapse:collapse" cellpadding="4">${params}</table>
6047
- <p style="color:#666">Your approval signs this exact change (digest ${esc(bindingDigest(p.act).slice(0, 16))}\u2026) and nothing else. It expires at ${esc(p.act.expires_at)}.</p>
7011
+ <p style="color:#666">Your approval signs this exact change (digest ${esc2(bindingDigest(p.act).slice(0, 16))}\u2026) and nothing else. It expires at ${esc2(p.act.expires_at)}.</p>
6048
7012
  ${buttons}</body>`;
6049
7013
  }
6050
7014
  // Annotate the CommonJS export names for ESM import in node:
6051
7015
  0 && (module.exports = {
6052
7016
  ACT_MEDIA_TYPE,
7017
+ MAX_PRESENTED,
6053
7018
  Nomos,
6054
7019
  NomosAPIError,
6055
7020
  NomosActError,
@@ -6064,6 +7029,8 @@ ${buttons}</body>`;
6064
7029
  NomosRateLimitError,
6065
7030
  NomosValidationError,
6066
7031
  NomosWebhookSignatureError,
7032
+ RegistryError,
7033
+ RevocationUnavailable,
6067
7034
  WELL_KNOWN,
6068
7035
  accept,
6069
7036
  act,
@@ -6071,24 +7038,40 @@ ${buttons}</body>`;
6071
7038
  bindingDigest,
6072
7039
  buildAct,
6073
7040
  can,
7041
+ certificateRegistry,
6074
7042
  certify,
6075
7043
  fetchDocument,
6076
7044
  generateSigner,
6077
7045
  issuer,
6078
7046
  kidOf,
6079
7047
  localSigner,
7048
+ memoryPasskeyStore,
7049
+ memoryRegistryStore,
6080
7050
  memoryStore,
6081
7051
  nomos,
6082
7052
  originOf,
7053
+ passkeyApprover,
7054
+ postgresPasskeyStore,
7055
+ postgresRegistryStore,
7056
+ postgresStore,
7057
+ presentedCertificates,
7058
+ readAttestation,
7059
+ reliedKids,
7060
+ revocationListCertificates,
7061
+ revocationSource,
6083
7062
  seal,
6084
7063
  seededSigner,
6085
7064
  signConsent,
7065
+ signEnrolmentToken,
6086
7066
  signInForce,
7067
+ signRevocation,
7068
+ signRevocationList,
6087
7069
  signTestimony,
6088
7070
  statusFor,
6089
7071
  verifyAct,
6090
7072
  verifyArtifact,
6091
7073
  verifyChain,
7074
+ verifyEnrolmentToken,
6092
7075
  verifySeal,
6093
7076
  witness
6094
7077
  });