@nomosprotocol/sdk 2.0.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/dist/index.cjs +1234 -113
- package/dist/index.d.cts +431 -11
- package/dist/index.d.ts +431 -11
- package/dist/index.js +1210 -112
- package/package.json +4 -2
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,31 +36,53 @@ __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,
|
|
44
|
+
applyToolBindings: () => applyToolBindings,
|
|
41
45
|
approver: () => approver,
|
|
42
46
|
bindingDigest: () => bindingDigest,
|
|
43
47
|
buildAct: () => buildAct,
|
|
44
48
|
can: () => can,
|
|
49
|
+
certificateRegistry: () => certificateRegistry,
|
|
45
50
|
certify: () => certify,
|
|
51
|
+
describeSuggestion: () => describeSuggestion,
|
|
46
52
|
fetchDocument: () => fetchDocument,
|
|
47
53
|
generateSigner: () => generateSigner,
|
|
48
54
|
issuer: () => issuer,
|
|
49
55
|
kidOf: () => kidOf2,
|
|
50
56
|
localSigner: () => localSigner,
|
|
57
|
+
memoryPasskeyStore: () => memoryPasskeyStore,
|
|
58
|
+
memoryRegistryStore: () => memoryRegistryStore,
|
|
51
59
|
memoryStore: () => memoryStore,
|
|
52
60
|
nomos: () => nomos,
|
|
53
61
|
originOf: () => originOf,
|
|
62
|
+
parseToolBindings: () => parseToolBindings,
|
|
63
|
+
passkeyApprover: () => passkeyApprover,
|
|
64
|
+
postgresPasskeyStore: () => postgresPasskeyStore,
|
|
65
|
+
postgresRegistryStore: () => postgresRegistryStore,
|
|
66
|
+
postgresStore: () => postgresStore,
|
|
67
|
+
presentedCertificates: () => presentedCertificates,
|
|
68
|
+
readAttestation: () => readAttestation,
|
|
69
|
+
reliedKids: () => reliedKids,
|
|
70
|
+
revocationListCertificates: () => revocationListCertificates,
|
|
71
|
+
revocationSource: () => revocationSource,
|
|
54
72
|
seal: () => seal,
|
|
55
73
|
seededSigner: () => seededSigner,
|
|
56
74
|
signConsent: () => signConsent,
|
|
75
|
+
signEnrolmentToken: () => signEnrolmentToken,
|
|
57
76
|
signInForce: () => signInForce,
|
|
77
|
+
signRevocation: () => signRevocation,
|
|
78
|
+
signRevocationList: () => signRevocationList,
|
|
58
79
|
signTestimony: () => signTestimony,
|
|
59
80
|
statusFor: () => statusFor,
|
|
81
|
+
suggestToolBindings: () => suggestToolBindings,
|
|
60
82
|
verifyAct: () => verifyAct,
|
|
61
83
|
verifyArtifact: () => verifyArtifact,
|
|
62
84
|
verifyChain: () => verifyChain,
|
|
85
|
+
verifyEnrolmentToken: () => verifyEnrolmentToken,
|
|
63
86
|
verifySeal: () => verifySeal,
|
|
64
87
|
witness: () => witness
|
|
65
88
|
});
|
|
@@ -168,7 +191,7 @@ function isRetryable(status) {
|
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
// src/http-client.ts
|
|
171
|
-
var SDK_VERSION = "
|
|
194
|
+
var SDK_VERSION = "2.3.1";
|
|
172
195
|
var HttpClient = class {
|
|
173
196
|
constructor(opts) {
|
|
174
197
|
this.opts = opts;
|
|
@@ -833,7 +856,7 @@ async function canAuthenticated(http, params, opts) {
|
|
|
833
856
|
var nomos = { can };
|
|
834
857
|
|
|
835
858
|
// src/nomos.ts
|
|
836
|
-
var DEFAULT_BASE_URL = "https://
|
|
859
|
+
var DEFAULT_BASE_URL = "https://www.nomosprotocol.com";
|
|
837
860
|
var DEFAULT_MAX_RETRIES = 3;
|
|
838
861
|
var DEFAULT_TIMEOUT = 3e4;
|
|
839
862
|
var Nomos = class {
|
|
@@ -843,7 +866,7 @@ var Nomos = class {
|
|
|
843
866
|
constructor(apiKey, config = {}) {
|
|
844
867
|
if (!apiKey || typeof apiKey !== "string") {
|
|
845
868
|
throw new NomosAuthenticationError(
|
|
846
|
-
'A valid API key is required. Pass it as the first argument: new Nomos("
|
|
869
|
+
'A valid API key is required. Pass it as the first argument: new Nomos("nmk_...")'
|
|
847
870
|
);
|
|
848
871
|
}
|
|
849
872
|
this.mode = apiKey.startsWith("nms_test_") ? "test" : "live";
|
|
@@ -865,7 +888,7 @@ var Nomos = class {
|
|
|
865
888
|
* return shape either way, so calling code never needs to know which rail answered.
|
|
866
889
|
*
|
|
867
890
|
* @example
|
|
868
|
-
* const client = new Nomos('
|
|
891
|
+
* const client = new Nomos('nmk_...');
|
|
869
892
|
* const verdict = await client.can({ artifact_id: 'loan_approval_v1', credit_score: 720 });
|
|
870
893
|
*/
|
|
871
894
|
async can(params, opts) {
|
|
@@ -873,7 +896,7 @@ var Nomos = class {
|
|
|
873
896
|
}
|
|
874
897
|
};
|
|
875
898
|
|
|
876
|
-
//
|
|
899
|
+
// node_modules/@noble/hashes/_u64.js
|
|
877
900
|
var U32_MASK64 = /* @__PURE__ */ (() => BigInt(2 ** 32 - 1))();
|
|
878
901
|
var _32n = /* @__PURE__ */ BigInt(32);
|
|
879
902
|
function fromBig(n, le = false) {
|
|
@@ -916,7 +939,7 @@ var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0
|
|
|
916
939
|
var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
|
917
940
|
var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
918
941
|
|
|
919
|
-
//
|
|
942
|
+
// node_modules/@noble/hashes/utils.js
|
|
920
943
|
function isBytes(a) {
|
|
921
944
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
922
945
|
}
|
|
@@ -1081,7 +1104,7 @@ var oidNist = (suffix) => ({
|
|
|
1081
1104
|
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
1082
1105
|
});
|
|
1083
1106
|
|
|
1084
|
-
//
|
|
1107
|
+
// node_modules/@noble/hashes/_md.js
|
|
1085
1108
|
function Chi(a, b, c) {
|
|
1086
1109
|
return a & b ^ ~a & c;
|
|
1087
1110
|
}
|
|
@@ -1212,7 +1235,7 @@ var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
|
1212
1235
|
327033209
|
|
1213
1236
|
]);
|
|
1214
1237
|
|
|
1215
|
-
//
|
|
1238
|
+
// node_modules/@noble/hashes/sha2.js
|
|
1216
1239
|
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
1217
1240
|
1116352408,
|
|
1218
1241
|
1899447441,
|
|
@@ -1609,7 +1632,7 @@ var sha512 = /* @__PURE__ */ createHasher(
|
|
|
1609
1632
|
/* @__PURE__ */ oidNist(3)
|
|
1610
1633
|
);
|
|
1611
1634
|
|
|
1612
|
-
//
|
|
1635
|
+
// node_modules/@nomosprotocol/verify/dist/bytes.js
|
|
1613
1636
|
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1614
1637
|
var REV = new Map([...B64].map((c, i) => [c, i]));
|
|
1615
1638
|
var utf8 = (s) => new TextEncoder().encode(s);
|
|
@@ -1671,7 +1694,7 @@ function fromBase64url(s) {
|
|
|
1671
1694
|
return raw && toBase64url(raw) === s ? raw : null;
|
|
1672
1695
|
}
|
|
1673
1696
|
|
|
1674
|
-
//
|
|
1697
|
+
// node_modules/@nomosprotocol/verify/dist/jcs.js
|
|
1675
1698
|
function value(v) {
|
|
1676
1699
|
if (v === null)
|
|
1677
1700
|
return "null";
|
|
@@ -1705,7 +1728,7 @@ function codePointCompare(a, b) {
|
|
|
1705
1728
|
return A.length - B.length;
|
|
1706
1729
|
}
|
|
1707
1730
|
|
|
1708
|
-
//
|
|
1731
|
+
// node_modules/@noble/curves/utils.js
|
|
1709
1732
|
function aarray(item, title, inner = () => {
|
|
1710
1733
|
}) {
|
|
1711
1734
|
if (!Array.isArray(item))
|
|
@@ -1887,7 +1910,7 @@ function validateObject(object, fields = {}, optFields = {}, title = "object") {
|
|
|
1887
1910
|
iter(optFields, true);
|
|
1888
1911
|
}
|
|
1889
1912
|
|
|
1890
|
-
//
|
|
1913
|
+
// node_modules/@noble/curves/abstract/modular.js
|
|
1891
1914
|
var _0n2 = /* @__PURE__ */ BigInt(0);
|
|
1892
1915
|
var _1n2 = /* @__PURE__ */ BigInt(1);
|
|
1893
1916
|
var _2n = /* @__PURE__ */ BigInt(2);
|
|
@@ -2350,7 +2373,7 @@ function mapHashToField(key, fieldOrder, isLE = false) {
|
|
|
2350
2373
|
return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
|
|
2351
2374
|
}
|
|
2352
2375
|
|
|
2353
|
-
//
|
|
2376
|
+
// node_modules/@noble/curves/abstract/curve.js
|
|
2354
2377
|
var _0n3 = /* @__PURE__ */ BigInt(0);
|
|
2355
2378
|
var _1n3 = /* @__PURE__ */ BigInt(1);
|
|
2356
2379
|
var _4n2 = /* @__PURE__ */ BigInt(4);
|
|
@@ -2737,7 +2760,7 @@ function createKeygen(randomSecretKey, getPublicKey) {
|
|
|
2737
2760
|
};
|
|
2738
2761
|
}
|
|
2739
2762
|
|
|
2740
|
-
//
|
|
2763
|
+
// node_modules/@noble/curves/abstract/edwards.js
|
|
2741
2764
|
var _0n4 = /* @__PURE__ */ BigInt(0);
|
|
2742
2765
|
var _1n4 = /* @__PURE__ */ BigInt(1);
|
|
2743
2766
|
var _2n2 = /* @__PURE__ */ BigInt(2);
|
|
@@ -3188,7 +3211,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3188
3211
|
});
|
|
3189
3212
|
}
|
|
3190
3213
|
|
|
3191
|
-
//
|
|
3214
|
+
// node_modules/@noble/curves/ed25519.js
|
|
3192
3215
|
var _1n5 = /* @__PURE__ */ BigInt(1);
|
|
3193
3216
|
var _2n3 = /* @__PURE__ */ BigInt(2);
|
|
3194
3217
|
var _5n2 = /* @__PURE__ */ BigInt(5);
|
|
@@ -3263,7 +3286,7 @@ function ed(opts) {
|
|
|
3263
3286
|
}
|
|
3264
3287
|
var ed25519 = /* @__PURE__ */ ed({});
|
|
3265
3288
|
|
|
3266
|
-
//
|
|
3289
|
+
// node_modules/@noble/hashes/hmac.js
|
|
3267
3290
|
var _HMAC = class {
|
|
3268
3291
|
oHash;
|
|
3269
3292
|
iHash;
|
|
@@ -3340,7 +3363,7 @@ var hmac = /* @__PURE__ */ (() => {
|
|
|
3340
3363
|
return hmac_;
|
|
3341
3364
|
})();
|
|
3342
3365
|
|
|
3343
|
-
//
|
|
3366
|
+
// node_modules/@noble/curves/abstract/der.js
|
|
3344
3367
|
var _0n5 = /* @__PURE__ */ BigInt(0);
|
|
3345
3368
|
var DERErr = class extends Error {
|
|
3346
3369
|
constructor(m = "") {
|
|
@@ -3434,18 +3457,18 @@ var _DER = {
|
|
|
3434
3457
|
}
|
|
3435
3458
|
},
|
|
3436
3459
|
toSig(bytes, maxScalarBytes) {
|
|
3437
|
-
const { Err: E, _int: int, _tlv:
|
|
3460
|
+
const { Err: E, _int: int, _tlv: tlv2 } = _DER;
|
|
3438
3461
|
if (maxScalarBytes !== void 0) {
|
|
3439
3462
|
asafenumber(maxScalarBytes, "maxScalarBytes");
|
|
3440
3463
|
if (maxScalarBytes < 1)
|
|
3441
3464
|
throw new E("invalid signature: maxScalarBytes must be positive");
|
|
3442
3465
|
}
|
|
3443
3466
|
const data = abytes2(bytes, void 0, "signature");
|
|
3444
|
-
const { v: seqBytes, l: seqLeftBytes } =
|
|
3467
|
+
const { v: seqBytes, l: seqLeftBytes } = tlv2.decode(48, data);
|
|
3445
3468
|
if (seqLeftBytes.length)
|
|
3446
3469
|
throw new E("invalid signature: left bytes after parsing");
|
|
3447
|
-
const { v: rBytes, l: rLeftBytes } =
|
|
3448
|
-
const { v: sBytes, l: sLeftBytes } =
|
|
3470
|
+
const { v: rBytes, l: rLeftBytes } = tlv2.decode(2, seqBytes);
|
|
3471
|
+
const { v: sBytes, l: sLeftBytes } = tlv2.decode(2, rLeftBytes);
|
|
3449
3472
|
if (sLeftBytes.length)
|
|
3450
3473
|
throw new E("invalid signature: left bytes after parsing");
|
|
3451
3474
|
if (maxScalarBytes !== void 0 && (rBytes.length > maxScalarBytes || sBytes.length > maxScalarBytes))
|
|
@@ -3453,12 +3476,12 @@ var _DER = {
|
|
|
3453
3476
|
return { r: int.decode(rBytes), s: int.decode(sBytes) };
|
|
3454
3477
|
},
|
|
3455
3478
|
hexFromSig(sig) {
|
|
3456
|
-
const { _tlv:
|
|
3479
|
+
const { _tlv: tlv2, _int: int } = _DER;
|
|
3457
3480
|
validateObject(sig, { r: "bigint", s: "bigint" }, {}, "sig");
|
|
3458
|
-
const rs =
|
|
3459
|
-
const ss =
|
|
3481
|
+
const rs = tlv2.encode(2, int.encode(sig.r));
|
|
3482
|
+
const ss = tlv2.encode(2, int.encode(sig.s));
|
|
3460
3483
|
const seq = rs + ss;
|
|
3461
|
-
return
|
|
3484
|
+
return tlv2.encode(48, seq);
|
|
3462
3485
|
}
|
|
3463
3486
|
};
|
|
3464
3487
|
var DER = /* @__PURE__ */ (() => {
|
|
@@ -3467,7 +3490,7 @@ var DER = /* @__PURE__ */ (() => {
|
|
|
3467
3490
|
return Object.freeze(_DER);
|
|
3468
3491
|
})();
|
|
3469
3492
|
|
|
3470
|
-
//
|
|
3493
|
+
// node_modules/@noble/curves/abstract/weierstrass.js
|
|
3471
3494
|
var divNearest = (num, den) => (num + (num >= 0 ? den : -den) / _2n4) / den;
|
|
3472
3495
|
function _splitEndoScalar(k, basis, n) {
|
|
3473
3496
|
aInRange("scalar", k, _0n6, n);
|
|
@@ -4299,7 +4322,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
4299
4322
|
});
|
|
4300
4323
|
}
|
|
4301
4324
|
|
|
4302
|
-
//
|
|
4325
|
+
// node_modules/@noble/curves/nist.js
|
|
4303
4326
|
var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
4304
4327
|
p: BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),
|
|
4305
4328
|
n: BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),
|
|
@@ -4312,21 +4335,21 @@ var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
|
4312
4335
|
var p256_Point = /* @__PURE__ */ weierstrass(p256_CURVE);
|
|
4313
4336
|
var p256 = /* @__PURE__ */ ecdsa(p256_Point, sha256);
|
|
4314
4337
|
|
|
4315
|
-
//
|
|
4338
|
+
// node_modules/@nomosprotocol/verify/dist/suites.js
|
|
4316
4339
|
var SUITES = ["Ed25519", "ES256", "WebAuthn-ES256"];
|
|
4317
4340
|
var N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
4318
4341
|
var ED25519_PREFIX = Uint8Array.from([48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]);
|
|
4319
4342
|
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
4343
|
var SUITES_FOR = { "Ed25519": ["Ed25519"], "P-256": ["ES256", "WebAuthn-ES256"] };
|
|
4321
4344
|
var isSuite = (s) => typeof s === "string" && SUITES.includes(s);
|
|
4322
|
-
function pemDer(
|
|
4323
|
-
if (typeof
|
|
4345
|
+
function pemDer(pem2) {
|
|
4346
|
+
if (typeof pem2 !== "string")
|
|
4324
4347
|
return null;
|
|
4325
|
-
const m =
|
|
4348
|
+
const m = pem2.match(/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=\s]+?)\s*-----END PUBLIC KEY-----\s*$/);
|
|
4326
4349
|
return m ? fromBase64(m[1].replace(/\s+/g, "")) : null;
|
|
4327
4350
|
}
|
|
4328
|
-
function loadKey(
|
|
4329
|
-
const der = pemDer(
|
|
4351
|
+
function loadKey(pem2) {
|
|
4352
|
+
const der = pemDer(pem2);
|
|
4330
4353
|
if (!der)
|
|
4331
4354
|
return { reason: "invalid_key" };
|
|
4332
4355
|
if (der.length === 44 && equal(der.subarray(0, 12), ED25519_PREFIX)) {
|
|
@@ -4350,16 +4373,16 @@ function loadKey(pem) {
|
|
|
4350
4373
|
}
|
|
4351
4374
|
return { reason: "invalid_key" };
|
|
4352
4375
|
}
|
|
4353
|
-
function kidOf(
|
|
4354
|
-
const k = loadKey(
|
|
4376
|
+
function kidOf(pem2) {
|
|
4377
|
+
const k = loadKey(pem2);
|
|
4355
4378
|
return "reason" in k ? null : toBase64url(sha256(k.der)).slice(0, 16);
|
|
4356
4379
|
}
|
|
4357
4380
|
var toBigInt = (b) => b.reduce((n, x) => n << 8n | BigInt(x), 0n);
|
|
4358
|
-
function verifySuite(suite,
|
|
4381
|
+
function verifySuite(suite, pem2, payload, proof) {
|
|
4359
4382
|
const fail2 = (reason) => ({ ok: false, reason });
|
|
4360
4383
|
if (!isSuite(suite))
|
|
4361
4384
|
return fail2("unknown_suite");
|
|
4362
|
-
const k = loadKey(
|
|
4385
|
+
const k = loadKey(pem2);
|
|
4363
4386
|
if ("reason" in k)
|
|
4364
4387
|
return fail2("invalid_key");
|
|
4365
4388
|
if (!SUITES_FOR[k.type].includes(suite))
|
|
@@ -4421,7 +4444,7 @@ function verifySuite(suite, pem, payload, proof) {
|
|
|
4421
4444
|
return ok ? { ok: true } : fail2("bad_signature");
|
|
4422
4445
|
}
|
|
4423
4446
|
|
|
4424
|
-
//
|
|
4447
|
+
// node_modules/@nomosprotocol/verify/dist/scope.js
|
|
4425
4448
|
var ARTIFACT_DIMENSIONS = ["artifact", "industry", "jurisdiction"];
|
|
4426
4449
|
var SET_DIMENSIONS = ["claim", "consent", "suite"];
|
|
4427
4450
|
var UNRESTRICTED = { terms: {} };
|
|
@@ -4540,7 +4563,7 @@ function formatScope(scope) {
|
|
|
4540
4563
|
return Object.entries(scope.terms).map(([d, v]) => `${d}:${v}`).join(" ");
|
|
4541
4564
|
}
|
|
4542
4565
|
|
|
4543
|
-
//
|
|
4566
|
+
// node_modules/@nomosprotocol/verify/dist/chain.js
|
|
4544
4567
|
var MAX_CHAIN = 20;
|
|
4545
4568
|
var CERT_SUITES = ["Ed25519", "ES256"];
|
|
4546
4569
|
var SEAL_SUITES = ["Ed25519", "ES256"];
|
|
@@ -4632,7 +4655,7 @@ var Memo = class {
|
|
|
4632
4655
|
}
|
|
4633
4656
|
};
|
|
4634
4657
|
function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo) {
|
|
4635
|
-
let
|
|
4658
|
+
let pem2 = rootPem, scope = UNRESTRICTED;
|
|
4636
4659
|
const hops = [];
|
|
4637
4660
|
for (const cert of path) {
|
|
4638
4661
|
const f = memo.of(cert);
|
|
@@ -4642,7 +4665,7 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4642
4665
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: "kid_mismatch" });
|
|
4643
4666
|
if (!suitePermitted(scope, cert.algorithm))
|
|
4644
4667
|
return fail("OUT_OF_SCOPE", { dimension: "suite", reason_code: "suite_not_permitted" });
|
|
4645
|
-
const sig = memo.sig(cert,
|
|
4668
|
+
const sig = memo.sig(cert, pem2);
|
|
4646
4669
|
if (!sig.ok)
|
|
4647
4670
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: sig.reason });
|
|
4648
4671
|
const issued = parseTime(cert.issued_at), expires = parseTime(cert.expires_at);
|
|
@@ -4662,14 +4685,14 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4662
4685
|
const narrowing = isNarrowerOrEqual(f.scope.scope, scope);
|
|
4663
4686
|
if (!narrowing.ok)
|
|
4664
4687
|
return fail("OUT_OF_SCOPE", { dimension: narrowing.dimension, reason_code: "widening" });
|
|
4665
|
-
hops.push({ cert, parentPem:
|
|
4666
|
-
|
|
4688
|
+
hops.push({ cert, parentPem: pem2, parentScope: scope });
|
|
4689
|
+
pem2 = cert.child_public_key_pem;
|
|
4667
4690
|
scope = mergeScopes(scope, f.scope.scope);
|
|
4668
4691
|
}
|
|
4669
4692
|
const admitted = purpose(scope);
|
|
4670
4693
|
if (!admitted.ok)
|
|
4671
4694
|
return fail("OUT_OF_SCOPE", { dimension: admitted.dimension });
|
|
4672
|
-
return { decision: "RESOLVED", pem, path: [], effective: scope, hops };
|
|
4695
|
+
return { decision: "RESOLVED", pem: pem2, path: [], effective: scope, hops };
|
|
4673
4696
|
}
|
|
4674
4697
|
function resolveKey(o) {
|
|
4675
4698
|
const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
|
|
@@ -4755,7 +4778,7 @@ function verifyArtifact(o) {
|
|
|
4755
4778
|
}
|
|
4756
4779
|
}
|
|
4757
4780
|
|
|
4758
|
-
//
|
|
4781
|
+
// node_modules/@nomosprotocol/verify/dist/evaluate.js
|
|
4759
4782
|
var jcsString = (v) => canonicalize(v);
|
|
4760
4783
|
var FIELD = /* @__PURE__ */ Symbol("field");
|
|
4761
4784
|
var ABSENT = /* @__PURE__ */ Symbol("absent");
|
|
@@ -5084,7 +5107,7 @@ function evaluate(artifact, facts) {
|
|
|
5084
5107
|
return ev;
|
|
5085
5108
|
}
|
|
5086
5109
|
|
|
5087
|
-
//
|
|
5110
|
+
// node_modules/@nomosprotocol/verify/dist/act.js
|
|
5088
5111
|
var MAX_WINDOW_SECONDS = 300;
|
|
5089
5112
|
var MAX_IN_FORCE_SECONDS = 86400;
|
|
5090
5113
|
var TESTIMONY_SUITES = ["Ed25519", "ES256"];
|
|
@@ -5122,7 +5145,7 @@ function hasAmbiguousEscalation(artifact) {
|
|
|
5122
5145
|
return [...named.values()].some((ids) => ids.size > 1);
|
|
5123
5146
|
}
|
|
5124
5147
|
var proofOf = (st) => st.algorithm === "WebAuthn-ES256" ? pick(st, WEBAUTHN_FIELDS) : { signature: st.signature };
|
|
5125
|
-
var statementSig = (
|
|
5148
|
+
var statementSig = (pem2, st, payload) => verifySuite(st.algorithm, pem2, jcs(payload), proofOf(st));
|
|
5126
5149
|
var time = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5127
5150
|
var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
5128
5151
|
function statementFailure(r, invalid, outOfScope, who) {
|
|
@@ -5287,11 +5310,11 @@ function verifyUnsafe(a, o) {
|
|
|
5287
5310
|
return { decision: "AUTHORIZED", commit: true, ...base };
|
|
5288
5311
|
if (ev.outcome === "block")
|
|
5289
5312
|
return { decision: "DENIED", commit: false, ...base };
|
|
5290
|
-
const
|
|
5291
|
-
if (!
|
|
5313
|
+
const esc3 = (art.governance?.escalations ?? []).find((e) => e?.id === ev.escalation_id);
|
|
5314
|
+
if (!esc3)
|
|
5292
5315
|
return { decision: "ESCALATED", commit: false, escalation_id: ev.escalation_id, consents_valid: 0, consents_required: null, ...base };
|
|
5293
|
-
const role =
|
|
5294
|
-
const needN = Math.trunc(Number(
|
|
5316
|
+
const role = esc3.role_required;
|
|
5317
|
+
const needN = Math.trunc(Number(esc3.required_consents ?? 1));
|
|
5295
5318
|
const approvers = /* @__PURE__ */ new Set();
|
|
5296
5319
|
for (const c of Array.isArray(act2.consents) ? act2.consents : []) {
|
|
5297
5320
|
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 +5340,7 @@ function verifyUnsafe(a, o) {
|
|
|
5317
5340
|
return { decision: "ESCALATED", commit: false, ...counted };
|
|
5318
5341
|
}
|
|
5319
5342
|
|
|
5320
|
-
//
|
|
5343
|
+
// node_modules/@nomosprotocol/verify/dist/seal.js
|
|
5321
5344
|
function verifySeal(artifact, publicKeyPem) {
|
|
5322
5345
|
const bad = checkSealShape(artifact);
|
|
5323
5346
|
if (bad)
|
|
@@ -5331,6 +5354,83 @@ function verifySeal(artifact, publicKeyPem) {
|
|
|
5331
5354
|
return sealFailure(artifact, publicKeyPem) ?? { decision: "VALID", kid };
|
|
5332
5355
|
}
|
|
5333
5356
|
|
|
5357
|
+
// node_modules/@nomosprotocol/verify/dist/revocation.js
|
|
5358
|
+
var MAX_LIST_CERTS = 1e3;
|
|
5359
|
+
var REVOCATION_SUITES = ["Ed25519", "ES256"];
|
|
5360
|
+
var CERT_FIELDS2 = ["parent_kid", "child_kid", "child_public_key_pem", "issued_at", "expires_at", "algorithm", "signature"];
|
|
5361
|
+
var FP = /^[0-9a-f]{64}$/;
|
|
5362
|
+
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 };
|
|
5363
|
+
var revocationListPayload = (l) => ({ generated_at: l.generated_at, statements: l.statements });
|
|
5364
|
+
var forSigning = (suite) => (scope) => suitePermitted(scope, suite) ? { ok: true } : { ok: false, dimension: "suite" };
|
|
5365
|
+
var str = (v) => typeof v === "string" && v !== "";
|
|
5366
|
+
var time2 = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5367
|
+
function signerPem(kid, pool, root, at, suite, revokedKids, revokedCerts) {
|
|
5368
|
+
const r = resolveKey({ targetKid: kid, pool, rootPublicKeyPem: root, purpose: forSigning(suite), now: new Date(at), revokedKids, revokedCerts });
|
|
5369
|
+
return r.decision === "RESOLVED" ? r.pem : null;
|
|
5370
|
+
}
|
|
5371
|
+
function evaluateStatement(st, pool, rootPublicKeyPem, now) {
|
|
5372
|
+
if (st === null || typeof st !== "object" || Array.isArray(st))
|
|
5373
|
+
return "bad_statement";
|
|
5374
|
+
const hasKid = "revoked_kid" in st, hasFp = "revoked_fingerprint" in st;
|
|
5375
|
+
if (hasKid === hasFp)
|
|
5376
|
+
return "bad_statement";
|
|
5377
|
+
const target = hasKid ? st.revoked_kid : st.revoked_fingerprint;
|
|
5378
|
+
if (!str(target) || hasFp && !FP.test(target))
|
|
5379
|
+
return "bad_statement";
|
|
5380
|
+
if (typeof st.reason !== "string" || !["issued_at", "kid", "algorithm", "signature"].every((f) => str(st[f])))
|
|
5381
|
+
return "bad_statement";
|
|
5382
|
+
if (!REVOCATION_SUITES.includes(st.algorithm))
|
|
5383
|
+
return "bad_statement";
|
|
5384
|
+
const issued = time2(st.issued_at);
|
|
5385
|
+
if (Number.isNaN(issued) || issued > now.getTime())
|
|
5386
|
+
return "bad_statement";
|
|
5387
|
+
const certs = pool.filter((c) => c.parent_kid === st.kid && (hasKid ? c.child_kid === target : fingerprint(c) === target));
|
|
5388
|
+
if (certs.length === 0)
|
|
5389
|
+
return "no_certificate";
|
|
5390
|
+
const parent = signerPem(st.kid, pool, rootPublicKeyPem, issued, st.algorithm, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
5391
|
+
if (!parent)
|
|
5392
|
+
return "signer_not_recognized";
|
|
5393
|
+
if (!certs.some((c) => verifySuite(c.algorithm, parent, jcs(certPayload(c)), { signature: c.signature }).ok))
|
|
5394
|
+
return "bad_certificate_signature";
|
|
5395
|
+
return verifySuite(st.algorithm, parent, jcs(revocationPayload(st)), { signature: st.signature }).ok ? null : "bad_signature";
|
|
5396
|
+
}
|
|
5397
|
+
function verifyRevocationList(list, o) {
|
|
5398
|
+
const L = list;
|
|
5399
|
+
if (L === null || typeof L !== "object" || !["generated_at", "kid", "algorithm", "signature"].every((f) => typeof L[f] === "string") || !Array.isArray(L.statements)) {
|
|
5400
|
+
return { decision: "MALFORMED", reason_code: "bad_list" };
|
|
5401
|
+
}
|
|
5402
|
+
const extra = L.key_certs ?? [];
|
|
5403
|
+
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"))) {
|
|
5404
|
+
return { decision: "MALFORMED", reason_code: "bad_list_certs" };
|
|
5405
|
+
}
|
|
5406
|
+
const seen = /* @__PURE__ */ new Set(), pool = [];
|
|
5407
|
+
for (const c of [...extra, ...o.ownCerts ?? []]) {
|
|
5408
|
+
const fp = fingerprint(c);
|
|
5409
|
+
if (!seen.has(fp)) {
|
|
5410
|
+
seen.add(fp);
|
|
5411
|
+
pool.push(c);
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set(), ignored = [];
|
|
5415
|
+
L.statements.forEach((st, index) => {
|
|
5416
|
+
const why = evaluateStatement(st, pool, o.rootPublicKeyPem, o.now);
|
|
5417
|
+
if (why)
|
|
5418
|
+
ignored.push({ index, reason: why });
|
|
5419
|
+
else if ("revoked_kid" in st)
|
|
5420
|
+
kids.add(st.revoked_kid);
|
|
5421
|
+
else
|
|
5422
|
+
certs.add(st.revoked_fingerprint);
|
|
5423
|
+
});
|
|
5424
|
+
const out = { decision: "OK", revoked_kids: [...kids].sort(), revoked_certs: [...certs].sort(), ignored };
|
|
5425
|
+
const generated = time2(L.generated_at);
|
|
5426
|
+
if (Number.isNaN(generated) || generated > o.now.getTime() || !REVOCATION_SUITES.includes(L.algorithm))
|
|
5427
|
+
return out;
|
|
5428
|
+
const signer = signerPem(L.kid, pool, o.rootPublicKeyPem, generated, L.algorithm, kids, certs);
|
|
5429
|
+
if (signer && verifySuite(L.algorithm, signer, jcs(revocationListPayload(L)), { signature: L.signature }).ok)
|
|
5430
|
+
out.as_of = L.generated_at;
|
|
5431
|
+
return out;
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5334
5434
|
// src/verify-chain.ts
|
|
5335
5435
|
var CHAIN_PROTOCOL_VERSION2 = "1";
|
|
5336
5436
|
var DEFAULT_BASE = "https://www.nomosprotocol.com";
|
|
@@ -5377,43 +5477,371 @@ async function verifyChainRemote(params, opts) {
|
|
|
5377
5477
|
throw new NomosAPIError(message, res.status, requestId);
|
|
5378
5478
|
}
|
|
5379
5479
|
|
|
5480
|
+
// src/bindings.ts
|
|
5481
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
5482
|
+
var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
5483
|
+
var isScalar = (v) => typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
|
|
5484
|
+
function parseToolBindings(raw) {
|
|
5485
|
+
const no = (why) => {
|
|
5486
|
+
throw new Error(why);
|
|
5487
|
+
};
|
|
5488
|
+
const obj = raw;
|
|
5489
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) no("it is not a JSON object");
|
|
5490
|
+
if (obj.version !== 1) no('"version" must be 1');
|
|
5491
|
+
if (typeof obj.artifact !== "string" || !obj.artifact) no('"artifact" must name the sealed policy');
|
|
5492
|
+
if (typeof obj.reviewed !== "boolean") no('"reviewed" must be true or false');
|
|
5493
|
+
if (!obj.tools || typeof obj.tools !== "object" || Array.isArray(obj.tools)) no('"tools" must be an object');
|
|
5494
|
+
for (const k of Object.keys(obj)) if (!["version", "artifact", "reviewed", "tools", "_review"].includes(k)) no(`unknown setting "${k}"`);
|
|
5495
|
+
const tools = {};
|
|
5496
|
+
for (const tool of Object.keys(obj.tools)) {
|
|
5497
|
+
if (FORBIDDEN_KEYS.has(tool)) no(`"${tool}" isn't a usable tool name`);
|
|
5498
|
+
const entry = obj.tools[tool];
|
|
5499
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) no(`tool "${tool}" must be an object with "facts"`);
|
|
5500
|
+
for (const k of Object.keys(entry)) if (!["facts", "_why"].includes(k)) no(`tool "${tool}": unknown setting "${k}"`);
|
|
5501
|
+
const facts = entry.facts;
|
|
5502
|
+
if (!facts || typeof facts !== "object" || Array.isArray(facts)) no(`tool "${tool}" needs "facts"`);
|
|
5503
|
+
tools[tool] = { facts: {} };
|
|
5504
|
+
for (const fact of Object.keys(facts)) {
|
|
5505
|
+
const where = `${tool}.${fact}`;
|
|
5506
|
+
if (fact === "tool") no(`${where}: "tool" is set by the guard to the tool's name`);
|
|
5507
|
+
if (FORBIDDEN_KEYS.has(fact)) no(`${where} isn't a usable fact name`);
|
|
5508
|
+
const b = facts[fact];
|
|
5509
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) no(`${where} must be an object with "arg"`);
|
|
5510
|
+
for (const k of Object.keys(b)) if (!["arg", "scale", "map", "_why"].includes(k)) no(`${where}: "${k}" isn't allowed. A fact can only come from an argument of the call`);
|
|
5511
|
+
if (typeof b.arg !== "string" || !b.arg) no(`${where} needs "arg", the argument it comes from`);
|
|
5512
|
+
if (b.arg.split(".").some((s) => !s || FORBIDDEN_KEYS.has(s))) no(`${where}: "${b.arg}" isn't a usable argument path`);
|
|
5513
|
+
if (b.scale !== void 0 && b.map !== void 0) no(`${where}: use "scale" or "map", not both`);
|
|
5514
|
+
if (b.scale !== void 0 && (typeof b.scale !== "number" || !Number.isFinite(b.scale) || b.scale === 0)) no(`${where}: "scale" must be a non-zero number`);
|
|
5515
|
+
if (b.map !== void 0) {
|
|
5516
|
+
if (!b.map || typeof b.map !== "object" || Array.isArray(b.map)) no(`${where}: "map" must be an object`);
|
|
5517
|
+
for (const k of Object.keys(b.map)) if (FORBIDDEN_KEYS.has(k) || !isScalar(b.map[k])) no(`${where}: "map" can only hold plain values`);
|
|
5518
|
+
}
|
|
5519
|
+
tools[tool].facts[fact] = { arg: b.arg, ...b.scale !== void 0 ? { scale: b.scale } : {}, ...b.map !== void 0 ? { map: b.map } : {} };
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
return { version: 1, artifact: obj.artifact, reviewed: obj.reviewed, tools };
|
|
5523
|
+
}
|
|
5524
|
+
function applyToolBindings(bindings, tool, args) {
|
|
5525
|
+
const inputs = { tool };
|
|
5526
|
+
const unresolved = [];
|
|
5527
|
+
const facts = hasOwn(bindings.tools, tool) ? bindings.tools[tool].facts : {};
|
|
5528
|
+
for (const [fact, b] of Object.entries(facts)) {
|
|
5529
|
+
let v = args;
|
|
5530
|
+
for (const seg of b.arg.split(".")) v = v && typeof v === "object" && !Array.isArray(v) && hasOwn(v, seg) ? v[seg] : void 0;
|
|
5531
|
+
if (v === void 0 || v === null) {
|
|
5532
|
+
unresolved.push({ fact, arg: b.arg, why: "missing" });
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
if (b.scale !== void 0) {
|
|
5536
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
5537
|
+
unresolved.push({ fact, arg: b.arg, why: "not_a_number" });
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
5540
|
+
inputs[fact] = Number((v * b.scale).toFixed(9));
|
|
5541
|
+
continue;
|
|
5542
|
+
}
|
|
5543
|
+
if (!isScalar(v)) {
|
|
5544
|
+
unresolved.push({ fact, arg: b.arg, why: "not_a_value" });
|
|
5545
|
+
continue;
|
|
5546
|
+
}
|
|
5547
|
+
if (b.map) {
|
|
5548
|
+
if (!hasOwn(b.map, String(v))) {
|
|
5549
|
+
unresolved.push({ fact, arg: b.arg, why: "not_in_map" });
|
|
5550
|
+
continue;
|
|
5551
|
+
}
|
|
5552
|
+
inputs[fact] = b.map[String(v)];
|
|
5553
|
+
continue;
|
|
5554
|
+
}
|
|
5555
|
+
inputs[fact] = v;
|
|
5556
|
+
}
|
|
5557
|
+
return { inputs, unresolved };
|
|
5558
|
+
}
|
|
5559
|
+
var WEAK = /* @__PURE__ */ new Set(["is", "has", "the", "a", "an", "of", "to", "in", "id", "name", "type", "value", "status", "code", "data", "info", "flag", "number", "num", "total", "ai", "system"]);
|
|
5560
|
+
var UNITS = { cents: 0.01, pence: 0.01, fils: 0.01, usd: null, dollars: null, aed: null, eur: null };
|
|
5561
|
+
var words = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).map((w) => w.toLowerCase()).filter(Boolean);
|
|
5562
|
+
var flatProperties = (props, prefix = "") => Object.entries(props ?? {}).flatMap(([k, s]) => FORBIDDEN_KEYS.has(k) ? [] : s?.type === "object" && s.properties ? flatProperties(s.properties, `${prefix}${k}.`) : [{ path: `${prefix}${k}`, schema: s }]);
|
|
5563
|
+
function suggestToolBindings(artifact, tools, fields) {
|
|
5564
|
+
const bindings = { version: 1, artifact, reviewed: false, tools: {} };
|
|
5565
|
+
const proposals = [];
|
|
5566
|
+
const unbound = [];
|
|
5567
|
+
for (const tool of tools) {
|
|
5568
|
+
bindings.tools[tool.name] = { facts: {} };
|
|
5569
|
+
const args = flatProperties(tool.inputSchema?.properties);
|
|
5570
|
+
for (const field of fields) {
|
|
5571
|
+
if (field.name === "tool") continue;
|
|
5572
|
+
const want = words(field.name).filter((w) => !WEAK.has(w) && !(w in UNITS));
|
|
5573
|
+
const scored = args.map((a) => {
|
|
5574
|
+
const raw = words(a.path);
|
|
5575
|
+
const have = raw.filter((w) => !WEAK.has(w) && !(w in UNITS));
|
|
5576
|
+
const shared = want.filter((w) => have.includes(w));
|
|
5577
|
+
const unit = raw.find((w) => w in UNITS);
|
|
5578
|
+
const same = words(field.name).join("_") === raw.join("_");
|
|
5579
|
+
const sameCore = want.length > 0 && want.length === have.length && shared.length === want.length;
|
|
5580
|
+
return { a, shared, unit, same, sameCore, score: same ? 3 : sameCore ? 2 : shared.length ? shared.length / Math.max(want.length, have.length) : 0 };
|
|
5581
|
+
}).filter((s) => s.score > 0).sort((x, y) => y.score - x.score);
|
|
5582
|
+
if (!scored.length) {
|
|
5583
|
+
unbound.push({ tool: tool.name, fact: field.name, candidates: [] });
|
|
5584
|
+
continue;
|
|
5585
|
+
}
|
|
5586
|
+
const best = scored[0];
|
|
5587
|
+
if (scored[1] && scored[1].score === best.score) {
|
|
5588
|
+
unbound.push({ tool: tool.name, fact: field.name, candidates: scored.filter((s) => s.score === best.score).map((s) => s.a.path) });
|
|
5589
|
+
continue;
|
|
5590
|
+
}
|
|
5591
|
+
const scale = best.unit ? UNITS[best.unit] ?? void 0 : void 0;
|
|
5592
|
+
const why = best.same ? "the same name" : best.sameCore ? "the same words" : `shares "${best.shared.join(", ")}"`;
|
|
5593
|
+
proposals.push({ tool: tool.name, fact: field.name, arg: best.a.path, confidence: best.same || best.sameCore ? "exact" : "likely", why: scale ? `${why}; ${best.unit} scaled by ${scale}` : why, ...scale ? { scale } : {} });
|
|
5594
|
+
bindings.tools[tool.name].facts[field.name] = { arg: best.a.path, ...scale ? { scale } : {} };
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
return { bindings, proposals, unbound };
|
|
5598
|
+
}
|
|
5599
|
+
function describeSuggestion(s) {
|
|
5600
|
+
const lines = [];
|
|
5601
|
+
for (const tool of Object.keys(s.bindings.tools)) {
|
|
5602
|
+
lines.push(`
|
|
5603
|
+
${tool}`);
|
|
5604
|
+
for (const p of s.proposals.filter((x) => x.tool === tool)) lines.push(` ${p.confidence === "exact" ? "matched " : "CHECK "} ${p.fact} <- ${p.arg}${p.scale ? ` x ${p.scale}` : ""} (${p.why})`);
|
|
5605
|
+
for (const u of s.unbound.filter((x) => x.tool === tool)) lines.push(` NO MATCH ${u.fact}${u.candidates.length ? ` (could be: ${u.candidates.join(", ")})` : ""}`);
|
|
5606
|
+
}
|
|
5607
|
+
return lines.join("\n");
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
// src/act/bytes.ts
|
|
5611
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
5612
|
+
var fromUtf82 = (b) => new TextDecoder().decode(b);
|
|
5613
|
+
function concat2(...parts) {
|
|
5614
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
5615
|
+
let o = 0;
|
|
5616
|
+
for (const p of parts) {
|
|
5617
|
+
out.set(p, o);
|
|
5618
|
+
o += p.length;
|
|
5619
|
+
}
|
|
5620
|
+
return out;
|
|
5621
|
+
}
|
|
5622
|
+
function equal2(a, b) {
|
|
5623
|
+
if (a.length !== b.length) return false;
|
|
5624
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
5625
|
+
return true;
|
|
5626
|
+
}
|
|
5627
|
+
function fromHex(h) {
|
|
5628
|
+
const out = new Uint8Array(h.length / 2);
|
|
5629
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
5630
|
+
return out;
|
|
5631
|
+
}
|
|
5632
|
+
function toBase642(b) {
|
|
5633
|
+
let s = "";
|
|
5634
|
+
for (let i = 0; i < b.length; i += 32768) s += String.fromCharCode(...b.subarray(i, i + 32768));
|
|
5635
|
+
return btoa(s);
|
|
5636
|
+
}
|
|
5637
|
+
function fromBase642(s) {
|
|
5638
|
+
const bin = atob(s);
|
|
5639
|
+
const out = new Uint8Array(bin.length);
|
|
5640
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
5641
|
+
return out;
|
|
5642
|
+
}
|
|
5643
|
+
var toBase64url2 = (b) => toBase642(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5644
|
+
var fromBase64url2 = (s) => {
|
|
5645
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
5646
|
+
return fromBase642(t + "=".repeat((4 - t.length % 4) % 4));
|
|
5647
|
+
};
|
|
5648
|
+
function randomBytes3(n) {
|
|
5649
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
5650
|
+
}
|
|
5651
|
+
var randomUUID = () => globalThis.crypto.randomUUID();
|
|
5652
|
+
|
|
5380
5653
|
// src/act/signer.ts
|
|
5381
|
-
var import_node_crypto = require("crypto");
|
|
5382
|
-
var P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551n;
|
|
5383
5654
|
function kidOf2(publicKeyPem) {
|
|
5384
|
-
const
|
|
5385
|
-
|
|
5655
|
+
const kid = kidOf(publicKeyPem);
|
|
5656
|
+
if (!kid) throw new Error("kidOf: not a canonical Ed25519 or P-256 public key (NOMOS-SPEC-010 \xA73.1)");
|
|
5657
|
+
return kid;
|
|
5658
|
+
}
|
|
5659
|
+
var ED25519_SPKI = fromHex("302a300506032b6570032100");
|
|
5660
|
+
var P256_SPKI = fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");
|
|
5661
|
+
var ED25519_PKCS8 = fromHex("302e020100300506032b657004220420");
|
|
5662
|
+
var P256_PKCS8_HEAD = fromHex("308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420");
|
|
5663
|
+
var P256_PKCS8_PUB = fromHex("a144034200");
|
|
5664
|
+
var OID_ED25519 = fromHex("2b6570");
|
|
5665
|
+
var OID_EC = fromHex("2a8648ce3d0201");
|
|
5666
|
+
var OID_P256 = fromHex("2a8648ce3d030107");
|
|
5667
|
+
function pem(label, der) {
|
|
5668
|
+
const b64 = toBase642(der);
|
|
5669
|
+
return `-----BEGIN ${label}-----
|
|
5670
|
+
${b64.match(/.{1,64}/g).join("\n")}
|
|
5671
|
+
-----END ${label}-----
|
|
5672
|
+
`;
|
|
5673
|
+
}
|
|
5674
|
+
function pemBody(text) {
|
|
5675
|
+
const m = text.trim().match(/^-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----$/);
|
|
5676
|
+
if (!m) throw new Error("Signer: expected a PEM private key");
|
|
5677
|
+
return { label: m[1], der: fromBase642(m[2].replace(/\s+/g, "")) };
|
|
5678
|
+
}
|
|
5679
|
+
function tlv(b, at) {
|
|
5680
|
+
const tag = b[at];
|
|
5681
|
+
let len = b[at + 1], start = at + 2;
|
|
5682
|
+
if (len & 128) {
|
|
5683
|
+
const n = len & 127;
|
|
5684
|
+
if (n < 1 || n > 2) throw new Error("Signer: unsupported DER length");
|
|
5685
|
+
len = 0;
|
|
5686
|
+
for (let i = 0; i < n; i++) len = len << 8 | b[start + i];
|
|
5687
|
+
start += n;
|
|
5688
|
+
}
|
|
5689
|
+
if (start + len > b.length) throw new Error("Signer: truncated DER");
|
|
5690
|
+
return { tag, start, end: start + len };
|
|
5691
|
+
}
|
|
5692
|
+
function children(b, outer) {
|
|
5693
|
+
const out = [];
|
|
5694
|
+
for (let at = outer.start; at < outer.end; ) {
|
|
5695
|
+
const t = tlv(b, at);
|
|
5696
|
+
out.push(t);
|
|
5697
|
+
at = t.end;
|
|
5698
|
+
}
|
|
5699
|
+
return out;
|
|
5700
|
+
}
|
|
5701
|
+
function sec1Scalar(b, seq) {
|
|
5702
|
+
const [version, d, ...rest] = children(b, seq);
|
|
5703
|
+
if (version.tag !== 2 || d.tag !== 4 || d.end - d.start !== 32) throw new Error("Signer: malformed EC private key");
|
|
5704
|
+
for (const r of rest) {
|
|
5705
|
+
if (r.tag === 160) {
|
|
5706
|
+
const oid = tlv(b, r.start);
|
|
5707
|
+
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)");
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
return b.slice(d.start, d.end);
|
|
5386
5711
|
}
|
|
5387
|
-
function
|
|
5388
|
-
const
|
|
5389
|
-
const
|
|
5390
|
-
if (
|
|
5391
|
-
|
|
5712
|
+
function readPrivateKey(text) {
|
|
5713
|
+
const { label, der } = pemBody(text);
|
|
5714
|
+
const top = tlv(der, 0);
|
|
5715
|
+
if (label === "EC PRIVATE KEY") return { type: "ES256", key: sec1Scalar(der, top) };
|
|
5716
|
+
if (label !== "PRIVATE KEY") throw new Error(`Signer: unsupported key type "${label}" (PKCS#8 "PRIVATE KEY" or SEC1 "EC PRIVATE KEY")`);
|
|
5717
|
+
const [, alg, key] = children(der, top);
|
|
5718
|
+
const [oid, params] = children(der, alg);
|
|
5719
|
+
const algOid = der.subarray(oid.start, oid.end);
|
|
5720
|
+
if (equal2(algOid, OID_ED25519)) {
|
|
5721
|
+
const inner = tlv(der, key.start);
|
|
5722
|
+
if (inner.tag !== 4 || inner.end - inner.start !== 32) throw new Error("Signer: malformed Ed25519 private key");
|
|
5723
|
+
return { type: "Ed25519", key: der.slice(inner.start, inner.end) };
|
|
5392
5724
|
}
|
|
5725
|
+
if (equal2(algOid, OID_EC)) {
|
|
5726
|
+
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)");
|
|
5727
|
+
return { type: "ES256", key: sec1Scalar(der, tlv(der, key.start)) };
|
|
5728
|
+
}
|
|
5729
|
+
throw new Error("Signer: only Ed25519 and P-256 keys are NOMOS signature suites (NOMOS-SPEC-010)");
|
|
5730
|
+
}
|
|
5731
|
+
function publicPem(s) {
|
|
5732
|
+
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)));
|
|
5733
|
+
}
|
|
5734
|
+
function privatePem(s) {
|
|
5735
|
+
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)));
|
|
5736
|
+
}
|
|
5737
|
+
function fromSecret(s) {
|
|
5738
|
+
const publicKeyPem = publicPem(s);
|
|
5393
5739
|
return {
|
|
5394
5740
|
kid: kidOf2(publicKeyPem),
|
|
5395
5741
|
publicKeyPem,
|
|
5396
|
-
algorithm:
|
|
5742
|
+
algorithm: s.type,
|
|
5397
5743
|
async sign(payload) {
|
|
5398
|
-
|
|
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");
|
|
5744
|
+
return toBase642(s.type === "Ed25519" ? ed25519.sign(payload, s.key) : p256.sign(payload, s.key, { prehash: true, lowS: true, format: "compact" }));
|
|
5403
5745
|
}
|
|
5404
5746
|
};
|
|
5405
5747
|
}
|
|
5406
5748
|
function localSigner(privateKeyPem) {
|
|
5407
|
-
return
|
|
5749
|
+
return fromSecret(readPrivateKey(privateKeyPem));
|
|
5408
5750
|
}
|
|
5409
5751
|
function generateSigner(algorithm = "Ed25519") {
|
|
5410
|
-
const
|
|
5411
|
-
return { ...
|
|
5752
|
+
const s = algorithm === "Ed25519" ? { type: "Ed25519", key: ed25519.utils.randomSecretKey() } : { type: "ES256", key: p256.utils.randomSecretKey() };
|
|
5753
|
+
return { ...fromSecret(s), privateKeyPem: privatePem(s) };
|
|
5412
5754
|
}
|
|
5413
5755
|
function seededSigner(seed) {
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5756
|
+
return fromSecret({ type: "Ed25519", key: sha2562(seed) });
|
|
5757
|
+
}
|
|
5758
|
+
var sha2562 = (s) => sha256(new TextEncoder().encode(s));
|
|
5759
|
+
|
|
5760
|
+
// src/act/certificates.ts
|
|
5761
|
+
var MAX_PRESENTED = 20;
|
|
5762
|
+
function reliedKids(act2) {
|
|
5763
|
+
const kids = [act2?.authority?.artifact?.seal?.kid, ...(act2?.testimony ?? []).map((t) => t?.kid), ...(act2?.consents ?? []).map((c) => c?.kid)];
|
|
5764
|
+
return [...new Set(kids.filter((k) => typeof k === "string" && k !== ""))];
|
|
5765
|
+
}
|
|
5766
|
+
function presentedCertificates(available, act2) {
|
|
5767
|
+
const unique = /* @__PURE__ */ new Map();
|
|
5768
|
+
for (const c of available) unique.set(fingerprint(c), c);
|
|
5769
|
+
const kids = new Set(reliedKids(act2)), chosen = /* @__PURE__ */ new Map();
|
|
5770
|
+
for (let grew = true; grew; ) {
|
|
5771
|
+
grew = false;
|
|
5772
|
+
for (const [fp, c] of unique) {
|
|
5773
|
+
if (!chosen.has(fp) && kids.has(c.child_kid)) {
|
|
5774
|
+
chosen.set(fp, c);
|
|
5775
|
+
kids.add(c.parent_kid);
|
|
5776
|
+
grew = true;
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
return [...chosen.values()];
|
|
5781
|
+
}
|
|
5782
|
+
async function signRevocation(parent, target, reason, issuedAt = /* @__PURE__ */ new Date()) {
|
|
5783
|
+
const st = "kid" in target ? { revoked_kid: target.kid, reason, issued_at: issuedAt.toISOString() } : { revoked_fingerprint: fingerprint(target.certificate), reason, issued_at: issuedAt.toISOString() };
|
|
5784
|
+
if ("certificate" in target && target.certificate.parent_kid !== parent.kid) throw new Error("signRevocation: only the parent that issued a certificate can revoke it");
|
|
5785
|
+
return { ...st, kid: parent.kid, algorithm: parent.algorithm, signature: await parent.sign(jcs(revocationPayload(st))) };
|
|
5786
|
+
}
|
|
5787
|
+
async function signRevocationList(signer, statements, keyCerts = [], generatedAt = /* @__PURE__ */ new Date()) {
|
|
5788
|
+
const L = { generated_at: generatedAt.toISOString(), kid: signer.kid, algorithm: signer.algorithm, statements, ...keyCerts.length ? { key_certs: keyCerts } : {} };
|
|
5789
|
+
L.signature = await signer.sign(jcs(revocationListPayload(L)));
|
|
5790
|
+
return L;
|
|
5791
|
+
}
|
|
5792
|
+
function revocationListCertificates(available, statements, listSignerKid) {
|
|
5793
|
+
const byFp = new Map(available.map((c) => [fingerprint(c), c]));
|
|
5794
|
+
const revokedCerts = statements.map((s) => s.revoked_fingerprint && byFp.get(s.revoked_fingerprint)).filter((c) => !!c);
|
|
5795
|
+
const kids = [listSignerKid, ...statements.flatMap((s) => [s.kid, s.revoked_kid].filter((k) => !!k)), ...revokedCerts.map((c) => c.parent_kid)];
|
|
5796
|
+
const closure = presentedCertificates(available, { authority: { artifact: { seal: {} } }, testimony: kids.map((kid) => ({ kid })) });
|
|
5797
|
+
const out = new Map(closure.map((c) => [fingerprint(c), c]));
|
|
5798
|
+
for (const c of revokedCerts) out.set(fingerprint(c), c);
|
|
5799
|
+
return [...out.values()];
|
|
5800
|
+
}
|
|
5801
|
+
var RevocationUnavailable = class extends Error {
|
|
5802
|
+
constructor(message, retryAfterSeconds) {
|
|
5803
|
+
super(message);
|
|
5804
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
5805
|
+
this.name = "RevocationUnavailable";
|
|
5806
|
+
}
|
|
5807
|
+
};
|
|
5808
|
+
function revocationSource(o) {
|
|
5809
|
+
const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
5810
|
+
const maxAge = (o.maxAgeSeconds ?? 3600) * 1e3, refresh = (o.refreshSeconds ?? 60) * 1e3;
|
|
5811
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
5812
|
+
let asOf = 0, fetchedAt = 0, inFlight = null, lastError = "";
|
|
5813
|
+
async function load() {
|
|
5814
|
+
fetchedAt = now().getTime();
|
|
5815
|
+
try {
|
|
5816
|
+
const res = await f(o.url, { headers: { accept: "application/json" } });
|
|
5817
|
+
if (!res.ok) {
|
|
5818
|
+
lastError = `${o.url} answered ${res.status}`;
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
const r = verifyRevocationList(await res.json(), { rootPublicKeyPem: o.root, now: now(), ownCerts: o.ownCerts });
|
|
5822
|
+
if (r.decision !== "OK") {
|
|
5823
|
+
lastError = `${o.url} served a malformed list (${r.reason_code})`;
|
|
5824
|
+
return;
|
|
5825
|
+
}
|
|
5826
|
+
for (const k of r.revoked_kids) kids.add(k);
|
|
5827
|
+
for (const c of r.revoked_certs) certs.add(c);
|
|
5828
|
+
if (r.as_of) asOf = Math.max(asOf, Date.parse(r.as_of));
|
|
5829
|
+
else lastError = `${o.url}'s list is not signed by a key your root reaches, so it does not show it is current`;
|
|
5830
|
+
} catch (e) {
|
|
5831
|
+
lastError = `${o.url}: ${e?.message ?? e}`;
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
return async () => {
|
|
5835
|
+
const t = now().getTime();
|
|
5836
|
+
if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
|
|
5837
|
+
inFlight = null;
|
|
5838
|
+
});
|
|
5839
|
+
if (inFlight) await inFlight;
|
|
5840
|
+
if (!asOf || t - asOf > maxAge) {
|
|
5841
|
+
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));
|
|
5842
|
+
}
|
|
5843
|
+
return { kids: new Set(kids), certs: new Set(certs) };
|
|
5844
|
+
};
|
|
5417
5845
|
}
|
|
5418
5846
|
|
|
5419
5847
|
// src/act/documents.ts
|
|
@@ -5518,6 +5946,11 @@ var Rollback = class extends Error {
|
|
|
5518
5946
|
this.answer = answer2;
|
|
5519
5947
|
}
|
|
5520
5948
|
};
|
|
5949
|
+
var LostRace = class extends Error {
|
|
5950
|
+
constructor() {
|
|
5951
|
+
super("replayed");
|
|
5952
|
+
}
|
|
5953
|
+
};
|
|
5521
5954
|
function accept(o) {
|
|
5522
5955
|
const kid = kidOf2(o.root);
|
|
5523
5956
|
for (const a of o.authorities) {
|
|
@@ -5525,7 +5958,7 @@ function accept(o) {
|
|
|
5525
5958
|
throw new Error(`accept(): authority ${a.artifactId} uses currencyPolicy "record" but has no inForceSealHash`);
|
|
5526
5959
|
}
|
|
5527
5960
|
}
|
|
5528
|
-
if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only.");
|
|
5961
|
+
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
5962
|
const store = o.store ?? memoryStore();
|
|
5530
5963
|
const maxWindowSeconds = o.maxWindowSeconds ?? 900;
|
|
5531
5964
|
const maxActBytes = o.maxActBytes ?? 1024 * 1024;
|
|
@@ -5551,8 +5984,7 @@ function accept(o) {
|
|
|
5551
5984
|
valid_until: new Date(now().getTime() + (o.documentTtlSeconds ?? 86400) * 1e3).toISOString()
|
|
5552
5985
|
};
|
|
5553
5986
|
}
|
|
5554
|
-
|
|
5555
|
-
const rev = await o.revocations?.() ?? {};
|
|
5987
|
+
function verifyWith(act2, authority, facts, at, seen, rev) {
|
|
5556
5988
|
return verifyAct(act2, {
|
|
5557
5989
|
relyingPartyId: o.relyingPartyId,
|
|
5558
5990
|
rootPublicKeyPem: o.root,
|
|
@@ -5584,9 +6016,20 @@ function accept(o) {
|
|
|
5584
6016
|
const authority = o.authorities.find((a) => a.artifactId === artifactId);
|
|
5585
6017
|
if (!authority) return answer({ decision: "NOT_ACCEPTED", reason_code: "authority_not_accepted", artifact_id: artifactId ?? null });
|
|
5586
6018
|
if (!authority.actions.includes(act2?.action?.type)) return answer({ decision: "NOT_ACCEPTED", reason_code: "action_not_accepted", action_type: act2?.action?.type ?? null });
|
|
6019
|
+
let rev;
|
|
6020
|
+
try {
|
|
6021
|
+
rev = await o.revocations?.() ?? {};
|
|
6022
|
+
} catch (e) {
|
|
6023
|
+
const retry = e instanceof RevocationUnavailable ? e.retryAfterSeconds : 60;
|
|
6024
|
+
return {
|
|
6025
|
+
status: 503,
|
|
6026
|
+
headers: { "content-type": "application/json", "retry-after": String(retry) },
|
|
6027
|
+
body: { error: "Revocation status is not current, so nothing is committed. Resubmit this same Act later.", detail: String(e?.message ?? e), retry_after: retry }
|
|
6028
|
+
};
|
|
6029
|
+
}
|
|
5587
6030
|
const verifiedAt = now();
|
|
5588
6031
|
const facts = await o.localFacts?.(act2) ?? {};
|
|
5589
|
-
const verdict =
|
|
6032
|
+
const verdict = verifyWith(act2, authority, facts, verifiedAt, await store.hasNonce(act2.nonce), rev);
|
|
5590
6033
|
const receiptOf = (v, f, committedAt) => ({
|
|
5591
6034
|
act_id: act2.act_id,
|
|
5592
6035
|
act_digest: String(v.act_digest ?? ""),
|
|
@@ -5606,35 +6049,34 @@ function accept(o) {
|
|
|
5606
6049
|
if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
|
|
5607
6050
|
await o.onVerdict?.(verdict, receiptOf(verdict, facts));
|
|
5608
6051
|
if (verdict.decision === "ESCALATED") {
|
|
5609
|
-
const
|
|
5610
|
-
return answer(verdict, { role:
|
|
6052
|
+
const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
|
|
6053
|
+
return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
|
|
5611
6054
|
}
|
|
5612
6055
|
if (o.mode === "shadow") return { ...answer(verdict), status: 200, body: { ...answer(verdict).body, shadow: true } };
|
|
5613
6056
|
return answer(verdict);
|
|
5614
6057
|
}
|
|
5615
6058
|
try {
|
|
5616
6059
|
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
|
-
}
|
|
6060
|
+
if (!await t.claimNonce(act2.nonce)) throw new LostRace();
|
|
5620
6061
|
let final = verdict, finalFacts = facts;
|
|
5621
6062
|
if (o.localFacts) {
|
|
5622
6063
|
const fresh = await o.localFacts(act2, t.tx);
|
|
5623
6064
|
if (!sameFacts(fresh, facts)) {
|
|
5624
6065
|
finalFacts = fresh;
|
|
5625
|
-
final =
|
|
6066
|
+
final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
|
|
5626
6067
|
if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
|
|
5627
6068
|
}
|
|
5628
6069
|
}
|
|
5629
6070
|
const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
|
|
5630
6071
|
const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
|
|
5631
6072
|
const receipt = receiptOf(final, finalFacts, now());
|
|
5632
|
-
await t.putReceipt(act2.nonce, receipt);
|
|
6073
|
+
await t.putReceipt(act2.nonce, receipt, act2);
|
|
5633
6074
|
await o.onVerdict?.(final, receipt);
|
|
5634
6075
|
return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
|
|
5635
6076
|
});
|
|
5636
6077
|
} catch (e) {
|
|
5637
6078
|
if (e instanceof Rollback) return e.answer;
|
|
6079
|
+
if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
|
|
5638
6080
|
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
6081
|
}
|
|
5640
6082
|
}
|
|
@@ -5673,7 +6115,7 @@ function accept(o) {
|
|
|
5673
6115
|
if (size > maxActBytes) break;
|
|
5674
6116
|
chunks.push(c);
|
|
5675
6117
|
}
|
|
5676
|
-
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) :
|
|
6118
|
+
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) : concat2(...chunks);
|
|
5677
6119
|
}
|
|
5678
6120
|
const a = await handle({ method: req.method, headers: req.headers, body });
|
|
5679
6121
|
send(a.status, a.headers, JSON.stringify(a.body));
|
|
@@ -5684,7 +6126,7 @@ function accept(o) {
|
|
|
5684
6126
|
function consentOrigins(act2) {
|
|
5685
6127
|
const origins = (Array.isArray(act2?.consents) ? act2.consents : []).filter((c) => c?.algorithm === "WebAuthn-ES256" && typeof c.client_data_json === "string").map((c) => {
|
|
5686
6128
|
try {
|
|
5687
|
-
return JSON.parse(
|
|
6129
|
+
return JSON.parse(fromUtf82(fromBase64url2(c.client_data_json))).origin;
|
|
5688
6130
|
} catch {
|
|
5689
6131
|
return null;
|
|
5690
6132
|
}
|
|
@@ -5692,8 +6134,74 @@ function consentOrigins(act2) {
|
|
|
5692
6134
|
return origins.length ? { consent_origins: [...new Set(origins)] } : {};
|
|
5693
6135
|
}
|
|
5694
6136
|
|
|
6137
|
+
// src/act/postgres.ts
|
|
6138
|
+
var IDENT = /^[a-z_][a-z0-9_]{0,62}(\.[a-z_][a-z0-9_]{0,62})?$/;
|
|
6139
|
+
function postgresStore(pool, opts = {}) {
|
|
6140
|
+
const table = opts.table ?? "nomos_acts";
|
|
6141
|
+
if (!IDENT.test(table)) throw new Error(`postgresStore: "${table}" is not a plain lower-case table name`);
|
|
6142
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${table} (
|
|
6143
|
+
nonce text PRIMARY KEY, -- SPEC-008 \xA75.2: the nonce ledger
|
|
6144
|
+
act_id text NOT NULL,
|
|
6145
|
+
receipt json NOT NULL, -- SPEC-008 \xA79; json, not jsonb: kept byte for byte
|
|
6146
|
+
act json NOT NULL, -- the exact Act verified, for re-verification
|
|
6147
|
+
committed_at timestamptz NOT NULL DEFAULT now()
|
|
6148
|
+
);`;
|
|
6149
|
+
return {
|
|
6150
|
+
schema,
|
|
6151
|
+
async migrate() {
|
|
6152
|
+
await pool.query(schema);
|
|
6153
|
+
},
|
|
6154
|
+
async hasNonce(nonce) {
|
|
6155
|
+
const r = await pool.query(`SELECT 1 FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
6156
|
+
return (r.rowCount ?? r.rows.length) > 0;
|
|
6157
|
+
},
|
|
6158
|
+
async receiptFor(nonce) {
|
|
6159
|
+
const r = await pool.query(`SELECT receipt FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
6160
|
+
return r.rows[0]?.receipt ?? null;
|
|
6161
|
+
},
|
|
6162
|
+
async transaction(fn) {
|
|
6163
|
+
const client = await pool.connect();
|
|
6164
|
+
let claimed = null, completed = false;
|
|
6165
|
+
try {
|
|
6166
|
+
await client.query("BEGIN");
|
|
6167
|
+
const out = await fn({
|
|
6168
|
+
tx: client,
|
|
6169
|
+
async claimNonce(nonce) {
|
|
6170
|
+
const r = await client.query(
|
|
6171
|
+
`INSERT INTO ${table} (nonce, act_id, receipt, act) VALUES ($1, '', '{}'::json, '{}'::json)
|
|
6172
|
+
ON CONFLICT (nonce) DO NOTHING RETURNING nonce`,
|
|
6173
|
+
[nonce]
|
|
6174
|
+
);
|
|
6175
|
+
const won = (r.rowCount ?? r.rows.length) === 1;
|
|
6176
|
+
if (won) claimed = nonce;
|
|
6177
|
+
return won;
|
|
6178
|
+
},
|
|
6179
|
+
async putReceipt(nonce, receipt, act2) {
|
|
6180
|
+
if (nonce !== claimed) throw new Error("postgresStore: putReceipt for a nonce this transaction did not claim");
|
|
6181
|
+
await client.query(
|
|
6182
|
+
`UPDATE ${table} SET act_id = $2, receipt = $3::json, act = $4::json WHERE nonce = $1`,
|
|
6183
|
+
[nonce, receipt.act_id, JSON.stringify(receipt), JSON.stringify(act2 ?? {})]
|
|
6184
|
+
);
|
|
6185
|
+
completed = true;
|
|
6186
|
+
}
|
|
6187
|
+
});
|
|
6188
|
+
if (claimed && !completed) throw new Error("postgresStore: the transaction claimed a nonce but stored no receipt");
|
|
6189
|
+
await client.query("COMMIT");
|
|
6190
|
+
client.release();
|
|
6191
|
+
return out;
|
|
6192
|
+
} catch (e) {
|
|
6193
|
+
let broken;
|
|
6194
|
+
await client.query("ROLLBACK").catch((err) => {
|
|
6195
|
+
broken = err;
|
|
6196
|
+
});
|
|
6197
|
+
client.release(broken);
|
|
6198
|
+
throw e;
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
|
|
5695
6204
|
// src/act/statements.ts
|
|
5696
|
-
var import_node_crypto2 = require("crypto");
|
|
5697
6205
|
async function certify(parent, child, opts) {
|
|
5698
6206
|
const unsigned = {
|
|
5699
6207
|
parent_kid: parent.kid,
|
|
@@ -5747,8 +6255,8 @@ function buildAct(o) {
|
|
|
5747
6255
|
const window = o.windowSeconds ?? 300;
|
|
5748
6256
|
const act2 = {
|
|
5749
6257
|
act_version: "1",
|
|
5750
|
-
act_id: `act-${
|
|
5751
|
-
nonce: (
|
|
6258
|
+
act_id: `act-${randomUUID()}`,
|
|
6259
|
+
nonce: toBase642(randomBytes3(16)),
|
|
5752
6260
|
// Issued a second in the past so a relying party's clock a moment behind ours still accepts it.
|
|
5753
6261
|
issued_at: new Date(now.getTime() - 1e3).toISOString(),
|
|
5754
6262
|
expires_at: new Date(now.getTime() - 1e3 + window * 1e3).toISOString(),
|
|
@@ -5762,6 +6270,575 @@ function buildAct(o) {
|
|
|
5762
6270
|
return act2;
|
|
5763
6271
|
}
|
|
5764
6272
|
|
|
6273
|
+
// src/act/registry.ts
|
|
6274
|
+
function memoryRegistryStore() {
|
|
6275
|
+
const certs = [], sts = [];
|
|
6276
|
+
return {
|
|
6277
|
+
async certificates() {
|
|
6278
|
+
return [...certs];
|
|
6279
|
+
},
|
|
6280
|
+
async addCertificate(c) {
|
|
6281
|
+
if (!certs.some((x) => fingerprint(x) === fingerprint(c))) certs.push(c);
|
|
6282
|
+
},
|
|
6283
|
+
async statements() {
|
|
6284
|
+
return [...sts];
|
|
6285
|
+
},
|
|
6286
|
+
async addStatement(s) {
|
|
6287
|
+
sts.push(s);
|
|
6288
|
+
}
|
|
6289
|
+
};
|
|
6290
|
+
}
|
|
6291
|
+
var RegistryError = class extends Error {
|
|
6292
|
+
constructor(message, code) {
|
|
6293
|
+
super(message);
|
|
6294
|
+
this.code = code;
|
|
6295
|
+
this.name = "RegistryError";
|
|
6296
|
+
}
|
|
6297
|
+
};
|
|
6298
|
+
var any = () => ({ ok: true });
|
|
6299
|
+
function certificateRegistry(o) {
|
|
6300
|
+
const rootKid = kidOf(o.root);
|
|
6301
|
+
if (!rootKid) throw new RegistryError("certificateRegistry: the root is not a canonical Ed25519 or P-256 public key", "bad_root");
|
|
6302
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6303
|
+
async function revokedSets() {
|
|
6304
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
6305
|
+
for (const s of await o.store.statements()) s.revoked_kid ? kids.add(s.revoked_kid) : s.revoked_fingerprint && certs.add(s.revoked_fingerprint);
|
|
6306
|
+
return { kids, certs };
|
|
6307
|
+
}
|
|
6308
|
+
async function scopesOf(kid) {
|
|
6309
|
+
if (kid === rootKid) return { ok: true, scopes: [{ terms: {} }] };
|
|
6310
|
+
const { kids, certs } = await revokedSets();
|
|
6311
|
+
const r = resolveKey({ targetKid: kid, pool: await o.store.certificates(), rootPublicKeyPem: o.root, purpose: any, now: now(), revokedKids: kids, revokedCerts: certs, allPaths: true });
|
|
6312
|
+
if (r.decision !== "RESOLVED") return { ok: false, reason: `${r.decision}${r.reason_code ? ` (${r.reason_code})` : ""}` };
|
|
6313
|
+
return { ok: true, scopes: r.admitted ?? [r.effective] };
|
|
6314
|
+
}
|
|
6315
|
+
async function pool(filter) {
|
|
6316
|
+
const { kids, certs } = await revokedSets(), t = now().getTime();
|
|
6317
|
+
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)));
|
|
6318
|
+
}
|
|
6319
|
+
return {
|
|
6320
|
+
rootKid,
|
|
6321
|
+
pool,
|
|
6322
|
+
/** NOMOS-SPEC-007 §3: certify `child` under `parent`. Refuses anything a verifier would refuse. */
|
|
6323
|
+
async issue(io) {
|
|
6324
|
+
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");
|
|
6325
|
+
const scope = parseScope(io.scope ?? null);
|
|
6326
|
+
if (!scope.ok) throw new RegistryError(`Malformed scope: ${scope.detail}`, "malformed_scope");
|
|
6327
|
+
const parentScopes = await scopesOf(io.parent.kid);
|
|
6328
|
+
if (!parentScopes.ok) throw new RegistryError(`The parent ${io.parent.kid} is not reached from the root: ${parentScopes.reason}`, "parent_not_recognized");
|
|
6329
|
+
const fits = parentScopes.scopes.filter((p) => suitePermitted(p, io.parent.algorithm) && isNarrowerOrEqual(scope.scope, p).ok);
|
|
6330
|
+
if (!fits.length) {
|
|
6331
|
+
const widened = parentScopes.scopes.map((p) => isNarrowerOrEqual(scope.scope, p)).find((x) => !x.ok);
|
|
6332
|
+
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");
|
|
6333
|
+
}
|
|
6334
|
+
const expiresAt = io.expiresAt ?? (io.days ? new Date(now().getTime() + io.days * 864e5) : void 0);
|
|
6335
|
+
if (!expiresAt) throw new RegistryError("Give expiresAt or days: a certificate always expires", "no_expiry");
|
|
6336
|
+
const c = await certify(io.parent, io.child, { scope: io.scope, issuedAt: io.issuedAt ?? now(), expiresAt });
|
|
6337
|
+
await o.store.addCertificate(c);
|
|
6338
|
+
return c;
|
|
6339
|
+
},
|
|
6340
|
+
/** rev. 1.10 §5.1: `parent` revokes a key it certified. The key is dead on every path. */
|
|
6341
|
+
async revokeKey(io) {
|
|
6342
|
+
const certs = (await o.store.certificates()).filter((c) => c.child_kid === io.kid && c.parent_kid === io.parent.kid);
|
|
6343
|
+
if (!certs.length) throw new RegistryError(`${io.parent.kid} never certified ${io.kid}: only a key's parent can revoke it`, "not_the_parent");
|
|
6344
|
+
const st = await signRevocation(io.parent, { kid: io.kid }, io.reason, now());
|
|
6345
|
+
await o.store.addStatement(st);
|
|
6346
|
+
return st;
|
|
6347
|
+
},
|
|
6348
|
+
/** rev. 1.10 §5.1: withdraw one certificate; the key stays valid under any other. */
|
|
6349
|
+
async revokeCertificate(io) {
|
|
6350
|
+
const c = (await o.store.certificates()).find((x) => fingerprint(x) === io.fingerprint);
|
|
6351
|
+
if (!c) throw new RegistryError(`No certificate ${io.fingerprint} in this registry`, "unknown_certificate");
|
|
6352
|
+
const st = await signRevocation(io.parent, { certificate: c }, io.reason, now());
|
|
6353
|
+
await o.store.addStatement(st);
|
|
6354
|
+
return st;
|
|
6355
|
+
},
|
|
6356
|
+
/** The certificates that connect `kid` to the root: what a witness or approver returns with
|
|
6357
|
+
* what it signs (SPEC-009 0.2.0 §4, §5). */
|
|
6358
|
+
async chainFor(kid) {
|
|
6359
|
+
return presentedCertificates(await pool(), { authority: { artifact: { seal: { kid } } } });
|
|
6360
|
+
},
|
|
6361
|
+
/** How `kid` is reached from the root, and with which effective scopes, or why it is not. */
|
|
6362
|
+
async check(kid) {
|
|
6363
|
+
return scopesOf(kid);
|
|
6364
|
+
},
|
|
6365
|
+
certificates: () => o.store.certificates(),
|
|
6366
|
+
statements: () => o.store.statements(),
|
|
6367
|
+
/** rev. 1.10 §5.3: the signed list, carrying the certificates that make it checkable. The
|
|
6368
|
+
* signer should be reached from the root, or the list establishes no date (§5.3). */
|
|
6369
|
+
async revocationList(signer) {
|
|
6370
|
+
const statements = await o.store.statements();
|
|
6371
|
+
return signRevocationList(signer, statements, revocationListCertificates(await o.store.certificates(), statements, signer.kid), now());
|
|
6372
|
+
}
|
|
6373
|
+
};
|
|
6374
|
+
}
|
|
6375
|
+
function postgresRegistryStore(pool, opts = {}) {
|
|
6376
|
+
const p = opts.prefix ?? "nomos_registry";
|
|
6377
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new RegistryError(`"${p}" is not a plain lower-case prefix`, "bad_prefix");
|
|
6378
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_certificates (
|
|
6379
|
+
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()
|
|
6380
|
+
);
|
|
6381
|
+
CREATE TABLE IF NOT EXISTS ${p}_statements (
|
|
6382
|
+
id bigserial PRIMARY KEY, target text NOT NULL, kid text NOT NULL, statement json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
|
|
6383
|
+
);`;
|
|
6384
|
+
return {
|
|
6385
|
+
schema,
|
|
6386
|
+
async migrate() {
|
|
6387
|
+
await pool.query(schema);
|
|
6388
|
+
},
|
|
6389
|
+
async certificates() {
|
|
6390
|
+
return (await pool.query(`SELECT certificate FROM ${p}_certificates ORDER BY added_at, fingerprint`)).rows.map((r) => r.certificate);
|
|
6391
|
+
},
|
|
6392
|
+
async addCertificate(c) {
|
|
6393
|
+
await pool.query(
|
|
6394
|
+
`INSERT INTO ${p}_certificates (fingerprint, child_kid, parent_kid, certificate) VALUES ($1, $2, $3, $4::json) ON CONFLICT (fingerprint) DO NOTHING`,
|
|
6395
|
+
[fingerprint(c), c.child_kid, c.parent_kid, JSON.stringify(c)]
|
|
6396
|
+
);
|
|
6397
|
+
},
|
|
6398
|
+
async statements() {
|
|
6399
|
+
return (await pool.query(`SELECT statement FROM ${p}_statements ORDER BY id`)).rows.map((r) => r.statement);
|
|
6400
|
+
},
|
|
6401
|
+
async addStatement(s) {
|
|
6402
|
+
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)]);
|
|
6403
|
+
}
|
|
6404
|
+
};
|
|
6405
|
+
}
|
|
6406
|
+
|
|
6407
|
+
// src/act/passkeys.ts
|
|
6408
|
+
function cbor(b) {
|
|
6409
|
+
let i = 0;
|
|
6410
|
+
const len = (info) => {
|
|
6411
|
+
if (info < 24) return info;
|
|
6412
|
+
const n = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : -1;
|
|
6413
|
+
if (n < 0) throw new Error("unsupported CBOR length");
|
|
6414
|
+
let v2 = 0;
|
|
6415
|
+
for (let k = 0; k < n; k++) v2 = v2 * 256 + b[i++];
|
|
6416
|
+
return v2;
|
|
6417
|
+
};
|
|
6418
|
+
const item = (depth) => {
|
|
6419
|
+
if (depth > 16 || i >= b.length) throw new Error("malformed CBOR");
|
|
6420
|
+
const head = b[i++], major = head >> 5, info = head & 31;
|
|
6421
|
+
switch (major) {
|
|
6422
|
+
case 0:
|
|
6423
|
+
return len(info);
|
|
6424
|
+
case 1:
|
|
6425
|
+
return -1 - len(info);
|
|
6426
|
+
case 2: {
|
|
6427
|
+
const n = len(info);
|
|
6428
|
+
const v2 = b.slice(i, i + n);
|
|
6429
|
+
i += n;
|
|
6430
|
+
if (v2.length !== n) throw new Error("truncated CBOR");
|
|
6431
|
+
return v2;
|
|
6432
|
+
}
|
|
6433
|
+
case 3: {
|
|
6434
|
+
const n = len(info);
|
|
6435
|
+
const v2 = fromUtf82(b.slice(i, i + n));
|
|
6436
|
+
i += n;
|
|
6437
|
+
return v2;
|
|
6438
|
+
}
|
|
6439
|
+
case 4: {
|
|
6440
|
+
const n = len(info), a = [];
|
|
6441
|
+
for (let k = 0; k < n; k++) a.push(item(depth + 1));
|
|
6442
|
+
return a;
|
|
6443
|
+
}
|
|
6444
|
+
case 5: {
|
|
6445
|
+
const n = len(info), m = /* @__PURE__ */ new Map();
|
|
6446
|
+
for (let k = 0; k < n; k++) m.set(item(depth + 1), item(depth + 1));
|
|
6447
|
+
return m;
|
|
6448
|
+
}
|
|
6449
|
+
case 7:
|
|
6450
|
+
if (info === 20) return false;
|
|
6451
|
+
if (info === 21) return true;
|
|
6452
|
+
if (info === 22) return null;
|
|
6453
|
+
throw new Error("unsupported CBOR simple value");
|
|
6454
|
+
default:
|
|
6455
|
+
throw new Error("unsupported CBOR type");
|
|
6456
|
+
}
|
|
6457
|
+
};
|
|
6458
|
+
const v = item(0);
|
|
6459
|
+
return { value: v, end: i };
|
|
6460
|
+
}
|
|
6461
|
+
function readAttestation(attestationObject) {
|
|
6462
|
+
const att = cbor(attestationObject).value;
|
|
6463
|
+
if (!(att instanceof Map) || !(att.get("authData") instanceof Uint8Array)) throw new Error("not an attestation object");
|
|
6464
|
+
const a = att.get("authData");
|
|
6465
|
+
if (a.length < 55 || !(a[32] & 64)) throw new Error("no attested credential data");
|
|
6466
|
+
const idLen = a[53] << 8 | a[54];
|
|
6467
|
+
const credentialId = a.slice(55, 55 + idLen);
|
|
6468
|
+
const cose = cbor(a.slice(55 + idLen)).value;
|
|
6469
|
+
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)");
|
|
6470
|
+
const x = cose.get(-2), y = cose.get(-3);
|
|
6471
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) throw new Error("malformed COSE key");
|
|
6472
|
+
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);
|
|
6473
|
+
const publicKeyPem = pem("PUBLIC KEY", spki);
|
|
6474
|
+
if (!kidOf(publicKeyPem)) throw new Error("the passkey's key is not a valid P-256 point");
|
|
6475
|
+
return { authData: a, flags: a[32], credentialId, publicKeyPem };
|
|
6476
|
+
}
|
|
6477
|
+
async function signEnrolmentToken(authority, c) {
|
|
6478
|
+
if (!/^[^,\s]+$/.test(c.approver)) throw new Error("An approver id has no commas or spaces (NOMOS-SPEC-007 rev. 1.9 \xA73.7)");
|
|
6479
|
+
const claims = { approver: c.approver, roles: [...c.roles], origin: c.origin.replace(/\/$/, ""), expires_at: c.expiresAt.toISOString(), nonce: toBase64url2(randomBytes3(16)) };
|
|
6480
|
+
const token = { ...claims, kid: authority.kid, algorithm: authority.algorithm, signature: await authority.sign(jcs(claims)) };
|
|
6481
|
+
return toBase64url2(utf82(JSON.stringify(token)));
|
|
6482
|
+
}
|
|
6483
|
+
function verifyEnrolmentToken(token, authorityPem, origin, now) {
|
|
6484
|
+
let t;
|
|
6485
|
+
try {
|
|
6486
|
+
t = JSON.parse(fromUtf82(fromBase64url2(token)));
|
|
6487
|
+
} catch {
|
|
6488
|
+
return { error: "not an enrolment link" };
|
|
6489
|
+
}
|
|
6490
|
+
const claims = { approver: t?.approver, roles: t?.roles, origin: t?.origin, expires_at: t?.expires_at, nonce: t?.nonce };
|
|
6491
|
+
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" };
|
|
6492
|
+
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" };
|
|
6493
|
+
if (claims.origin !== origin) return { error: "this enrolment link is for another service" };
|
|
6494
|
+
if (!(Date.parse(claims.expires_at) > now.getTime())) return { error: "this enrolment link has expired" };
|
|
6495
|
+
return claims;
|
|
6496
|
+
}
|
|
6497
|
+
function memoryPasskeyStore() {
|
|
6498
|
+
const used = /* @__PURE__ */ new Set(), challenges = /* @__PURE__ */ new Map(), creds = [], pend = /* @__PURE__ */ new Map();
|
|
6499
|
+
return {
|
|
6500
|
+
async claimToken(n) {
|
|
6501
|
+
if (used.has(n)) return false;
|
|
6502
|
+
used.add(n);
|
|
6503
|
+
return true;
|
|
6504
|
+
},
|
|
6505
|
+
async tokenUsed(n) {
|
|
6506
|
+
return used.has(n);
|
|
6507
|
+
},
|
|
6508
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6509
|
+
if (!used.has(n)) challenges.set(n, c);
|
|
6510
|
+
},
|
|
6511
|
+
async enrolmentChallenge(n) {
|
|
6512
|
+
return used.has(n) ? null : challenges.get(n) ?? null;
|
|
6513
|
+
},
|
|
6514
|
+
async addCredential(c) {
|
|
6515
|
+
creds.push(structuredClone(c));
|
|
6516
|
+
},
|
|
6517
|
+
async credentials() {
|
|
6518
|
+
return structuredClone(creds);
|
|
6519
|
+
},
|
|
6520
|
+
async createPending(p) {
|
|
6521
|
+
pend.set(p.id, structuredClone(p));
|
|
6522
|
+
},
|
|
6523
|
+
async pending(id) {
|
|
6524
|
+
const p = pend.get(id);
|
|
6525
|
+
return p ? structuredClone(p) : null;
|
|
6526
|
+
},
|
|
6527
|
+
async addAttempt(id, c, a) {
|
|
6528
|
+
const p = pend.get(id);
|
|
6529
|
+
if (!p || p.status !== "pending") return false;
|
|
6530
|
+
p.attempts[c] = { ...a };
|
|
6531
|
+
return true;
|
|
6532
|
+
},
|
|
6533
|
+
async resolvePending(id, out) {
|
|
6534
|
+
const p = pend.get(id);
|
|
6535
|
+
if (!p || p.status !== "pending") return false;
|
|
6536
|
+
Object.assign(p, structuredClone(out));
|
|
6537
|
+
return true;
|
|
6538
|
+
}
|
|
6539
|
+
};
|
|
6540
|
+
}
|
|
6541
|
+
function postgresPasskeyStore(pool, opts = {}) {
|
|
6542
|
+
const p = opts.prefix ?? "nomos_passkeys";
|
|
6543
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new Error(`"${p}" is not a plain lower-case prefix`);
|
|
6544
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_tokens (nonce text PRIMARY KEY, challenge text, used boolean NOT NULL DEFAULT false);
|
|
6545
|
+
CREATE TABLE IF NOT EXISTS ${p}_credentials (credential_id text PRIMARY KEY, credential json NOT NULL, added_at timestamptz NOT NULL DEFAULT now());
|
|
6546
|
+
CREATE TABLE IF NOT EXISTS ${p}_pending (id text PRIMARY KEY, approval jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now());`;
|
|
6547
|
+
return {
|
|
6548
|
+
schema,
|
|
6549
|
+
async migrate() {
|
|
6550
|
+
await pool.query(schema);
|
|
6551
|
+
},
|
|
6552
|
+
async claimToken(n) {
|
|
6553
|
+
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]);
|
|
6554
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6555
|
+
},
|
|
6556
|
+
async tokenUsed(n) {
|
|
6557
|
+
return (await pool.query(`SELECT 1 FROM ${p}_tokens WHERE nonce = $1 AND used = true`, [n])).rows.length > 0;
|
|
6558
|
+
},
|
|
6559
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6560
|
+
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]);
|
|
6561
|
+
},
|
|
6562
|
+
async enrolmentChallenge(n) {
|
|
6563
|
+
return (await pool.query(`SELECT challenge FROM ${p}_tokens WHERE nonce = $1 AND used = false`, [n])).rows[0]?.challenge ?? null;
|
|
6564
|
+
},
|
|
6565
|
+
async addCredential(c) {
|
|
6566
|
+
await pool.query(`INSERT INTO ${p}_credentials (credential_id, credential) VALUES ($1, $2::json)`, [c.credential_id, JSON.stringify(c)]);
|
|
6567
|
+
},
|
|
6568
|
+
async credentials() {
|
|
6569
|
+
return (await pool.query(`SELECT credential FROM ${p}_credentials ORDER BY added_at`)).rows.map((r) => r.credential);
|
|
6570
|
+
},
|
|
6571
|
+
async createPending(a) {
|
|
6572
|
+
await pool.query(`INSERT INTO ${p}_pending (id, approval) VALUES ($1, $2::jsonb)`, [a.id, JSON.stringify(a)]);
|
|
6573
|
+
},
|
|
6574
|
+
async pending(id) {
|
|
6575
|
+
return (await pool.query(`SELECT approval FROM ${p}_pending WHERE id = $1`, [id])).rows[0]?.approval ?? null;
|
|
6576
|
+
},
|
|
6577
|
+
// Each change is one conditional statement: Postgres serializes them on the row, and none
|
|
6578
|
+
// rewrites the whole record, so a stale reader cannot write an old status back.
|
|
6579
|
+
async addAttempt(id, c, a) {
|
|
6580
|
+
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)]);
|
|
6581
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6582
|
+
},
|
|
6583
|
+
async resolvePending(id, out) {
|
|
6584
|
+
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)]);
|
|
6585
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6586
|
+
}
|
|
6587
|
+
};
|
|
6588
|
+
}
|
|
6589
|
+
var MAX_ATTEMPTS = 50;
|
|
6590
|
+
var esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6591
|
+
function passkeyApprover(o) {
|
|
6592
|
+
const base = o.origin.replace(/\/$/, "");
|
|
6593
|
+
const rpId = o.rpId ?? new URL(base).hostname;
|
|
6594
|
+
const store = o.store ?? memoryPasskeyStore();
|
|
6595
|
+
if (!o.store) console.warn("[nomos] passkeyApprover(): using the in-memory store. Enrolled passkeys are lost on restart. Development only; use postgresPasskeyStore() in production.");
|
|
6596
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6597
|
+
const rpIdHash = sha256(utf82(rpId));
|
|
6598
|
+
const expired = (p) => p.status === "pending" && Date.parse(p.act.expires_at) <= now().getTime();
|
|
6599
|
+
const statusOf = (p) => expired(p) ? "expired" : p.status;
|
|
6600
|
+
function clientData(b64, type, challenge) {
|
|
6601
|
+
let c;
|
|
6602
|
+
try {
|
|
6603
|
+
c = JSON.parse(fromUtf82(fromBase64url2(b64)));
|
|
6604
|
+
} catch {
|
|
6605
|
+
return "unreadable client data";
|
|
6606
|
+
}
|
|
6607
|
+
if (c?.type !== type) return `expected a ${type} ceremony`;
|
|
6608
|
+
if (c?.challenge !== challenge) return "the challenge does not match";
|
|
6609
|
+
if (c?.origin !== base) return `the ceremony ran on ${c?.origin}, not ${base}`;
|
|
6610
|
+
return null;
|
|
6611
|
+
}
|
|
6612
|
+
async function route(method, path, json) {
|
|
6613
|
+
if (method === "POST" && path === "/nomos/approvals") {
|
|
6614
|
+
const { role, act: act2 } = await json();
|
|
6615
|
+
if (!o.roles.includes(role)) return { status: 404, body: { error: `This service does not approve as ${role}.` } };
|
|
6616
|
+
if (typeof act2?.expires_at !== "string" || !act2?.action) return { status: 400, body: { error: "The request needs the Act." } };
|
|
6617
|
+
const id = randomUUID();
|
|
6618
|
+
await store.createPending({ id, role, act: act2, status: "pending", created_at: now().toISOString(), attempts: {} });
|
|
6619
|
+
await o.onPending?.({ id, role, act: act2, approvalUrl: `${base}/approve/${id}` });
|
|
6620
|
+
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
6621
|
+
}
|
|
6622
|
+
let m = path.match(/^\/nomos\/approvals\/([0-9a-f-]{36})$/);
|
|
6623
|
+
if (method === "GET" && m) {
|
|
6624
|
+
const p = await store.pending(m[1]);
|
|
6625
|
+
if (!p) return null;
|
|
6626
|
+
const s = statusOf(p);
|
|
6627
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...p.key_certs?.length ? { key_certs: p.key_certs } : {} } : {} } };
|
|
6628
|
+
}
|
|
6629
|
+
m = path.match(/^\/approve\/([0-9a-f-]{36})(?:\/(devices|options|assertion|refuse))?$/);
|
|
6630
|
+
if (m) {
|
|
6631
|
+
const p = await store.pending(m[1]);
|
|
6632
|
+
if (!p) return null;
|
|
6633
|
+
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) };
|
|
6634
|
+
if (method !== "POST" || !m[2]) return null;
|
|
6635
|
+
if (statusOf(p) !== "pending") return { status: 409, body: { error: `This request is ${statusOf(p)}.` } };
|
|
6636
|
+
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." } };
|
|
6637
|
+
const req = await json();
|
|
6638
|
+
if (m[2] === "devices") {
|
|
6639
|
+
const mine = (await store.credentials()).filter((c) => c.approver === String(req?.approver ?? "") && c.roles.includes(p.role));
|
|
6640
|
+
return { status: 200, body: { devices: mine.map((c) => ({ credential_id: c.credential_id, label: c.label })) } };
|
|
6641
|
+
}
|
|
6642
|
+
const cred = (await store.credentials()).find((c) => c.credential_id === req?.credential_id);
|
|
6643
|
+
if (!cred || !cred.roles.includes(p.role)) return { status: 403, body: { error: `That passkey does not approve as ${p.role}.` } };
|
|
6644
|
+
if (m[2] === "options") {
|
|
6645
|
+
const signed_at = now().toISOString();
|
|
6646
|
+
const payload2 = consentPayload({ binding_digest: bindingDigest(p.act), role: p.role, decision: "approve", signed_at, kid: cred.kid });
|
|
6647
|
+
const challenge = toBase64url2(sha256(jcs(payload2)));
|
|
6648
|
+
if (Object.keys(p.attempts).length >= MAX_ATTEMPTS) return { status: 429, body: { error: "Too many approval ceremonies for this request." } };
|
|
6649
|
+
const expires_at = new Date(Math.min(Date.parse(p.act.expires_at), now().getTime() + 5 * 6e4)).toISOString();
|
|
6650
|
+
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." } };
|
|
6651
|
+
return { status: 200, body: { challenge, rpId, allowCredentials: [{ type: "public-key", id: cred.credential_id }], userVerification: "required", timeout: 12e4 } };
|
|
6652
|
+
}
|
|
6653
|
+
let offered;
|
|
6654
|
+
try {
|
|
6655
|
+
offered = JSON.parse(fromUtf82(fromBase64url2(String(req.client_data_json ?? "")))).challenge;
|
|
6656
|
+
} catch {
|
|
6657
|
+
return { status: 400, body: { error: "unreadable client data" } };
|
|
6658
|
+
}
|
|
6659
|
+
const attempt = p.attempts[offered];
|
|
6660
|
+
if (!attempt || attempt.credential_id !== cred.credential_id) return { status: 400, body: { error: "This ceremony was not offered for this passkey." } };
|
|
6661
|
+
if (Date.parse(attempt.expires_at) <= now().getTime()) return { status: 400, body: { error: "This ceremony has expired; start again." } };
|
|
6662
|
+
const consent = {
|
|
6663
|
+
binding_digest: bindingDigest(p.act),
|
|
6664
|
+
role: p.role,
|
|
6665
|
+
decision: "approve",
|
|
6666
|
+
signed_at: attempt.signed_at,
|
|
6667
|
+
kid: cred.kid,
|
|
6668
|
+
algorithm: "WebAuthn-ES256",
|
|
6669
|
+
authenticator_data: String(req.authenticator_data ?? ""),
|
|
6670
|
+
client_data_json: String(req.client_data_json ?? ""),
|
|
6671
|
+
signature: String(req.signature ?? "")
|
|
6672
|
+
};
|
|
6673
|
+
const payload = jcs(consentPayload(consent));
|
|
6674
|
+
const cd = clientData(consent.client_data_json, "webauthn.get", toBase64url2(sha256(payload)));
|
|
6675
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6676
|
+
const ad = fromBase64url2(consent.authenticator_data);
|
|
6677
|
+
if (ad.length < 37 || !equal2(ad.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The assertion is for another relying-party id." } };
|
|
6678
|
+
const v = verifySuite("WebAuthn-ES256", cred.public_key_pem, payload, consent);
|
|
6679
|
+
if (!v.ok) return { status: 400, body: { error: `The passkey's signature does not verify (${v.reason}).` } };
|
|
6680
|
+
const key_certs = await o.registry.chainFor(cred.kid);
|
|
6681
|
+
if (!key_certs.length) return { status: 409, body: { error: "This passkey is no longer certified (revoked or expired)." } };
|
|
6682
|
+
if (!await store.resolvePending(p.id, { status: "approved", consent, key_certs })) return { status: 409, body: { error: "This request is no longer pending." } };
|
|
6683
|
+
return { status: 200, body: { status: "approved" } };
|
|
6684
|
+
}
|
|
6685
|
+
if (method === "GET" && path === "/enroll") return { status: 200, type: "text/html", body: enrolPage(o.rpName ?? rpId) };
|
|
6686
|
+
if (method === "POST" && (path === "/nomos/enrol/begin" || path === "/nomos/enrol/finish")) {
|
|
6687
|
+
const req = await json();
|
|
6688
|
+
const claims = verifyEnrolmentToken(String(req?.token ?? ""), o.enrolmentAuthority, base, now());
|
|
6689
|
+
if ("error" in claims) return { status: 403, body: { error: claims.error } };
|
|
6690
|
+
if (!claims.roles.every((r) => o.roles.includes(r))) return { status: 403, body: { error: `This service does not enrol for ${claims.roles.join(", ")}.` } };
|
|
6691
|
+
if (path === "/nomos/enrol/begin") {
|
|
6692
|
+
if (await store.tokenUsed(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6693
|
+
const challenge2 = toBase64url2(randomBytes3(32));
|
|
6694
|
+
await store.saveEnrolmentChallenge(claims.nonce, challenge2);
|
|
6695
|
+
const existing = (await store.credentials()).filter((c) => c.approver === claims.approver);
|
|
6696
|
+
return { status: 200, body: {
|
|
6697
|
+
challenge: challenge2,
|
|
6698
|
+
rp: { id: rpId, name: o.rpName ?? rpId },
|
|
6699
|
+
user: { id: toBase64url2(utf82(claims.approver)), name: claims.approver, displayName: claims.approver },
|
|
6700
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
6701
|
+
authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
|
|
6702
|
+
attestation: "none",
|
|
6703
|
+
timeout: 12e4,
|
|
6704
|
+
excludeCredentials: existing.map((c) => ({ type: "public-key", id: c.credential_id })),
|
|
6705
|
+
approver: claims.approver,
|
|
6706
|
+
roles: claims.roles
|
|
6707
|
+
} };
|
|
6708
|
+
}
|
|
6709
|
+
const challenge = await store.enrolmentChallenge(claims.nonce);
|
|
6710
|
+
if (!challenge) return { status: 403, body: { error: "Start enrolment again: this link has been used or has no ceremony in progress." } };
|
|
6711
|
+
const cd = clientData(String(req.client_data_json ?? ""), "webauthn.create", challenge);
|
|
6712
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6713
|
+
let att;
|
|
6714
|
+
try {
|
|
6715
|
+
att = readAttestation(fromBase64url2(String(req.attestation_object ?? "")));
|
|
6716
|
+
} catch (e) {
|
|
6717
|
+
return { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6718
|
+
}
|
|
6719
|
+
if (!equal2(att.authData.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The passkey was created for another relying-party id." } };
|
|
6720
|
+
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)." } };
|
|
6721
|
+
if (toBase64url2(att.credentialId) !== req.credential_id) return { status: 400, body: { error: "The credential id does not match the attestation." } };
|
|
6722
|
+
if (!await store.claimToken(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6723
|
+
const kid = kidOf(att.publicKeyPem);
|
|
6724
|
+
const certificate = await o.registry.issue({
|
|
6725
|
+
parent: o.enroller,
|
|
6726
|
+
child: { kid, publicKeyPem: att.publicKeyPem },
|
|
6727
|
+
scope: `consent:${claims.roles.join(",")} approver:${claims.approver} suite:WebAuthn-ES256`,
|
|
6728
|
+
days: o.certificateDays ?? 365
|
|
6729
|
+
});
|
|
6730
|
+
const label = String(req.label ?? "").slice(0, 60) || "Passkey";
|
|
6731
|
+
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() });
|
|
6732
|
+
return { status: 200, body: { enrolled: true, approver: claims.approver, roles: claims.roles, kid, certificate } };
|
|
6733
|
+
}
|
|
6734
|
+
return null;
|
|
6735
|
+
}
|
|
6736
|
+
async function fetchHandler(request) {
|
|
6737
|
+
const out = await route(request.method, new URL(request.url).pathname, () => request.json()).catch((e) => ({ status: 400, body: { error: String(e?.message ?? e) } }));
|
|
6738
|
+
if (!out) return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });
|
|
6739
|
+
const html = out.type === "text/html";
|
|
6740
|
+
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() } : {} } });
|
|
6741
|
+
}
|
|
6742
|
+
function node() {
|
|
6743
|
+
return async (req, res, next) => {
|
|
6744
|
+
const path = String(req.originalUrl ?? req.url ?? "").split("?")[0];
|
|
6745
|
+
const json = async () => {
|
|
6746
|
+
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
6747
|
+
const chunks = [];
|
|
6748
|
+
for await (const c of req) chunks.push(c);
|
|
6749
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
6750
|
+
};
|
|
6751
|
+
let out;
|
|
6752
|
+
try {
|
|
6753
|
+
out = await route(req.method, path, json);
|
|
6754
|
+
} catch (e) {
|
|
6755
|
+
out = { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6756
|
+
}
|
|
6757
|
+
if (!out) {
|
|
6758
|
+
if (next) return next();
|
|
6759
|
+
res.statusCode = 404;
|
|
6760
|
+
return res.end(JSON.stringify({ error: "Not found" }));
|
|
6761
|
+
}
|
|
6762
|
+
res.statusCode = out.status;
|
|
6763
|
+
const html = out.type === "text/html";
|
|
6764
|
+
res.setHeader("content-type", html ? "text/html; charset=utf-8" : "application/json");
|
|
6765
|
+
if (html) res.setHeader("content-security-policy", CSP());
|
|
6766
|
+
res.end(html ? String(out.body) : JSON.stringify(out.body));
|
|
6767
|
+
};
|
|
6768
|
+
}
|
|
6769
|
+
return { store, fetch: fetchHandler, express: node, node };
|
|
6770
|
+
}
|
|
6771
|
+
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}
|
|
6772
|
+
@media (prefers-color-scheme:dark){body{color:#eee;background:#111}table td{border-color:#333}}
|
|
6773
|
+
h2{margin:0 0 12px}table{border-collapse:collapse;margin:12px 0}td{padding:6px 10px;border-bottom:1px solid #ddd}
|
|
6774
|
+
button{font:inherit;padding:10px 18px;border-radius:8px;border:1px solid #888;background:transparent;color:inherit;cursor:pointer;margin:4px 8px 4px 0}
|
|
6775
|
+
button.primary{background:#2563eb;border-color:#2563eb;color:#fff}.muted{color:#777;font-size:13px}#msg{margin-top:16px;font-weight:600}`;
|
|
6776
|
+
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))};
|
|
6777
|
+
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(()=>({}))}));
|
|
6778
|
+
const say=t=>{document.getElementById('msg').textContent=t};`;
|
|
6779
|
+
function approvalPage(p, status, devices) {
|
|
6780
|
+
const a = p.act.action ?? {};
|
|
6781
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc(k)}</td><td><b>${esc(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
6782
|
+
const device = (d) => `<button class="primary" data-cred="${esc(d.credential_id)}">Approve${d.approver ? ` as ${esc(d.approver)}` : ""} \xB7 ${esc(d.label)}</button>`;
|
|
6783
|
+
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>`;
|
|
6784
|
+
const body = status === "pending" ? `${choose}<button id="refuse">Refuse</button>` : `<p><b>${esc(status)}</b></p>`;
|
|
6785
|
+
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>
|
|
6786
|
+
<h2>Approve as ${esc(p.role)}?</h2>
|
|
6787
|
+
<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>
|
|
6788
|
+
<table>${params}</table>
|
|
6789
|
+
<p class="muted">Your passkey signs this exact change and nothing else. It expires at ${esc(p.act.expires_at)}.</p>
|
|
6790
|
+
${body}<div id="msg" data-id="${esc(p.id)}"></div>
|
|
6791
|
+
<script>${APPROVE_JS}</script>`;
|
|
6792
|
+
}
|
|
6793
|
+
var APPROVE_JS = `${B642}
|
|
6794
|
+
const id=document.getElementById('msg').dataset.id;
|
|
6795
|
+
async function approve(credential_id){
|
|
6796
|
+
try{
|
|
6797
|
+
const o=await post('/approve/'+id+'/options',{credential_id});
|
|
6798
|
+
if(!o.ok) return say(o.body.error||'Not allowed.');
|
|
6799
|
+
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}});
|
|
6800
|
+
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)});
|
|
6801
|
+
say(r.ok?'Approved. You can close this page.':(r.body.error||'Not approved.'));
|
|
6802
|
+
if(r.ok) document.querySelectorAll('button').forEach(b=>b.disabled=true);
|
|
6803
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6804
|
+
}
|
|
6805
|
+
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)}
|
|
6806
|
+
function wire(root){root.querySelectorAll('[data-cred]').forEach(b=>b.addEventListener('click',()=>approve(b.dataset.cred)))}
|
|
6807
|
+
wire(document);
|
|
6808
|
+
document.getElementById('refuse')?.addEventListener('click',refuse);
|
|
6809
|
+
document.getElementById('find')?.addEventListener('click',async()=>{
|
|
6810
|
+
const r=await post('/approve/'+id+'/devices',{approver:document.getElementById('who').value});
|
|
6811
|
+
const box=document.getElementById('devices');box.textContent='';
|
|
6812
|
+
if(!r.body.devices?.length) return say('No passkey of yours approves this.');
|
|
6813
|
+
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)}
|
|
6814
|
+
wire(box);
|
|
6815
|
+
});`;
|
|
6816
|
+
function enrolPage(rpName) {
|
|
6817
|
+
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>
|
|
6818
|
+
<h2>Enrol a passkey for ${esc(rpName)}</h2>
|
|
6819
|
+
<p id="who" class="muted">Checking your enrolment link\u2026</p>
|
|
6820
|
+
<p><label>Name this device <input id="label" value="" placeholder="e.g. iPhone"></label></p>
|
|
6821
|
+
<button class="primary" id="go" disabled>Create passkey</button>
|
|
6822
|
+
<div id="msg"></div>
|
|
6823
|
+
<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>
|
|
6824
|
+
<script>${ENROL_JS}</script>`;
|
|
6825
|
+
}
|
|
6826
|
+
var ENROL_JS = `${B642}
|
|
6827
|
+
const token=location.hash.slice(1);let opts=null;
|
|
6828
|
+
(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}
|
|
6829
|
+
opts=r.body;document.getElementById('who').textContent='Enrolling '+opts.approver+' to approve as '+opts.roles.join(', ')+'.';document.getElementById('go').disabled=false})();
|
|
6830
|
+
async function enrol(){
|
|
6831
|
+
try{
|
|
6832
|
+
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)}))}});
|
|
6833
|
+
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)});
|
|
6834
|
+
say(r.ok?'Enrolled. This device can now approve as '+r.body.roles.join(', ')+'.':(r.body.error||'Not enrolled.'));
|
|
6835
|
+
if(r.ok) document.getElementById('go').disabled=true;
|
|
6836
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6837
|
+
}
|
|
6838
|
+
document.getElementById('go').addEventListener('click',enrol);`;
|
|
6839
|
+
var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js))))}'`;
|
|
6840
|
+
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'`;
|
|
6841
|
+
|
|
5765
6842
|
// src/act/carrier.ts
|
|
5766
6843
|
var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
|
|
5767
6844
|
var NomosActError = class extends Error {
|
|
@@ -5798,6 +6875,7 @@ async function act(o) {
|
|
|
5798
6875
|
}
|
|
5799
6876
|
}
|
|
5800
6877
|
const testimony = [];
|
|
6878
|
+
const available = [...issuer2.key_certs ?? []];
|
|
5801
6879
|
const unavailable = [];
|
|
5802
6880
|
for (const claim of witnessed) {
|
|
5803
6881
|
const w = witnesses.find((x) => x.claims.includes(claim));
|
|
@@ -5821,8 +6899,10 @@ async function act(o) {
|
|
|
5821
6899
|
continue;
|
|
5822
6900
|
}
|
|
5823
6901
|
testimony.push(t);
|
|
6902
|
+
if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
|
|
5824
6903
|
}
|
|
5825
|
-
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts:
|
|
6904
|
+
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
|
|
6905
|
+
present(theAct, available);
|
|
5826
6906
|
let { status, body } = await submit(f, rp.act_endpoint, theAct);
|
|
5827
6907
|
let approvalUrl;
|
|
5828
6908
|
while (status === 202 && body.decision === "ESCALATED") {
|
|
@@ -5830,16 +6910,18 @@ async function act(o) {
|
|
|
5830
6910
|
const endpoint = (issuer2.approvals ?? []).find((a) => a.role === role)?.endpoint;
|
|
5831
6911
|
if (!endpoint) return done("escalated", body, theAct, { summary: `Needs approval from ${role}, and ${issuer2.issuer_id} publishes no approval endpoint for that role.` });
|
|
5832
6912
|
const deadline = Math.min(Date.parse(theAct.expires_at), Date.now() + (o.approvalTimeoutMs ?? Infinity));
|
|
5833
|
-
const
|
|
6913
|
+
const answer2 = await requestConsent(f, endpoint, role, theAct, deadline, o.pollMs ?? 1e3, (url) => {
|
|
5834
6914
|
approvalUrl = url;
|
|
5835
6915
|
o.onApproval?.({ role, url, actDigest: body.act_digest });
|
|
5836
6916
|
});
|
|
5837
|
-
if (
|
|
5838
|
-
if (!
|
|
6917
|
+
if (answer2 === "refused") return done("denied", body, theAct, { summary: `The ${role} refused this action.` });
|
|
6918
|
+
if (!answer2) {
|
|
5839
6919
|
const expired = Date.now() >= Date.parse(theAct.expires_at);
|
|
5840
6920
|
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
6921
|
}
|
|
5842
|
-
theAct.consents.push(consent);
|
|
6922
|
+
theAct.consents.push(answer2.consent);
|
|
6923
|
+
if (Array.isArray(answer2.keyCerts)) available.push(...answer2.keyCerts);
|
|
6924
|
+
present(theAct, available);
|
|
5843
6925
|
const before = Number(body.consents_valid ?? 0);
|
|
5844
6926
|
({ status, body } = await submit(f, rp.act_endpoint, theAct));
|
|
5845
6927
|
if (status === 202 && Number(body.consents_valid ?? 0) <= before) {
|
|
@@ -5860,6 +6942,13 @@ async function act(o) {
|
|
|
5860
6942
|
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
6943
|
});
|
|
5862
6944
|
}
|
|
6945
|
+
function present(theAct, available) {
|
|
6946
|
+
const set = presentedCertificates(available, theAct);
|
|
6947
|
+
if (set.length > MAX_PRESENTED) {
|
|
6948
|
+
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.`);
|
|
6949
|
+
}
|
|
6950
|
+
theAct.authority.key_certs = set;
|
|
6951
|
+
}
|
|
5863
6952
|
function done(status, verdict, theAct, extra) {
|
|
5864
6953
|
return { status, verdict, act: theAct, ...extra };
|
|
5865
6954
|
}
|
|
@@ -5877,13 +6966,13 @@ async function submit(f, endpoint, theAct) {
|
|
|
5877
6966
|
async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPending) {
|
|
5878
6967
|
const res = await f(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ role, act: { ...theAct, consents: [] } }) });
|
|
5879
6968
|
const body = await res.json().catch(() => ({}));
|
|
5880
|
-
if (res.status === 200 && body.status === "approved") return body.consent;
|
|
6969
|
+
if (res.status === 200 && body.status === "approved") return { consent: body.consent, keyCerts: body.key_certs };
|
|
5881
6970
|
if (res.status !== 202 || !body.poll_url) return null;
|
|
5882
6971
|
onPending(body.approval_url);
|
|
5883
6972
|
while (Date.now() < deadline) {
|
|
5884
6973
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
5885
6974
|
const p = await (await f(body.poll_url)).json().catch(() => ({}));
|
|
5886
|
-
if (p.status === "approved") return p.consent;
|
|
6975
|
+
if (p.status === "approved") return { consent: p.consent, keyCerts: p.key_certs };
|
|
5887
6976
|
if (p.status === "refused") return "refused";
|
|
5888
6977
|
if (p.status === "expired") return null;
|
|
5889
6978
|
}
|
|
@@ -5891,7 +6980,6 @@ async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPen
|
|
|
5891
6980
|
}
|
|
5892
6981
|
|
|
5893
6982
|
// src/act/roles.ts
|
|
5894
|
-
var import_node_crypto3 = require("crypto");
|
|
5895
6983
|
function serve(route) {
|
|
5896
6984
|
async function fetchHandler(request) {
|
|
5897
6985
|
const out = await route({ method: request.method, path: new URL(request.url).pathname, json: () => request.json() });
|
|
@@ -5906,7 +6994,7 @@ function serve(route) {
|
|
|
5906
6994
|
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
5907
6995
|
const chunks = [];
|
|
5908
6996
|
for await (const c of req) chunks.push(c);
|
|
5909
|
-
return JSON.parse(
|
|
6997
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
5910
6998
|
};
|
|
5911
6999
|
let out;
|
|
5912
7000
|
try {
|
|
@@ -5930,7 +7018,9 @@ var day = 864e5;
|
|
|
5930
7018
|
function issuer(o) {
|
|
5931
7019
|
const byId = new Map(o.authorities.map((a) => [a.meta.artifact_id, a]));
|
|
5932
7020
|
const base = o.origin.replace(/\/$/, "");
|
|
5933
|
-
const
|
|
7021
|
+
const pool = async () => typeof o.keyCerts === "function" ? await o.keyCerts() : o.keyCerts;
|
|
7022
|
+
const revocationsUrl = o.revocations ? `${base}/nomos/revocations` : o.revocationsUrl;
|
|
7023
|
+
const build = (keyCerts) => ({
|
|
5934
7024
|
nomos_document: "issuer",
|
|
5935
7025
|
version: "1",
|
|
5936
7026
|
issuer_id: o.issuerId,
|
|
@@ -5941,17 +7031,24 @@ function issuer(o) {
|
|
|
5941
7031
|
artifact_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}.nomos`,
|
|
5942
7032
|
in_force_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}/in-force`
|
|
5943
7033
|
})),
|
|
5944
|
-
key_certs:
|
|
7034
|
+
key_certs: keyCerts,
|
|
5945
7035
|
...o.witnesses ? { witnesses: o.witnesses } : {},
|
|
5946
7036
|
...o.approvals ? { approvals: o.approvals } : {},
|
|
5947
|
-
...
|
|
7037
|
+
...revocationsUrl ? { revocations_url: revocationsUrl } : {},
|
|
5948
7038
|
valid_until: new Date(Date.now() + day).toISOString()
|
|
5949
7039
|
});
|
|
7040
|
+
const document = () => {
|
|
7041
|
+
if (typeof o.keyCerts === "function") throw new Error("issuer(): keyCerts is a function, so the document must be read with `await resolveDocument()`");
|
|
7042
|
+
return build(o.keyCerts);
|
|
7043
|
+
};
|
|
7044
|
+
const resolveDocument = async () => build(await pool());
|
|
5950
7045
|
return {
|
|
5951
7046
|
document,
|
|
7047
|
+
resolveDocument,
|
|
5952
7048
|
...serve(async ({ method, path }) => {
|
|
5953
7049
|
if (method !== "GET") return null;
|
|
5954
|
-
if (path === WELL_KNOWN.issuer) return { status: 200, body:
|
|
7050
|
+
if (path === WELL_KNOWN.issuer) return { status: 200, body: await resolveDocument() };
|
|
7051
|
+
if (path === "/nomos/revocations" && o.revocations) return { status: 200, body: await o.revocations() };
|
|
5955
7052
|
const m = path.match(/^\/nomos\/authorities\/([^/]+?)(\.nomos|\/in-force)$/);
|
|
5956
7053
|
const art = m && byId.get(decodeURIComponent(m[1]));
|
|
5957
7054
|
if (!art) return null;
|
|
@@ -5986,13 +7083,14 @@ function witness(o) {
|
|
|
5986
7083
|
if (value2 === void 0) return { status: 404, body: { error: `${o.witnessId} knows nothing about ${about}.` } };
|
|
5987
7084
|
const max = o.maxValiditySeconds ?? 900;
|
|
5988
7085
|
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) }) } };
|
|
7086
|
+
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
7087
|
})
|
|
5991
7088
|
};
|
|
5992
7089
|
}
|
|
5993
7090
|
function approver(o) {
|
|
5994
7091
|
const base = o.origin.replace(/\/$/, "");
|
|
5995
7092
|
const pending = /* @__PURE__ */ new Map();
|
|
7093
|
+
const chain = o.keyCerts?.length ? { key_certs: o.keyCerts } : {};
|
|
5996
7094
|
const statusOf = (p) => p.status === "pending" && Date.parse(p.act.expires_at) < Date.now() ? "expired" : p.status;
|
|
5997
7095
|
return {
|
|
5998
7096
|
pending,
|
|
@@ -6002,9 +7100,9 @@ function approver(o) {
|
|
|
6002
7100
|
if (!o.roles.includes(role)) return { status: 404, body: { error: `This approver does not consent as ${role}.` } };
|
|
6003
7101
|
if (o.decide === "auto" || typeof o.decide === "function") {
|
|
6004
7102
|
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" } };
|
|
7103
|
+
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role), ...chain } } : { status: 200, body: { status: "refused" } };
|
|
6006
7104
|
}
|
|
6007
|
-
const id =
|
|
7105
|
+
const id = randomUUID();
|
|
6008
7106
|
pending.set(id, { role, act: act2, status: "pending" });
|
|
6009
7107
|
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
6010
7108
|
}
|
|
@@ -6013,7 +7111,7 @@ function approver(o) {
|
|
|
6013
7111
|
const p = pending.get(m[1]);
|
|
6014
7112
|
if (!p) return null;
|
|
6015
7113
|
const s = statusOf(p);
|
|
6016
|
-
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent } : {} } };
|
|
7114
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...chain } : {} } };
|
|
6017
7115
|
}
|
|
6018
7116
|
m = path.match(/^\/approve\/([0-9a-f-]+)(\/(approve|refuse))?$/);
|
|
6019
7117
|
if (m) {
|
|
@@ -6033,23 +7131,24 @@ function approver(o) {
|
|
|
6033
7131
|
})
|
|
6034
7132
|
};
|
|
6035
7133
|
}
|
|
6036
|
-
var
|
|
7134
|
+
var esc2 = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6037
7135
|
function page(p, status, id) {
|
|
6038
7136
|
const a = p.act.action ?? {};
|
|
6039
|
-
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${
|
|
7137
|
+
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
7138
|
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>${
|
|
6042
|
-
return `<!doctype html><meta charset="utf-8"><title>Approve: ${
|
|
7139
|
+
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${esc2(status)}</b></p>`;
|
|
7140
|
+
return `<!doctype html><meta charset="utf-8"><title>Approve: ${esc2(a.type)}</title>
|
|
6043
7141
|
<body style="font:15px system-ui;max-width:560px;margin:40px auto;padding:0 16px">
|
|
6044
|
-
<h2>Approve as ${
|
|
6045
|
-
<p><b>${
|
|
7142
|
+
<h2>Approve as ${esc2(p.role)}?</h2>
|
|
7143
|
+
<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
7144
|
<table style="border-collapse:collapse" cellpadding="4">${params}</table>
|
|
6047
|
-
<p style="color:#666">Your approval signs this exact change (digest ${
|
|
7145
|
+
<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
7146
|
${buttons}</body>`;
|
|
6049
7147
|
}
|
|
6050
7148
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6051
7149
|
0 && (module.exports = {
|
|
6052
7150
|
ACT_MEDIA_TYPE,
|
|
7151
|
+
MAX_PRESENTED,
|
|
6053
7152
|
Nomos,
|
|
6054
7153
|
NomosAPIError,
|
|
6055
7154
|
NomosActError,
|
|
@@ -6064,31 +7163,53 @@ ${buttons}</body>`;
|
|
|
6064
7163
|
NomosRateLimitError,
|
|
6065
7164
|
NomosValidationError,
|
|
6066
7165
|
NomosWebhookSignatureError,
|
|
7166
|
+
RegistryError,
|
|
7167
|
+
RevocationUnavailable,
|
|
6067
7168
|
WELL_KNOWN,
|
|
6068
7169
|
accept,
|
|
6069
7170
|
act,
|
|
7171
|
+
applyToolBindings,
|
|
6070
7172
|
approver,
|
|
6071
7173
|
bindingDigest,
|
|
6072
7174
|
buildAct,
|
|
6073
7175
|
can,
|
|
7176
|
+
certificateRegistry,
|
|
6074
7177
|
certify,
|
|
7178
|
+
describeSuggestion,
|
|
6075
7179
|
fetchDocument,
|
|
6076
7180
|
generateSigner,
|
|
6077
7181
|
issuer,
|
|
6078
7182
|
kidOf,
|
|
6079
7183
|
localSigner,
|
|
7184
|
+
memoryPasskeyStore,
|
|
7185
|
+
memoryRegistryStore,
|
|
6080
7186
|
memoryStore,
|
|
6081
7187
|
nomos,
|
|
6082
7188
|
originOf,
|
|
7189
|
+
parseToolBindings,
|
|
7190
|
+
passkeyApprover,
|
|
7191
|
+
postgresPasskeyStore,
|
|
7192
|
+
postgresRegistryStore,
|
|
7193
|
+
postgresStore,
|
|
7194
|
+
presentedCertificates,
|
|
7195
|
+
readAttestation,
|
|
7196
|
+
reliedKids,
|
|
7197
|
+
revocationListCertificates,
|
|
7198
|
+
revocationSource,
|
|
6083
7199
|
seal,
|
|
6084
7200
|
seededSigner,
|
|
6085
7201
|
signConsent,
|
|
7202
|
+
signEnrolmentToken,
|
|
6086
7203
|
signInForce,
|
|
7204
|
+
signRevocation,
|
|
7205
|
+
signRevocationList,
|
|
6087
7206
|
signTestimony,
|
|
6088
7207
|
statusFor,
|
|
7208
|
+
suggestToolBindings,
|
|
6089
7209
|
verifyAct,
|
|
6090
7210
|
verifyArtifact,
|
|
6091
7211
|
verifyChain,
|
|
7212
|
+
verifyEnrolmentToken,
|
|
6092
7213
|
verifySeal,
|
|
6093
7214
|
witness
|
|
6094
7215
|
});
|