@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.js
CHANGED
|
@@ -107,7 +107,7 @@ function isRetryable(status) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// src/http-client.ts
|
|
110
|
-
var SDK_VERSION = "
|
|
110
|
+
var SDK_VERSION = "2.3.1";
|
|
111
111
|
var HttpClient = class {
|
|
112
112
|
constructor(opts) {
|
|
113
113
|
this.opts = opts;
|
|
@@ -772,7 +772,7 @@ async function canAuthenticated(http, params, opts) {
|
|
|
772
772
|
var nomos = { can };
|
|
773
773
|
|
|
774
774
|
// src/nomos.ts
|
|
775
|
-
var DEFAULT_BASE_URL = "https://
|
|
775
|
+
var DEFAULT_BASE_URL = "https://www.nomosprotocol.com";
|
|
776
776
|
var DEFAULT_MAX_RETRIES = 3;
|
|
777
777
|
var DEFAULT_TIMEOUT = 3e4;
|
|
778
778
|
var Nomos = class {
|
|
@@ -782,7 +782,7 @@ var Nomos = class {
|
|
|
782
782
|
constructor(apiKey, config = {}) {
|
|
783
783
|
if (!apiKey || typeof apiKey !== "string") {
|
|
784
784
|
throw new NomosAuthenticationError(
|
|
785
|
-
'A valid API key is required. Pass it as the first argument: new Nomos("
|
|
785
|
+
'A valid API key is required. Pass it as the first argument: new Nomos("nmk_...")'
|
|
786
786
|
);
|
|
787
787
|
}
|
|
788
788
|
this.mode = apiKey.startsWith("nms_test_") ? "test" : "live";
|
|
@@ -804,7 +804,7 @@ var Nomos = class {
|
|
|
804
804
|
* return shape either way, so calling code never needs to know which rail answered.
|
|
805
805
|
*
|
|
806
806
|
* @example
|
|
807
|
-
* const client = new Nomos('
|
|
807
|
+
* const client = new Nomos('nmk_...');
|
|
808
808
|
* const verdict = await client.can({ artifact_id: 'loan_approval_v1', credit_score: 720 });
|
|
809
809
|
*/
|
|
810
810
|
async can(params, opts) {
|
|
@@ -812,7 +812,7 @@ var Nomos = class {
|
|
|
812
812
|
}
|
|
813
813
|
};
|
|
814
814
|
|
|
815
|
-
//
|
|
815
|
+
// node_modules/@noble/hashes/_u64.js
|
|
816
816
|
var U32_MASK64 = /* @__PURE__ */ (() => BigInt(2 ** 32 - 1))();
|
|
817
817
|
var _32n = /* @__PURE__ */ BigInt(32);
|
|
818
818
|
function fromBig(n, le = false) {
|
|
@@ -855,7 +855,7 @@ var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0
|
|
|
855
855
|
var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
|
856
856
|
var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
857
857
|
|
|
858
|
-
//
|
|
858
|
+
// node_modules/@noble/hashes/utils.js
|
|
859
859
|
function isBytes(a) {
|
|
860
860
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
861
861
|
}
|
|
@@ -1020,7 +1020,7 @@ var oidNist = (suffix) => ({
|
|
|
1020
1020
|
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
1021
1021
|
});
|
|
1022
1022
|
|
|
1023
|
-
//
|
|
1023
|
+
// node_modules/@noble/hashes/_md.js
|
|
1024
1024
|
function Chi(a, b, c) {
|
|
1025
1025
|
return a & b ^ ~a & c;
|
|
1026
1026
|
}
|
|
@@ -1151,7 +1151,7 @@ var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
|
1151
1151
|
327033209
|
|
1152
1152
|
]);
|
|
1153
1153
|
|
|
1154
|
-
//
|
|
1154
|
+
// node_modules/@noble/hashes/sha2.js
|
|
1155
1155
|
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
1156
1156
|
1116352408,
|
|
1157
1157
|
1899447441,
|
|
@@ -1548,7 +1548,7 @@ var sha512 = /* @__PURE__ */ createHasher(
|
|
|
1548
1548
|
/* @__PURE__ */ oidNist(3)
|
|
1549
1549
|
);
|
|
1550
1550
|
|
|
1551
|
-
//
|
|
1551
|
+
// node_modules/@nomosprotocol/verify/dist/bytes.js
|
|
1552
1552
|
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1553
1553
|
var REV = new Map([...B64].map((c, i) => [c, i]));
|
|
1554
1554
|
var utf8 = (s) => new TextEncoder().encode(s);
|
|
@@ -1610,7 +1610,7 @@ function fromBase64url(s) {
|
|
|
1610
1610
|
return raw && toBase64url(raw) === s ? raw : null;
|
|
1611
1611
|
}
|
|
1612
1612
|
|
|
1613
|
-
//
|
|
1613
|
+
// node_modules/@nomosprotocol/verify/dist/jcs.js
|
|
1614
1614
|
function value(v) {
|
|
1615
1615
|
if (v === null)
|
|
1616
1616
|
return "null";
|
|
@@ -1644,7 +1644,7 @@ function codePointCompare(a, b) {
|
|
|
1644
1644
|
return A.length - B.length;
|
|
1645
1645
|
}
|
|
1646
1646
|
|
|
1647
|
-
//
|
|
1647
|
+
// node_modules/@noble/curves/utils.js
|
|
1648
1648
|
function aarray(item, title, inner = () => {
|
|
1649
1649
|
}) {
|
|
1650
1650
|
if (!Array.isArray(item))
|
|
@@ -1826,7 +1826,7 @@ function validateObject(object, fields = {}, optFields = {}, title = "object") {
|
|
|
1826
1826
|
iter(optFields, true);
|
|
1827
1827
|
}
|
|
1828
1828
|
|
|
1829
|
-
//
|
|
1829
|
+
// node_modules/@noble/curves/abstract/modular.js
|
|
1830
1830
|
var _0n2 = /* @__PURE__ */ BigInt(0);
|
|
1831
1831
|
var _1n2 = /* @__PURE__ */ BigInt(1);
|
|
1832
1832
|
var _2n = /* @__PURE__ */ BigInt(2);
|
|
@@ -2289,7 +2289,7 @@ function mapHashToField(key, fieldOrder, isLE = false) {
|
|
|
2289
2289
|
return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
|
|
2290
2290
|
}
|
|
2291
2291
|
|
|
2292
|
-
//
|
|
2292
|
+
// node_modules/@noble/curves/abstract/curve.js
|
|
2293
2293
|
var _0n3 = /* @__PURE__ */ BigInt(0);
|
|
2294
2294
|
var _1n3 = /* @__PURE__ */ BigInt(1);
|
|
2295
2295
|
var _4n2 = /* @__PURE__ */ BigInt(4);
|
|
@@ -2676,7 +2676,7 @@ function createKeygen(randomSecretKey, getPublicKey) {
|
|
|
2676
2676
|
};
|
|
2677
2677
|
}
|
|
2678
2678
|
|
|
2679
|
-
//
|
|
2679
|
+
// node_modules/@noble/curves/abstract/edwards.js
|
|
2680
2680
|
var _0n4 = /* @__PURE__ */ BigInt(0);
|
|
2681
2681
|
var _1n4 = /* @__PURE__ */ BigInt(1);
|
|
2682
2682
|
var _2n2 = /* @__PURE__ */ BigInt(2);
|
|
@@ -3127,7 +3127,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3127
3127
|
});
|
|
3128
3128
|
}
|
|
3129
3129
|
|
|
3130
|
-
//
|
|
3130
|
+
// node_modules/@noble/curves/ed25519.js
|
|
3131
3131
|
var _1n5 = /* @__PURE__ */ BigInt(1);
|
|
3132
3132
|
var _2n3 = /* @__PURE__ */ BigInt(2);
|
|
3133
3133
|
var _5n2 = /* @__PURE__ */ BigInt(5);
|
|
@@ -3202,7 +3202,7 @@ function ed(opts) {
|
|
|
3202
3202
|
}
|
|
3203
3203
|
var ed25519 = /* @__PURE__ */ ed({});
|
|
3204
3204
|
|
|
3205
|
-
//
|
|
3205
|
+
// node_modules/@noble/hashes/hmac.js
|
|
3206
3206
|
var _HMAC = class {
|
|
3207
3207
|
oHash;
|
|
3208
3208
|
iHash;
|
|
@@ -3279,7 +3279,7 @@ var hmac = /* @__PURE__ */ (() => {
|
|
|
3279
3279
|
return hmac_;
|
|
3280
3280
|
})();
|
|
3281
3281
|
|
|
3282
|
-
//
|
|
3282
|
+
// node_modules/@noble/curves/abstract/der.js
|
|
3283
3283
|
var _0n5 = /* @__PURE__ */ BigInt(0);
|
|
3284
3284
|
var DERErr = class extends Error {
|
|
3285
3285
|
constructor(m = "") {
|
|
@@ -3373,18 +3373,18 @@ var _DER = {
|
|
|
3373
3373
|
}
|
|
3374
3374
|
},
|
|
3375
3375
|
toSig(bytes, maxScalarBytes) {
|
|
3376
|
-
const { Err: E, _int: int, _tlv:
|
|
3376
|
+
const { Err: E, _int: int, _tlv: tlv2 } = _DER;
|
|
3377
3377
|
if (maxScalarBytes !== void 0) {
|
|
3378
3378
|
asafenumber(maxScalarBytes, "maxScalarBytes");
|
|
3379
3379
|
if (maxScalarBytes < 1)
|
|
3380
3380
|
throw new E("invalid signature: maxScalarBytes must be positive");
|
|
3381
3381
|
}
|
|
3382
3382
|
const data = abytes2(bytes, void 0, "signature");
|
|
3383
|
-
const { v: seqBytes, l: seqLeftBytes } =
|
|
3383
|
+
const { v: seqBytes, l: seqLeftBytes } = tlv2.decode(48, data);
|
|
3384
3384
|
if (seqLeftBytes.length)
|
|
3385
3385
|
throw new E("invalid signature: left bytes after parsing");
|
|
3386
|
-
const { v: rBytes, l: rLeftBytes } =
|
|
3387
|
-
const { v: sBytes, l: sLeftBytes } =
|
|
3386
|
+
const { v: rBytes, l: rLeftBytes } = tlv2.decode(2, seqBytes);
|
|
3387
|
+
const { v: sBytes, l: sLeftBytes } = tlv2.decode(2, rLeftBytes);
|
|
3388
3388
|
if (sLeftBytes.length)
|
|
3389
3389
|
throw new E("invalid signature: left bytes after parsing");
|
|
3390
3390
|
if (maxScalarBytes !== void 0 && (rBytes.length > maxScalarBytes || sBytes.length > maxScalarBytes))
|
|
@@ -3392,12 +3392,12 @@ var _DER = {
|
|
|
3392
3392
|
return { r: int.decode(rBytes), s: int.decode(sBytes) };
|
|
3393
3393
|
},
|
|
3394
3394
|
hexFromSig(sig) {
|
|
3395
|
-
const { _tlv:
|
|
3395
|
+
const { _tlv: tlv2, _int: int } = _DER;
|
|
3396
3396
|
validateObject(sig, { r: "bigint", s: "bigint" }, {}, "sig");
|
|
3397
|
-
const rs =
|
|
3398
|
-
const ss =
|
|
3397
|
+
const rs = tlv2.encode(2, int.encode(sig.r));
|
|
3398
|
+
const ss = tlv2.encode(2, int.encode(sig.s));
|
|
3399
3399
|
const seq = rs + ss;
|
|
3400
|
-
return
|
|
3400
|
+
return tlv2.encode(48, seq);
|
|
3401
3401
|
}
|
|
3402
3402
|
};
|
|
3403
3403
|
var DER = /* @__PURE__ */ (() => {
|
|
@@ -3406,7 +3406,7 @@ var DER = /* @__PURE__ */ (() => {
|
|
|
3406
3406
|
return Object.freeze(_DER);
|
|
3407
3407
|
})();
|
|
3408
3408
|
|
|
3409
|
-
//
|
|
3409
|
+
// node_modules/@noble/curves/abstract/weierstrass.js
|
|
3410
3410
|
var divNearest = (num, den) => (num + (num >= 0 ? den : -den) / _2n4) / den;
|
|
3411
3411
|
function _splitEndoScalar(k, basis, n) {
|
|
3412
3412
|
aInRange("scalar", k, _0n6, n);
|
|
@@ -4238,7 +4238,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
4238
4238
|
});
|
|
4239
4239
|
}
|
|
4240
4240
|
|
|
4241
|
-
//
|
|
4241
|
+
// node_modules/@noble/curves/nist.js
|
|
4242
4242
|
var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
4243
4243
|
p: BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),
|
|
4244
4244
|
n: BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),
|
|
@@ -4251,21 +4251,21 @@ var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
|
4251
4251
|
var p256_Point = /* @__PURE__ */ weierstrass(p256_CURVE);
|
|
4252
4252
|
var p256 = /* @__PURE__ */ ecdsa(p256_Point, sha256);
|
|
4253
4253
|
|
|
4254
|
-
//
|
|
4254
|
+
// node_modules/@nomosprotocol/verify/dist/suites.js
|
|
4255
4255
|
var SUITES = ["Ed25519", "ES256", "WebAuthn-ES256"];
|
|
4256
4256
|
var N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
4257
4257
|
var ED25519_PREFIX = Uint8Array.from([48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]);
|
|
4258
4258
|
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]);
|
|
4259
4259
|
var SUITES_FOR = { "Ed25519": ["Ed25519"], "P-256": ["ES256", "WebAuthn-ES256"] };
|
|
4260
4260
|
var isSuite = (s) => typeof s === "string" && SUITES.includes(s);
|
|
4261
|
-
function pemDer(
|
|
4262
|
-
if (typeof
|
|
4261
|
+
function pemDer(pem2) {
|
|
4262
|
+
if (typeof pem2 !== "string")
|
|
4263
4263
|
return null;
|
|
4264
|
-
const m =
|
|
4264
|
+
const m = pem2.match(/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=\s]+?)\s*-----END PUBLIC KEY-----\s*$/);
|
|
4265
4265
|
return m ? fromBase64(m[1].replace(/\s+/g, "")) : null;
|
|
4266
4266
|
}
|
|
4267
|
-
function loadKey(
|
|
4268
|
-
const der = pemDer(
|
|
4267
|
+
function loadKey(pem2) {
|
|
4268
|
+
const der = pemDer(pem2);
|
|
4269
4269
|
if (!der)
|
|
4270
4270
|
return { reason: "invalid_key" };
|
|
4271
4271
|
if (der.length === 44 && equal(der.subarray(0, 12), ED25519_PREFIX)) {
|
|
@@ -4289,16 +4289,16 @@ function loadKey(pem) {
|
|
|
4289
4289
|
}
|
|
4290
4290
|
return { reason: "invalid_key" };
|
|
4291
4291
|
}
|
|
4292
|
-
function kidOf(
|
|
4293
|
-
const k = loadKey(
|
|
4292
|
+
function kidOf(pem2) {
|
|
4293
|
+
const k = loadKey(pem2);
|
|
4294
4294
|
return "reason" in k ? null : toBase64url(sha256(k.der)).slice(0, 16);
|
|
4295
4295
|
}
|
|
4296
4296
|
var toBigInt = (b) => b.reduce((n, x) => n << 8n | BigInt(x), 0n);
|
|
4297
|
-
function verifySuite(suite,
|
|
4297
|
+
function verifySuite(suite, pem2, payload, proof) {
|
|
4298
4298
|
const fail2 = (reason) => ({ ok: false, reason });
|
|
4299
4299
|
if (!isSuite(suite))
|
|
4300
4300
|
return fail2("unknown_suite");
|
|
4301
|
-
const k = loadKey(
|
|
4301
|
+
const k = loadKey(pem2);
|
|
4302
4302
|
if ("reason" in k)
|
|
4303
4303
|
return fail2("invalid_key");
|
|
4304
4304
|
if (!SUITES_FOR[k.type].includes(suite))
|
|
@@ -4360,7 +4360,7 @@ function verifySuite(suite, pem, payload, proof) {
|
|
|
4360
4360
|
return ok ? { ok: true } : fail2("bad_signature");
|
|
4361
4361
|
}
|
|
4362
4362
|
|
|
4363
|
-
//
|
|
4363
|
+
// node_modules/@nomosprotocol/verify/dist/scope.js
|
|
4364
4364
|
var ARTIFACT_DIMENSIONS = ["artifact", "industry", "jurisdiction"];
|
|
4365
4365
|
var SET_DIMENSIONS = ["claim", "consent", "suite"];
|
|
4366
4366
|
var UNRESTRICTED = { terms: {} };
|
|
@@ -4479,7 +4479,7 @@ function formatScope(scope) {
|
|
|
4479
4479
|
return Object.entries(scope.terms).map(([d, v]) => `${d}:${v}`).join(" ");
|
|
4480
4480
|
}
|
|
4481
4481
|
|
|
4482
|
-
//
|
|
4482
|
+
// node_modules/@nomosprotocol/verify/dist/chain.js
|
|
4483
4483
|
var MAX_CHAIN = 20;
|
|
4484
4484
|
var CERT_SUITES = ["Ed25519", "ES256"];
|
|
4485
4485
|
var SEAL_SUITES = ["Ed25519", "ES256"];
|
|
@@ -4571,7 +4571,7 @@ var Memo = class {
|
|
|
4571
4571
|
}
|
|
4572
4572
|
};
|
|
4573
4573
|
function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo) {
|
|
4574
|
-
let
|
|
4574
|
+
let pem2 = rootPem, scope = UNRESTRICTED;
|
|
4575
4575
|
const hops = [];
|
|
4576
4576
|
for (const cert of path) {
|
|
4577
4577
|
const f = memo.of(cert);
|
|
@@ -4581,7 +4581,7 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4581
4581
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: "kid_mismatch" });
|
|
4582
4582
|
if (!suitePermitted(scope, cert.algorithm))
|
|
4583
4583
|
return fail("OUT_OF_SCOPE", { dimension: "suite", reason_code: "suite_not_permitted" });
|
|
4584
|
-
const sig = memo.sig(cert,
|
|
4584
|
+
const sig = memo.sig(cert, pem2);
|
|
4585
4585
|
if (!sig.ok)
|
|
4586
4586
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: sig.reason });
|
|
4587
4587
|
const issued = parseTime(cert.issued_at), expires = parseTime(cert.expires_at);
|
|
@@ -4601,14 +4601,14 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4601
4601
|
const narrowing = isNarrowerOrEqual(f.scope.scope, scope);
|
|
4602
4602
|
if (!narrowing.ok)
|
|
4603
4603
|
return fail("OUT_OF_SCOPE", { dimension: narrowing.dimension, reason_code: "widening" });
|
|
4604
|
-
hops.push({ cert, parentPem:
|
|
4605
|
-
|
|
4604
|
+
hops.push({ cert, parentPem: pem2, parentScope: scope });
|
|
4605
|
+
pem2 = cert.child_public_key_pem;
|
|
4606
4606
|
scope = mergeScopes(scope, f.scope.scope);
|
|
4607
4607
|
}
|
|
4608
4608
|
const admitted = purpose(scope);
|
|
4609
4609
|
if (!admitted.ok)
|
|
4610
4610
|
return fail("OUT_OF_SCOPE", { dimension: admitted.dimension });
|
|
4611
|
-
return { decision: "RESOLVED", pem, path: [], effective: scope, hops };
|
|
4611
|
+
return { decision: "RESOLVED", pem: pem2, path: [], effective: scope, hops };
|
|
4612
4612
|
}
|
|
4613
4613
|
function resolveKey(o) {
|
|
4614
4614
|
const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
|
|
@@ -4694,7 +4694,7 @@ function verifyArtifact(o) {
|
|
|
4694
4694
|
}
|
|
4695
4695
|
}
|
|
4696
4696
|
|
|
4697
|
-
//
|
|
4697
|
+
// node_modules/@nomosprotocol/verify/dist/evaluate.js
|
|
4698
4698
|
var jcsString = (v) => canonicalize(v);
|
|
4699
4699
|
var FIELD = /* @__PURE__ */ Symbol("field");
|
|
4700
4700
|
var ABSENT = /* @__PURE__ */ Symbol("absent");
|
|
@@ -5023,7 +5023,7 @@ function evaluate(artifact, facts) {
|
|
|
5023
5023
|
return ev;
|
|
5024
5024
|
}
|
|
5025
5025
|
|
|
5026
|
-
//
|
|
5026
|
+
// node_modules/@nomosprotocol/verify/dist/act.js
|
|
5027
5027
|
var MAX_WINDOW_SECONDS = 300;
|
|
5028
5028
|
var MAX_IN_FORCE_SECONDS = 86400;
|
|
5029
5029
|
var TESTIMONY_SUITES = ["Ed25519", "ES256"];
|
|
@@ -5061,7 +5061,7 @@ function hasAmbiguousEscalation(artifact) {
|
|
|
5061
5061
|
return [...named.values()].some((ids) => ids.size > 1);
|
|
5062
5062
|
}
|
|
5063
5063
|
var proofOf = (st) => st.algorithm === "WebAuthn-ES256" ? pick(st, WEBAUTHN_FIELDS) : { signature: st.signature };
|
|
5064
|
-
var statementSig = (
|
|
5064
|
+
var statementSig = (pem2, st, payload) => verifySuite(st.algorithm, pem2, jcs(payload), proofOf(st));
|
|
5065
5065
|
var time = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5066
5066
|
var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
5067
5067
|
function statementFailure(r, invalid, outOfScope, who) {
|
|
@@ -5226,11 +5226,11 @@ function verifyUnsafe(a, o) {
|
|
|
5226
5226
|
return { decision: "AUTHORIZED", commit: true, ...base };
|
|
5227
5227
|
if (ev.outcome === "block")
|
|
5228
5228
|
return { decision: "DENIED", commit: false, ...base };
|
|
5229
|
-
const
|
|
5230
|
-
if (!
|
|
5229
|
+
const esc3 = (art.governance?.escalations ?? []).find((e) => e?.id === ev.escalation_id);
|
|
5230
|
+
if (!esc3)
|
|
5231
5231
|
return { decision: "ESCALATED", commit: false, escalation_id: ev.escalation_id, consents_valid: 0, consents_required: null, ...base };
|
|
5232
|
-
const role =
|
|
5233
|
-
const needN = Math.trunc(Number(
|
|
5232
|
+
const role = esc3.role_required;
|
|
5233
|
+
const needN = Math.trunc(Number(esc3.required_consents ?? 1));
|
|
5234
5234
|
const approvers = /* @__PURE__ */ new Set();
|
|
5235
5235
|
for (const c of Array.isArray(act2.consents) ? act2.consents : []) {
|
|
5236
5236
|
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")) {
|
|
@@ -5256,7 +5256,7 @@ function verifyUnsafe(a, o) {
|
|
|
5256
5256
|
return { decision: "ESCALATED", commit: false, ...counted };
|
|
5257
5257
|
}
|
|
5258
5258
|
|
|
5259
|
-
//
|
|
5259
|
+
// node_modules/@nomosprotocol/verify/dist/seal.js
|
|
5260
5260
|
function verifySeal(artifact, publicKeyPem) {
|
|
5261
5261
|
const bad = checkSealShape(artifact);
|
|
5262
5262
|
if (bad)
|
|
@@ -5270,6 +5270,83 @@ function verifySeal(artifact, publicKeyPem) {
|
|
|
5270
5270
|
return sealFailure(artifact, publicKeyPem) ?? { decision: "VALID", kid };
|
|
5271
5271
|
}
|
|
5272
5272
|
|
|
5273
|
+
// node_modules/@nomosprotocol/verify/dist/revocation.js
|
|
5274
|
+
var MAX_LIST_CERTS = 1e3;
|
|
5275
|
+
var REVOCATION_SUITES = ["Ed25519", "ES256"];
|
|
5276
|
+
var CERT_FIELDS2 = ["parent_kid", "child_kid", "child_public_key_pem", "issued_at", "expires_at", "algorithm", "signature"];
|
|
5277
|
+
var FP = /^[0-9a-f]{64}$/;
|
|
5278
|
+
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 };
|
|
5279
|
+
var revocationListPayload = (l) => ({ generated_at: l.generated_at, statements: l.statements });
|
|
5280
|
+
var forSigning = (suite) => (scope) => suitePermitted(scope, suite) ? { ok: true } : { ok: false, dimension: "suite" };
|
|
5281
|
+
var str = (v) => typeof v === "string" && v !== "";
|
|
5282
|
+
var time2 = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5283
|
+
function signerPem(kid, pool, root, at, suite, revokedKids, revokedCerts) {
|
|
5284
|
+
const r = resolveKey({ targetKid: kid, pool, rootPublicKeyPem: root, purpose: forSigning(suite), now: new Date(at), revokedKids, revokedCerts });
|
|
5285
|
+
return r.decision === "RESOLVED" ? r.pem : null;
|
|
5286
|
+
}
|
|
5287
|
+
function evaluateStatement(st, pool, rootPublicKeyPem, now) {
|
|
5288
|
+
if (st === null || typeof st !== "object" || Array.isArray(st))
|
|
5289
|
+
return "bad_statement";
|
|
5290
|
+
const hasKid = "revoked_kid" in st, hasFp = "revoked_fingerprint" in st;
|
|
5291
|
+
if (hasKid === hasFp)
|
|
5292
|
+
return "bad_statement";
|
|
5293
|
+
const target = hasKid ? st.revoked_kid : st.revoked_fingerprint;
|
|
5294
|
+
if (!str(target) || hasFp && !FP.test(target))
|
|
5295
|
+
return "bad_statement";
|
|
5296
|
+
if (typeof st.reason !== "string" || !["issued_at", "kid", "algorithm", "signature"].every((f) => str(st[f])))
|
|
5297
|
+
return "bad_statement";
|
|
5298
|
+
if (!REVOCATION_SUITES.includes(st.algorithm))
|
|
5299
|
+
return "bad_statement";
|
|
5300
|
+
const issued = time2(st.issued_at);
|
|
5301
|
+
if (Number.isNaN(issued) || issued > now.getTime())
|
|
5302
|
+
return "bad_statement";
|
|
5303
|
+
const certs = pool.filter((c) => c.parent_kid === st.kid && (hasKid ? c.child_kid === target : fingerprint(c) === target));
|
|
5304
|
+
if (certs.length === 0)
|
|
5305
|
+
return "no_certificate";
|
|
5306
|
+
const parent = signerPem(st.kid, pool, rootPublicKeyPem, issued, st.algorithm, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
5307
|
+
if (!parent)
|
|
5308
|
+
return "signer_not_recognized";
|
|
5309
|
+
if (!certs.some((c) => verifySuite(c.algorithm, parent, jcs(certPayload(c)), { signature: c.signature }).ok))
|
|
5310
|
+
return "bad_certificate_signature";
|
|
5311
|
+
return verifySuite(st.algorithm, parent, jcs(revocationPayload(st)), { signature: st.signature }).ok ? null : "bad_signature";
|
|
5312
|
+
}
|
|
5313
|
+
function verifyRevocationList(list, o) {
|
|
5314
|
+
const L = list;
|
|
5315
|
+
if (L === null || typeof L !== "object" || !["generated_at", "kid", "algorithm", "signature"].every((f) => typeof L[f] === "string") || !Array.isArray(L.statements)) {
|
|
5316
|
+
return { decision: "MALFORMED", reason_code: "bad_list" };
|
|
5317
|
+
}
|
|
5318
|
+
const extra = L.key_certs ?? [];
|
|
5319
|
+
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"))) {
|
|
5320
|
+
return { decision: "MALFORMED", reason_code: "bad_list_certs" };
|
|
5321
|
+
}
|
|
5322
|
+
const seen = /* @__PURE__ */ new Set(), pool = [];
|
|
5323
|
+
for (const c of [...extra, ...o.ownCerts ?? []]) {
|
|
5324
|
+
const fp = fingerprint(c);
|
|
5325
|
+
if (!seen.has(fp)) {
|
|
5326
|
+
seen.add(fp);
|
|
5327
|
+
pool.push(c);
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set(), ignored = [];
|
|
5331
|
+
L.statements.forEach((st, index) => {
|
|
5332
|
+
const why = evaluateStatement(st, pool, o.rootPublicKeyPem, o.now);
|
|
5333
|
+
if (why)
|
|
5334
|
+
ignored.push({ index, reason: why });
|
|
5335
|
+
else if ("revoked_kid" in st)
|
|
5336
|
+
kids.add(st.revoked_kid);
|
|
5337
|
+
else
|
|
5338
|
+
certs.add(st.revoked_fingerprint);
|
|
5339
|
+
});
|
|
5340
|
+
const out = { decision: "OK", revoked_kids: [...kids].sort(), revoked_certs: [...certs].sort(), ignored };
|
|
5341
|
+
const generated = time2(L.generated_at);
|
|
5342
|
+
if (Number.isNaN(generated) || generated > o.now.getTime() || !REVOCATION_SUITES.includes(L.algorithm))
|
|
5343
|
+
return out;
|
|
5344
|
+
const signer = signerPem(L.kid, pool, o.rootPublicKeyPem, generated, L.algorithm, kids, certs);
|
|
5345
|
+
if (signer && verifySuite(L.algorithm, signer, jcs(revocationListPayload(L)), { signature: L.signature }).ok)
|
|
5346
|
+
out.as_of = L.generated_at;
|
|
5347
|
+
return out;
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5273
5350
|
// src/verify-chain.ts
|
|
5274
5351
|
var CHAIN_PROTOCOL_VERSION2 = "1";
|
|
5275
5352
|
var DEFAULT_BASE = "https://www.nomosprotocol.com";
|
|
@@ -5316,43 +5393,371 @@ async function verifyChainRemote(params, opts) {
|
|
|
5316
5393
|
throw new NomosAPIError(message, res.status, requestId);
|
|
5317
5394
|
}
|
|
5318
5395
|
|
|
5396
|
+
// src/bindings.ts
|
|
5397
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
5398
|
+
var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
5399
|
+
var isScalar = (v) => typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
|
|
5400
|
+
function parseToolBindings(raw) {
|
|
5401
|
+
const no = (why) => {
|
|
5402
|
+
throw new Error(why);
|
|
5403
|
+
};
|
|
5404
|
+
const obj = raw;
|
|
5405
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) no("it is not a JSON object");
|
|
5406
|
+
if (obj.version !== 1) no('"version" must be 1');
|
|
5407
|
+
if (typeof obj.artifact !== "string" || !obj.artifact) no('"artifact" must name the sealed policy');
|
|
5408
|
+
if (typeof obj.reviewed !== "boolean") no('"reviewed" must be true or false');
|
|
5409
|
+
if (!obj.tools || typeof obj.tools !== "object" || Array.isArray(obj.tools)) no('"tools" must be an object');
|
|
5410
|
+
for (const k of Object.keys(obj)) if (!["version", "artifact", "reviewed", "tools", "_review"].includes(k)) no(`unknown setting "${k}"`);
|
|
5411
|
+
const tools = {};
|
|
5412
|
+
for (const tool of Object.keys(obj.tools)) {
|
|
5413
|
+
if (FORBIDDEN_KEYS.has(tool)) no(`"${tool}" isn't a usable tool name`);
|
|
5414
|
+
const entry = obj.tools[tool];
|
|
5415
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) no(`tool "${tool}" must be an object with "facts"`);
|
|
5416
|
+
for (const k of Object.keys(entry)) if (!["facts", "_why"].includes(k)) no(`tool "${tool}": unknown setting "${k}"`);
|
|
5417
|
+
const facts = entry.facts;
|
|
5418
|
+
if (!facts || typeof facts !== "object" || Array.isArray(facts)) no(`tool "${tool}" needs "facts"`);
|
|
5419
|
+
tools[tool] = { facts: {} };
|
|
5420
|
+
for (const fact of Object.keys(facts)) {
|
|
5421
|
+
const where = `${tool}.${fact}`;
|
|
5422
|
+
if (fact === "tool") no(`${where}: "tool" is set by the guard to the tool's name`);
|
|
5423
|
+
if (FORBIDDEN_KEYS.has(fact)) no(`${where} isn't a usable fact name`);
|
|
5424
|
+
const b = facts[fact];
|
|
5425
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) no(`${where} must be an object with "arg"`);
|
|
5426
|
+
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`);
|
|
5427
|
+
if (typeof b.arg !== "string" || !b.arg) no(`${where} needs "arg", the argument it comes from`);
|
|
5428
|
+
if (b.arg.split(".").some((s) => !s || FORBIDDEN_KEYS.has(s))) no(`${where}: "${b.arg}" isn't a usable argument path`);
|
|
5429
|
+
if (b.scale !== void 0 && b.map !== void 0) no(`${where}: use "scale" or "map", not both`);
|
|
5430
|
+
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`);
|
|
5431
|
+
if (b.map !== void 0) {
|
|
5432
|
+
if (!b.map || typeof b.map !== "object" || Array.isArray(b.map)) no(`${where}: "map" must be an object`);
|
|
5433
|
+
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`);
|
|
5434
|
+
}
|
|
5435
|
+
tools[tool].facts[fact] = { arg: b.arg, ...b.scale !== void 0 ? { scale: b.scale } : {}, ...b.map !== void 0 ? { map: b.map } : {} };
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
return { version: 1, artifact: obj.artifact, reviewed: obj.reviewed, tools };
|
|
5439
|
+
}
|
|
5440
|
+
function applyToolBindings(bindings, tool, args) {
|
|
5441
|
+
const inputs = { tool };
|
|
5442
|
+
const unresolved = [];
|
|
5443
|
+
const facts = hasOwn(bindings.tools, tool) ? bindings.tools[tool].facts : {};
|
|
5444
|
+
for (const [fact, b] of Object.entries(facts)) {
|
|
5445
|
+
let v = args;
|
|
5446
|
+
for (const seg of b.arg.split(".")) v = v && typeof v === "object" && !Array.isArray(v) && hasOwn(v, seg) ? v[seg] : void 0;
|
|
5447
|
+
if (v === void 0 || v === null) {
|
|
5448
|
+
unresolved.push({ fact, arg: b.arg, why: "missing" });
|
|
5449
|
+
continue;
|
|
5450
|
+
}
|
|
5451
|
+
if (b.scale !== void 0) {
|
|
5452
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
5453
|
+
unresolved.push({ fact, arg: b.arg, why: "not_a_number" });
|
|
5454
|
+
continue;
|
|
5455
|
+
}
|
|
5456
|
+
inputs[fact] = Number((v * b.scale).toFixed(9));
|
|
5457
|
+
continue;
|
|
5458
|
+
}
|
|
5459
|
+
if (!isScalar(v)) {
|
|
5460
|
+
unresolved.push({ fact, arg: b.arg, why: "not_a_value" });
|
|
5461
|
+
continue;
|
|
5462
|
+
}
|
|
5463
|
+
if (b.map) {
|
|
5464
|
+
if (!hasOwn(b.map, String(v))) {
|
|
5465
|
+
unresolved.push({ fact, arg: b.arg, why: "not_in_map" });
|
|
5466
|
+
continue;
|
|
5467
|
+
}
|
|
5468
|
+
inputs[fact] = b.map[String(v)];
|
|
5469
|
+
continue;
|
|
5470
|
+
}
|
|
5471
|
+
inputs[fact] = v;
|
|
5472
|
+
}
|
|
5473
|
+
return { inputs, unresolved };
|
|
5474
|
+
}
|
|
5475
|
+
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"]);
|
|
5476
|
+
var UNITS = { cents: 0.01, pence: 0.01, fils: 0.01, usd: null, dollars: null, aed: null, eur: null };
|
|
5477
|
+
var words = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).map((w) => w.toLowerCase()).filter(Boolean);
|
|
5478
|
+
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 }]);
|
|
5479
|
+
function suggestToolBindings(artifact, tools, fields) {
|
|
5480
|
+
const bindings = { version: 1, artifact, reviewed: false, tools: {} };
|
|
5481
|
+
const proposals = [];
|
|
5482
|
+
const unbound = [];
|
|
5483
|
+
for (const tool of tools) {
|
|
5484
|
+
bindings.tools[tool.name] = { facts: {} };
|
|
5485
|
+
const args = flatProperties(tool.inputSchema?.properties);
|
|
5486
|
+
for (const field of fields) {
|
|
5487
|
+
if (field.name === "tool") continue;
|
|
5488
|
+
const want = words(field.name).filter((w) => !WEAK.has(w) && !(w in UNITS));
|
|
5489
|
+
const scored = args.map((a) => {
|
|
5490
|
+
const raw = words(a.path);
|
|
5491
|
+
const have = raw.filter((w) => !WEAK.has(w) && !(w in UNITS));
|
|
5492
|
+
const shared = want.filter((w) => have.includes(w));
|
|
5493
|
+
const unit = raw.find((w) => w in UNITS);
|
|
5494
|
+
const same = words(field.name).join("_") === raw.join("_");
|
|
5495
|
+
const sameCore = want.length > 0 && want.length === have.length && shared.length === want.length;
|
|
5496
|
+
return { a, shared, unit, same, sameCore, score: same ? 3 : sameCore ? 2 : shared.length ? shared.length / Math.max(want.length, have.length) : 0 };
|
|
5497
|
+
}).filter((s) => s.score > 0).sort((x, y) => y.score - x.score);
|
|
5498
|
+
if (!scored.length) {
|
|
5499
|
+
unbound.push({ tool: tool.name, fact: field.name, candidates: [] });
|
|
5500
|
+
continue;
|
|
5501
|
+
}
|
|
5502
|
+
const best = scored[0];
|
|
5503
|
+
if (scored[1] && scored[1].score === best.score) {
|
|
5504
|
+
unbound.push({ tool: tool.name, fact: field.name, candidates: scored.filter((s) => s.score === best.score).map((s) => s.a.path) });
|
|
5505
|
+
continue;
|
|
5506
|
+
}
|
|
5507
|
+
const scale = best.unit ? UNITS[best.unit] ?? void 0 : void 0;
|
|
5508
|
+
const why = best.same ? "the same name" : best.sameCore ? "the same words" : `shares "${best.shared.join(", ")}"`;
|
|
5509
|
+
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 } : {} });
|
|
5510
|
+
bindings.tools[tool.name].facts[field.name] = { arg: best.a.path, ...scale ? { scale } : {} };
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
return { bindings, proposals, unbound };
|
|
5514
|
+
}
|
|
5515
|
+
function describeSuggestion(s) {
|
|
5516
|
+
const lines = [];
|
|
5517
|
+
for (const tool of Object.keys(s.bindings.tools)) {
|
|
5518
|
+
lines.push(`
|
|
5519
|
+
${tool}`);
|
|
5520
|
+
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})`);
|
|
5521
|
+
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(", ")})` : ""}`);
|
|
5522
|
+
}
|
|
5523
|
+
return lines.join("\n");
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
// src/act/bytes.ts
|
|
5527
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
5528
|
+
var fromUtf82 = (b) => new TextDecoder().decode(b);
|
|
5529
|
+
function concat2(...parts) {
|
|
5530
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
5531
|
+
let o = 0;
|
|
5532
|
+
for (const p of parts) {
|
|
5533
|
+
out.set(p, o);
|
|
5534
|
+
o += p.length;
|
|
5535
|
+
}
|
|
5536
|
+
return out;
|
|
5537
|
+
}
|
|
5538
|
+
function equal2(a, b) {
|
|
5539
|
+
if (a.length !== b.length) return false;
|
|
5540
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
5541
|
+
return true;
|
|
5542
|
+
}
|
|
5543
|
+
function fromHex(h) {
|
|
5544
|
+
const out = new Uint8Array(h.length / 2);
|
|
5545
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
5546
|
+
return out;
|
|
5547
|
+
}
|
|
5548
|
+
function toBase642(b) {
|
|
5549
|
+
let s = "";
|
|
5550
|
+
for (let i = 0; i < b.length; i += 32768) s += String.fromCharCode(...b.subarray(i, i + 32768));
|
|
5551
|
+
return btoa(s);
|
|
5552
|
+
}
|
|
5553
|
+
function fromBase642(s) {
|
|
5554
|
+
const bin = atob(s);
|
|
5555
|
+
const out = new Uint8Array(bin.length);
|
|
5556
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
5557
|
+
return out;
|
|
5558
|
+
}
|
|
5559
|
+
var toBase64url2 = (b) => toBase642(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5560
|
+
var fromBase64url2 = (s) => {
|
|
5561
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
5562
|
+
return fromBase642(t + "=".repeat((4 - t.length % 4) % 4));
|
|
5563
|
+
};
|
|
5564
|
+
function randomBytes3(n) {
|
|
5565
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
5566
|
+
}
|
|
5567
|
+
var randomUUID = () => globalThis.crypto.randomUUID();
|
|
5568
|
+
|
|
5319
5569
|
// src/act/signer.ts
|
|
5320
|
-
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign as nodeSign } from "crypto";
|
|
5321
|
-
var P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551n;
|
|
5322
5570
|
function kidOf2(publicKeyPem) {
|
|
5323
|
-
const
|
|
5324
|
-
|
|
5571
|
+
const kid = kidOf(publicKeyPem);
|
|
5572
|
+
if (!kid) throw new Error("kidOf: not a canonical Ed25519 or P-256 public key (NOMOS-SPEC-010 \xA73.1)");
|
|
5573
|
+
return kid;
|
|
5574
|
+
}
|
|
5575
|
+
var ED25519_SPKI = fromHex("302a300506032b6570032100");
|
|
5576
|
+
var P256_SPKI = fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");
|
|
5577
|
+
var ED25519_PKCS8 = fromHex("302e020100300506032b657004220420");
|
|
5578
|
+
var P256_PKCS8_HEAD = fromHex("308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420");
|
|
5579
|
+
var P256_PKCS8_PUB = fromHex("a144034200");
|
|
5580
|
+
var OID_ED25519 = fromHex("2b6570");
|
|
5581
|
+
var OID_EC = fromHex("2a8648ce3d0201");
|
|
5582
|
+
var OID_P256 = fromHex("2a8648ce3d030107");
|
|
5583
|
+
function pem(label, der) {
|
|
5584
|
+
const b64 = toBase642(der);
|
|
5585
|
+
return `-----BEGIN ${label}-----
|
|
5586
|
+
${b64.match(/.{1,64}/g).join("\n")}
|
|
5587
|
+
-----END ${label}-----
|
|
5588
|
+
`;
|
|
5589
|
+
}
|
|
5590
|
+
function pemBody(text) {
|
|
5591
|
+
const m = text.trim().match(/^-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----$/);
|
|
5592
|
+
if (!m) throw new Error("Signer: expected a PEM private key");
|
|
5593
|
+
return { label: m[1], der: fromBase642(m[2].replace(/\s+/g, "")) };
|
|
5594
|
+
}
|
|
5595
|
+
function tlv(b, at) {
|
|
5596
|
+
const tag = b[at];
|
|
5597
|
+
let len = b[at + 1], start = at + 2;
|
|
5598
|
+
if (len & 128) {
|
|
5599
|
+
const n = len & 127;
|
|
5600
|
+
if (n < 1 || n > 2) throw new Error("Signer: unsupported DER length");
|
|
5601
|
+
len = 0;
|
|
5602
|
+
for (let i = 0; i < n; i++) len = len << 8 | b[start + i];
|
|
5603
|
+
start += n;
|
|
5604
|
+
}
|
|
5605
|
+
if (start + len > b.length) throw new Error("Signer: truncated DER");
|
|
5606
|
+
return { tag, start, end: start + len };
|
|
5607
|
+
}
|
|
5608
|
+
function children(b, outer) {
|
|
5609
|
+
const out = [];
|
|
5610
|
+
for (let at = outer.start; at < outer.end; ) {
|
|
5611
|
+
const t = tlv(b, at);
|
|
5612
|
+
out.push(t);
|
|
5613
|
+
at = t.end;
|
|
5614
|
+
}
|
|
5615
|
+
return out;
|
|
5616
|
+
}
|
|
5617
|
+
function sec1Scalar(b, seq) {
|
|
5618
|
+
const [version, d, ...rest] = children(b, seq);
|
|
5619
|
+
if (version.tag !== 2 || d.tag !== 4 || d.end - d.start !== 32) throw new Error("Signer: malformed EC private key");
|
|
5620
|
+
for (const r of rest) {
|
|
5621
|
+
if (r.tag === 160) {
|
|
5622
|
+
const oid = tlv(b, r.start);
|
|
5623
|
+
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)");
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
return b.slice(d.start, d.end);
|
|
5325
5627
|
}
|
|
5326
|
-
function
|
|
5327
|
-
const
|
|
5328
|
-
const
|
|
5329
|
-
if (
|
|
5330
|
-
|
|
5628
|
+
function readPrivateKey(text) {
|
|
5629
|
+
const { label, der } = pemBody(text);
|
|
5630
|
+
const top = tlv(der, 0);
|
|
5631
|
+
if (label === "EC PRIVATE KEY") return { type: "ES256", key: sec1Scalar(der, top) };
|
|
5632
|
+
if (label !== "PRIVATE KEY") throw new Error(`Signer: unsupported key type "${label}" (PKCS#8 "PRIVATE KEY" or SEC1 "EC PRIVATE KEY")`);
|
|
5633
|
+
const [, alg, key] = children(der, top);
|
|
5634
|
+
const [oid, params] = children(der, alg);
|
|
5635
|
+
const algOid = der.subarray(oid.start, oid.end);
|
|
5636
|
+
if (equal2(algOid, OID_ED25519)) {
|
|
5637
|
+
const inner = tlv(der, key.start);
|
|
5638
|
+
if (inner.tag !== 4 || inner.end - inner.start !== 32) throw new Error("Signer: malformed Ed25519 private key");
|
|
5639
|
+
return { type: "Ed25519", key: der.slice(inner.start, inner.end) };
|
|
5331
5640
|
}
|
|
5641
|
+
if (equal2(algOid, OID_EC)) {
|
|
5642
|
+
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)");
|
|
5643
|
+
return { type: "ES256", key: sec1Scalar(der, tlv(der, key.start)) };
|
|
5644
|
+
}
|
|
5645
|
+
throw new Error("Signer: only Ed25519 and P-256 keys are NOMOS signature suites (NOMOS-SPEC-010)");
|
|
5646
|
+
}
|
|
5647
|
+
function publicPem(s) {
|
|
5648
|
+
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)));
|
|
5649
|
+
}
|
|
5650
|
+
function privatePem(s) {
|
|
5651
|
+
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)));
|
|
5652
|
+
}
|
|
5653
|
+
function fromSecret(s) {
|
|
5654
|
+
const publicKeyPem = publicPem(s);
|
|
5332
5655
|
return {
|
|
5333
5656
|
kid: kidOf2(publicKeyPem),
|
|
5334
5657
|
publicKeyPem,
|
|
5335
|
-
algorithm:
|
|
5658
|
+
algorithm: s.type,
|
|
5336
5659
|
async sign(payload) {
|
|
5337
|
-
|
|
5338
|
-
const raw = nodeSign("sha256", Buffer.from(payload), { key: privateKey, dsaEncoding: "ieee-p1363" });
|
|
5339
|
-
let s = BigInt("0x" + raw.subarray(32).toString("hex"));
|
|
5340
|
-
if (s > P256_N / 2n) s = P256_N - s;
|
|
5341
|
-
return Buffer.concat([raw.subarray(0, 32), Buffer.from(s.toString(16).padStart(64, "0"), "hex")]).toString("base64");
|
|
5660
|
+
return toBase642(s.type === "Ed25519" ? ed25519.sign(payload, s.key) : p256.sign(payload, s.key, { prehash: true, lowS: true, format: "compact" }));
|
|
5342
5661
|
}
|
|
5343
5662
|
};
|
|
5344
5663
|
}
|
|
5345
5664
|
function localSigner(privateKeyPem) {
|
|
5346
|
-
return
|
|
5665
|
+
return fromSecret(readPrivateKey(privateKeyPem));
|
|
5347
5666
|
}
|
|
5348
5667
|
function generateSigner(algorithm = "Ed25519") {
|
|
5349
|
-
const
|
|
5350
|
-
return { ...
|
|
5668
|
+
const s = algorithm === "Ed25519" ? { type: "Ed25519", key: ed25519.utils.randomSecretKey() } : { type: "ES256", key: p256.utils.randomSecretKey() };
|
|
5669
|
+
return { ...fromSecret(s), privateKeyPem: privatePem(s) };
|
|
5351
5670
|
}
|
|
5352
5671
|
function seededSigner(seed) {
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5672
|
+
return fromSecret({ type: "Ed25519", key: sha2562(seed) });
|
|
5673
|
+
}
|
|
5674
|
+
var sha2562 = (s) => sha256(new TextEncoder().encode(s));
|
|
5675
|
+
|
|
5676
|
+
// src/act/certificates.ts
|
|
5677
|
+
var MAX_PRESENTED = 20;
|
|
5678
|
+
function reliedKids(act2) {
|
|
5679
|
+
const kids = [act2?.authority?.artifact?.seal?.kid, ...(act2?.testimony ?? []).map((t) => t?.kid), ...(act2?.consents ?? []).map((c) => c?.kid)];
|
|
5680
|
+
return [...new Set(kids.filter((k) => typeof k === "string" && k !== ""))];
|
|
5681
|
+
}
|
|
5682
|
+
function presentedCertificates(available, act2) {
|
|
5683
|
+
const unique = /* @__PURE__ */ new Map();
|
|
5684
|
+
for (const c of available) unique.set(fingerprint(c), c);
|
|
5685
|
+
const kids = new Set(reliedKids(act2)), chosen = /* @__PURE__ */ new Map();
|
|
5686
|
+
for (let grew = true; grew; ) {
|
|
5687
|
+
grew = false;
|
|
5688
|
+
for (const [fp, c] of unique) {
|
|
5689
|
+
if (!chosen.has(fp) && kids.has(c.child_kid)) {
|
|
5690
|
+
chosen.set(fp, c);
|
|
5691
|
+
kids.add(c.parent_kid);
|
|
5692
|
+
grew = true;
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
return [...chosen.values()];
|
|
5697
|
+
}
|
|
5698
|
+
async function signRevocation(parent, target, reason, issuedAt = /* @__PURE__ */ new Date()) {
|
|
5699
|
+
const st = "kid" in target ? { revoked_kid: target.kid, reason, issued_at: issuedAt.toISOString() } : { revoked_fingerprint: fingerprint(target.certificate), reason, issued_at: issuedAt.toISOString() };
|
|
5700
|
+
if ("certificate" in target && target.certificate.parent_kid !== parent.kid) throw new Error("signRevocation: only the parent that issued a certificate can revoke it");
|
|
5701
|
+
return { ...st, kid: parent.kid, algorithm: parent.algorithm, signature: await parent.sign(jcs(revocationPayload(st))) };
|
|
5702
|
+
}
|
|
5703
|
+
async function signRevocationList(signer, statements, keyCerts = [], generatedAt = /* @__PURE__ */ new Date()) {
|
|
5704
|
+
const L = { generated_at: generatedAt.toISOString(), kid: signer.kid, algorithm: signer.algorithm, statements, ...keyCerts.length ? { key_certs: keyCerts } : {} };
|
|
5705
|
+
L.signature = await signer.sign(jcs(revocationListPayload(L)));
|
|
5706
|
+
return L;
|
|
5707
|
+
}
|
|
5708
|
+
function revocationListCertificates(available, statements, listSignerKid) {
|
|
5709
|
+
const byFp = new Map(available.map((c) => [fingerprint(c), c]));
|
|
5710
|
+
const revokedCerts = statements.map((s) => s.revoked_fingerprint && byFp.get(s.revoked_fingerprint)).filter((c) => !!c);
|
|
5711
|
+
const kids = [listSignerKid, ...statements.flatMap((s) => [s.kid, s.revoked_kid].filter((k) => !!k)), ...revokedCerts.map((c) => c.parent_kid)];
|
|
5712
|
+
const closure = presentedCertificates(available, { authority: { artifact: { seal: {} } }, testimony: kids.map((kid) => ({ kid })) });
|
|
5713
|
+
const out = new Map(closure.map((c) => [fingerprint(c), c]));
|
|
5714
|
+
for (const c of revokedCerts) out.set(fingerprint(c), c);
|
|
5715
|
+
return [...out.values()];
|
|
5716
|
+
}
|
|
5717
|
+
var RevocationUnavailable = class extends Error {
|
|
5718
|
+
constructor(message, retryAfterSeconds) {
|
|
5719
|
+
super(message);
|
|
5720
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
5721
|
+
this.name = "RevocationUnavailable";
|
|
5722
|
+
}
|
|
5723
|
+
};
|
|
5724
|
+
function revocationSource(o) {
|
|
5725
|
+
const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
5726
|
+
const maxAge = (o.maxAgeSeconds ?? 3600) * 1e3, refresh = (o.refreshSeconds ?? 60) * 1e3;
|
|
5727
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
5728
|
+
let asOf = 0, fetchedAt = 0, inFlight = null, lastError = "";
|
|
5729
|
+
async function load() {
|
|
5730
|
+
fetchedAt = now().getTime();
|
|
5731
|
+
try {
|
|
5732
|
+
const res = await f(o.url, { headers: { accept: "application/json" } });
|
|
5733
|
+
if (!res.ok) {
|
|
5734
|
+
lastError = `${o.url} answered ${res.status}`;
|
|
5735
|
+
return;
|
|
5736
|
+
}
|
|
5737
|
+
const r = verifyRevocationList(await res.json(), { rootPublicKeyPem: o.root, now: now(), ownCerts: o.ownCerts });
|
|
5738
|
+
if (r.decision !== "OK") {
|
|
5739
|
+
lastError = `${o.url} served a malformed list (${r.reason_code})`;
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
for (const k of r.revoked_kids) kids.add(k);
|
|
5743
|
+
for (const c of r.revoked_certs) certs.add(c);
|
|
5744
|
+
if (r.as_of) asOf = Math.max(asOf, Date.parse(r.as_of));
|
|
5745
|
+
else lastError = `${o.url}'s list is not signed by a key your root reaches, so it does not show it is current`;
|
|
5746
|
+
} catch (e) {
|
|
5747
|
+
lastError = `${o.url}: ${e?.message ?? e}`;
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
return async () => {
|
|
5751
|
+
const t = now().getTime();
|
|
5752
|
+
if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
|
|
5753
|
+
inFlight = null;
|
|
5754
|
+
});
|
|
5755
|
+
if (inFlight) await inFlight;
|
|
5756
|
+
if (!asOf || t - asOf > maxAge) {
|
|
5757
|
+
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));
|
|
5758
|
+
}
|
|
5759
|
+
return { kids: new Set(kids), certs: new Set(certs) };
|
|
5760
|
+
};
|
|
5356
5761
|
}
|
|
5357
5762
|
|
|
5358
5763
|
// src/act/documents.ts
|
|
@@ -5457,6 +5862,11 @@ var Rollback = class extends Error {
|
|
|
5457
5862
|
this.answer = answer2;
|
|
5458
5863
|
}
|
|
5459
5864
|
};
|
|
5865
|
+
var LostRace = class extends Error {
|
|
5866
|
+
constructor() {
|
|
5867
|
+
super("replayed");
|
|
5868
|
+
}
|
|
5869
|
+
};
|
|
5460
5870
|
function accept(o) {
|
|
5461
5871
|
const kid = kidOf2(o.root);
|
|
5462
5872
|
for (const a of o.authorities) {
|
|
@@ -5464,7 +5874,7 @@ function accept(o) {
|
|
|
5464
5874
|
throw new Error(`accept(): authority ${a.artifactId} uses currencyPolicy "record" but has no inForceSealHash`);
|
|
5465
5875
|
}
|
|
5466
5876
|
}
|
|
5467
|
-
if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only.");
|
|
5877
|
+
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.");
|
|
5468
5878
|
const store = o.store ?? memoryStore();
|
|
5469
5879
|
const maxWindowSeconds = o.maxWindowSeconds ?? 900;
|
|
5470
5880
|
const maxActBytes = o.maxActBytes ?? 1024 * 1024;
|
|
@@ -5490,8 +5900,7 @@ function accept(o) {
|
|
|
5490
5900
|
valid_until: new Date(now().getTime() + (o.documentTtlSeconds ?? 86400) * 1e3).toISOString()
|
|
5491
5901
|
};
|
|
5492
5902
|
}
|
|
5493
|
-
|
|
5494
|
-
const rev = await o.revocations?.() ?? {};
|
|
5903
|
+
function verifyWith(act2, authority, facts, at, seen, rev) {
|
|
5495
5904
|
return verifyAct(act2, {
|
|
5496
5905
|
relyingPartyId: o.relyingPartyId,
|
|
5497
5906
|
rootPublicKeyPem: o.root,
|
|
@@ -5523,9 +5932,20 @@ function accept(o) {
|
|
|
5523
5932
|
const authority = o.authorities.find((a) => a.artifactId === artifactId);
|
|
5524
5933
|
if (!authority) return answer({ decision: "NOT_ACCEPTED", reason_code: "authority_not_accepted", artifact_id: artifactId ?? null });
|
|
5525
5934
|
if (!authority.actions.includes(act2?.action?.type)) return answer({ decision: "NOT_ACCEPTED", reason_code: "action_not_accepted", action_type: act2?.action?.type ?? null });
|
|
5935
|
+
let rev;
|
|
5936
|
+
try {
|
|
5937
|
+
rev = await o.revocations?.() ?? {};
|
|
5938
|
+
} catch (e) {
|
|
5939
|
+
const retry = e instanceof RevocationUnavailable ? e.retryAfterSeconds : 60;
|
|
5940
|
+
return {
|
|
5941
|
+
status: 503,
|
|
5942
|
+
headers: { "content-type": "application/json", "retry-after": String(retry) },
|
|
5943
|
+
body: { error: "Revocation status is not current, so nothing is committed. Resubmit this same Act later.", detail: String(e?.message ?? e), retry_after: retry }
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5526
5946
|
const verifiedAt = now();
|
|
5527
5947
|
const facts = await o.localFacts?.(act2) ?? {};
|
|
5528
|
-
const verdict =
|
|
5948
|
+
const verdict = verifyWith(act2, authority, facts, verifiedAt, await store.hasNonce(act2.nonce), rev);
|
|
5529
5949
|
const receiptOf = (v, f, committedAt) => ({
|
|
5530
5950
|
act_id: act2.act_id,
|
|
5531
5951
|
act_digest: String(v.act_digest ?? ""),
|
|
@@ -5545,35 +5965,34 @@ function accept(o) {
|
|
|
5545
5965
|
if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
|
|
5546
5966
|
await o.onVerdict?.(verdict, receiptOf(verdict, facts));
|
|
5547
5967
|
if (verdict.decision === "ESCALATED") {
|
|
5548
|
-
const
|
|
5549
|
-
return answer(verdict, { role:
|
|
5968
|
+
const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
|
|
5969
|
+
return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
|
|
5550
5970
|
}
|
|
5551
5971
|
if (o.mode === "shadow") return { ...answer(verdict), status: 200, body: { ...answer(verdict).body, shadow: true } };
|
|
5552
5972
|
return answer(verdict);
|
|
5553
5973
|
}
|
|
5554
5974
|
try {
|
|
5555
5975
|
return await store.transaction(async (t) => {
|
|
5556
|
-
if (!await t.claimNonce(act2.nonce))
|
|
5557
|
-
throw new Rollback(answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) }));
|
|
5558
|
-
}
|
|
5976
|
+
if (!await t.claimNonce(act2.nonce)) throw new LostRace();
|
|
5559
5977
|
let final = verdict, finalFacts = facts;
|
|
5560
5978
|
if (o.localFacts) {
|
|
5561
5979
|
const fresh = await o.localFacts(act2, t.tx);
|
|
5562
5980
|
if (!sameFacts(fresh, facts)) {
|
|
5563
5981
|
finalFacts = fresh;
|
|
5564
|
-
final =
|
|
5982
|
+
final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
|
|
5565
5983
|
if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
|
|
5566
5984
|
}
|
|
5567
5985
|
}
|
|
5568
5986
|
const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
|
|
5569
5987
|
const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
|
|
5570
5988
|
const receipt = receiptOf(final, finalFacts, now());
|
|
5571
|
-
await t.putReceipt(act2.nonce, receipt);
|
|
5989
|
+
await t.putReceipt(act2.nonce, receipt, act2);
|
|
5572
5990
|
await o.onVerdict?.(final, receipt);
|
|
5573
5991
|
return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
|
|
5574
5992
|
});
|
|
5575
5993
|
} catch (e) {
|
|
5576
5994
|
if (e instanceof Rollback) return e.answer;
|
|
5995
|
+
if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
|
|
5577
5996
|
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) } };
|
|
5578
5997
|
}
|
|
5579
5998
|
}
|
|
@@ -5612,7 +6031,7 @@ function accept(o) {
|
|
|
5612
6031
|
if (size > maxActBytes) break;
|
|
5613
6032
|
chunks.push(c);
|
|
5614
6033
|
}
|
|
5615
|
-
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) :
|
|
6034
|
+
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) : concat2(...chunks);
|
|
5616
6035
|
}
|
|
5617
6036
|
const a = await handle({ method: req.method, headers: req.headers, body });
|
|
5618
6037
|
send(a.status, a.headers, JSON.stringify(a.body));
|
|
@@ -5623,7 +6042,7 @@ function accept(o) {
|
|
|
5623
6042
|
function consentOrigins(act2) {
|
|
5624
6043
|
const origins = (Array.isArray(act2?.consents) ? act2.consents : []).filter((c) => c?.algorithm === "WebAuthn-ES256" && typeof c.client_data_json === "string").map((c) => {
|
|
5625
6044
|
try {
|
|
5626
|
-
return JSON.parse(
|
|
6045
|
+
return JSON.parse(fromUtf82(fromBase64url2(c.client_data_json))).origin;
|
|
5627
6046
|
} catch {
|
|
5628
6047
|
return null;
|
|
5629
6048
|
}
|
|
@@ -5631,8 +6050,74 @@ function consentOrigins(act2) {
|
|
|
5631
6050
|
return origins.length ? { consent_origins: [...new Set(origins)] } : {};
|
|
5632
6051
|
}
|
|
5633
6052
|
|
|
6053
|
+
// src/act/postgres.ts
|
|
6054
|
+
var IDENT = /^[a-z_][a-z0-9_]{0,62}(\.[a-z_][a-z0-9_]{0,62})?$/;
|
|
6055
|
+
function postgresStore(pool, opts = {}) {
|
|
6056
|
+
const table = opts.table ?? "nomos_acts";
|
|
6057
|
+
if (!IDENT.test(table)) throw new Error(`postgresStore: "${table}" is not a plain lower-case table name`);
|
|
6058
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${table} (
|
|
6059
|
+
nonce text PRIMARY KEY, -- SPEC-008 \xA75.2: the nonce ledger
|
|
6060
|
+
act_id text NOT NULL,
|
|
6061
|
+
receipt json NOT NULL, -- SPEC-008 \xA79; json, not jsonb: kept byte for byte
|
|
6062
|
+
act json NOT NULL, -- the exact Act verified, for re-verification
|
|
6063
|
+
committed_at timestamptz NOT NULL DEFAULT now()
|
|
6064
|
+
);`;
|
|
6065
|
+
return {
|
|
6066
|
+
schema,
|
|
6067
|
+
async migrate() {
|
|
6068
|
+
await pool.query(schema);
|
|
6069
|
+
},
|
|
6070
|
+
async hasNonce(nonce) {
|
|
6071
|
+
const r = await pool.query(`SELECT 1 FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
6072
|
+
return (r.rowCount ?? r.rows.length) > 0;
|
|
6073
|
+
},
|
|
6074
|
+
async receiptFor(nonce) {
|
|
6075
|
+
const r = await pool.query(`SELECT receipt FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
6076
|
+
return r.rows[0]?.receipt ?? null;
|
|
6077
|
+
},
|
|
6078
|
+
async transaction(fn) {
|
|
6079
|
+
const client = await pool.connect();
|
|
6080
|
+
let claimed = null, completed = false;
|
|
6081
|
+
try {
|
|
6082
|
+
await client.query("BEGIN");
|
|
6083
|
+
const out = await fn({
|
|
6084
|
+
tx: client,
|
|
6085
|
+
async claimNonce(nonce) {
|
|
6086
|
+
const r = await client.query(
|
|
6087
|
+
`INSERT INTO ${table} (nonce, act_id, receipt, act) VALUES ($1, '', '{}'::json, '{}'::json)
|
|
6088
|
+
ON CONFLICT (nonce) DO NOTHING RETURNING nonce`,
|
|
6089
|
+
[nonce]
|
|
6090
|
+
);
|
|
6091
|
+
const won = (r.rowCount ?? r.rows.length) === 1;
|
|
6092
|
+
if (won) claimed = nonce;
|
|
6093
|
+
return won;
|
|
6094
|
+
},
|
|
6095
|
+
async putReceipt(nonce, receipt, act2) {
|
|
6096
|
+
if (nonce !== claimed) throw new Error("postgresStore: putReceipt for a nonce this transaction did not claim");
|
|
6097
|
+
await client.query(
|
|
6098
|
+
`UPDATE ${table} SET act_id = $2, receipt = $3::json, act = $4::json WHERE nonce = $1`,
|
|
6099
|
+
[nonce, receipt.act_id, JSON.stringify(receipt), JSON.stringify(act2 ?? {})]
|
|
6100
|
+
);
|
|
6101
|
+
completed = true;
|
|
6102
|
+
}
|
|
6103
|
+
});
|
|
6104
|
+
if (claimed && !completed) throw new Error("postgresStore: the transaction claimed a nonce but stored no receipt");
|
|
6105
|
+
await client.query("COMMIT");
|
|
6106
|
+
client.release();
|
|
6107
|
+
return out;
|
|
6108
|
+
} catch (e) {
|
|
6109
|
+
let broken;
|
|
6110
|
+
await client.query("ROLLBACK").catch((err) => {
|
|
6111
|
+
broken = err;
|
|
6112
|
+
});
|
|
6113
|
+
client.release(broken);
|
|
6114
|
+
throw e;
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
};
|
|
6118
|
+
}
|
|
6119
|
+
|
|
5634
6120
|
// src/act/statements.ts
|
|
5635
|
-
import { randomBytes as randomBytes3, randomUUID } from "crypto";
|
|
5636
6121
|
async function certify(parent, child, opts) {
|
|
5637
6122
|
const unsigned = {
|
|
5638
6123
|
parent_kid: parent.kid,
|
|
@@ -5687,7 +6172,7 @@ function buildAct(o) {
|
|
|
5687
6172
|
const act2 = {
|
|
5688
6173
|
act_version: "1",
|
|
5689
6174
|
act_id: `act-${randomUUID()}`,
|
|
5690
|
-
nonce: randomBytes3(16)
|
|
6175
|
+
nonce: toBase642(randomBytes3(16)),
|
|
5691
6176
|
// Issued a second in the past so a relying party's clock a moment behind ours still accepts it.
|
|
5692
6177
|
issued_at: new Date(now.getTime() - 1e3).toISOString(),
|
|
5693
6178
|
expires_at: new Date(now.getTime() - 1e3 + window * 1e3).toISOString(),
|
|
@@ -5701,6 +6186,575 @@ function buildAct(o) {
|
|
|
5701
6186
|
return act2;
|
|
5702
6187
|
}
|
|
5703
6188
|
|
|
6189
|
+
// src/act/registry.ts
|
|
6190
|
+
function memoryRegistryStore() {
|
|
6191
|
+
const certs = [], sts = [];
|
|
6192
|
+
return {
|
|
6193
|
+
async certificates() {
|
|
6194
|
+
return [...certs];
|
|
6195
|
+
},
|
|
6196
|
+
async addCertificate(c) {
|
|
6197
|
+
if (!certs.some((x) => fingerprint(x) === fingerprint(c))) certs.push(c);
|
|
6198
|
+
},
|
|
6199
|
+
async statements() {
|
|
6200
|
+
return [...sts];
|
|
6201
|
+
},
|
|
6202
|
+
async addStatement(s) {
|
|
6203
|
+
sts.push(s);
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
var RegistryError = class extends Error {
|
|
6208
|
+
constructor(message, code) {
|
|
6209
|
+
super(message);
|
|
6210
|
+
this.code = code;
|
|
6211
|
+
this.name = "RegistryError";
|
|
6212
|
+
}
|
|
6213
|
+
};
|
|
6214
|
+
var any = () => ({ ok: true });
|
|
6215
|
+
function certificateRegistry(o) {
|
|
6216
|
+
const rootKid = kidOf(o.root);
|
|
6217
|
+
if (!rootKid) throw new RegistryError("certificateRegistry: the root is not a canonical Ed25519 or P-256 public key", "bad_root");
|
|
6218
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6219
|
+
async function revokedSets() {
|
|
6220
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
6221
|
+
for (const s of await o.store.statements()) s.revoked_kid ? kids.add(s.revoked_kid) : s.revoked_fingerprint && certs.add(s.revoked_fingerprint);
|
|
6222
|
+
return { kids, certs };
|
|
6223
|
+
}
|
|
6224
|
+
async function scopesOf(kid) {
|
|
6225
|
+
if (kid === rootKid) return { ok: true, scopes: [{ terms: {} }] };
|
|
6226
|
+
const { kids, certs } = await revokedSets();
|
|
6227
|
+
const r = resolveKey({ targetKid: kid, pool: await o.store.certificates(), rootPublicKeyPem: o.root, purpose: any, now: now(), revokedKids: kids, revokedCerts: certs, allPaths: true });
|
|
6228
|
+
if (r.decision !== "RESOLVED") return { ok: false, reason: `${r.decision}${r.reason_code ? ` (${r.reason_code})` : ""}` };
|
|
6229
|
+
return { ok: true, scopes: r.admitted ?? [r.effective] };
|
|
6230
|
+
}
|
|
6231
|
+
async function pool(filter) {
|
|
6232
|
+
const { kids, certs } = await revokedSets(), t = now().getTime();
|
|
6233
|
+
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)));
|
|
6234
|
+
}
|
|
6235
|
+
return {
|
|
6236
|
+
rootKid,
|
|
6237
|
+
pool,
|
|
6238
|
+
/** NOMOS-SPEC-007 §3: certify `child` under `parent`. Refuses anything a verifier would refuse. */
|
|
6239
|
+
async issue(io) {
|
|
6240
|
+
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");
|
|
6241
|
+
const scope = parseScope(io.scope ?? null);
|
|
6242
|
+
if (!scope.ok) throw new RegistryError(`Malformed scope: ${scope.detail}`, "malformed_scope");
|
|
6243
|
+
const parentScopes = await scopesOf(io.parent.kid);
|
|
6244
|
+
if (!parentScopes.ok) throw new RegistryError(`The parent ${io.parent.kid} is not reached from the root: ${parentScopes.reason}`, "parent_not_recognized");
|
|
6245
|
+
const fits = parentScopes.scopes.filter((p) => suitePermitted(p, io.parent.algorithm) && isNarrowerOrEqual(scope.scope, p).ok);
|
|
6246
|
+
if (!fits.length) {
|
|
6247
|
+
const widened = parentScopes.scopes.map((p) => isNarrowerOrEqual(scope.scope, p)).find((x) => !x.ok);
|
|
6248
|
+
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");
|
|
6249
|
+
}
|
|
6250
|
+
const expiresAt = io.expiresAt ?? (io.days ? new Date(now().getTime() + io.days * 864e5) : void 0);
|
|
6251
|
+
if (!expiresAt) throw new RegistryError("Give expiresAt or days: a certificate always expires", "no_expiry");
|
|
6252
|
+
const c = await certify(io.parent, io.child, { scope: io.scope, issuedAt: io.issuedAt ?? now(), expiresAt });
|
|
6253
|
+
await o.store.addCertificate(c);
|
|
6254
|
+
return c;
|
|
6255
|
+
},
|
|
6256
|
+
/** rev. 1.10 §5.1: `parent` revokes a key it certified. The key is dead on every path. */
|
|
6257
|
+
async revokeKey(io) {
|
|
6258
|
+
const certs = (await o.store.certificates()).filter((c) => c.child_kid === io.kid && c.parent_kid === io.parent.kid);
|
|
6259
|
+
if (!certs.length) throw new RegistryError(`${io.parent.kid} never certified ${io.kid}: only a key's parent can revoke it`, "not_the_parent");
|
|
6260
|
+
const st = await signRevocation(io.parent, { kid: io.kid }, io.reason, now());
|
|
6261
|
+
await o.store.addStatement(st);
|
|
6262
|
+
return st;
|
|
6263
|
+
},
|
|
6264
|
+
/** rev. 1.10 §5.1: withdraw one certificate; the key stays valid under any other. */
|
|
6265
|
+
async revokeCertificate(io) {
|
|
6266
|
+
const c = (await o.store.certificates()).find((x) => fingerprint(x) === io.fingerprint);
|
|
6267
|
+
if (!c) throw new RegistryError(`No certificate ${io.fingerprint} in this registry`, "unknown_certificate");
|
|
6268
|
+
const st = await signRevocation(io.parent, { certificate: c }, io.reason, now());
|
|
6269
|
+
await o.store.addStatement(st);
|
|
6270
|
+
return st;
|
|
6271
|
+
},
|
|
6272
|
+
/** The certificates that connect `kid` to the root: what a witness or approver returns with
|
|
6273
|
+
* what it signs (SPEC-009 0.2.0 §4, §5). */
|
|
6274
|
+
async chainFor(kid) {
|
|
6275
|
+
return presentedCertificates(await pool(), { authority: { artifact: { seal: { kid } } } });
|
|
6276
|
+
},
|
|
6277
|
+
/** How `kid` is reached from the root, and with which effective scopes, or why it is not. */
|
|
6278
|
+
async check(kid) {
|
|
6279
|
+
return scopesOf(kid);
|
|
6280
|
+
},
|
|
6281
|
+
certificates: () => o.store.certificates(),
|
|
6282
|
+
statements: () => o.store.statements(),
|
|
6283
|
+
/** rev. 1.10 §5.3: the signed list, carrying the certificates that make it checkable. The
|
|
6284
|
+
* signer should be reached from the root, or the list establishes no date (§5.3). */
|
|
6285
|
+
async revocationList(signer) {
|
|
6286
|
+
const statements = await o.store.statements();
|
|
6287
|
+
return signRevocationList(signer, statements, revocationListCertificates(await o.store.certificates(), statements, signer.kid), now());
|
|
6288
|
+
}
|
|
6289
|
+
};
|
|
6290
|
+
}
|
|
6291
|
+
function postgresRegistryStore(pool, opts = {}) {
|
|
6292
|
+
const p = opts.prefix ?? "nomos_registry";
|
|
6293
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new RegistryError(`"${p}" is not a plain lower-case prefix`, "bad_prefix");
|
|
6294
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_certificates (
|
|
6295
|
+
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()
|
|
6296
|
+
);
|
|
6297
|
+
CREATE TABLE IF NOT EXISTS ${p}_statements (
|
|
6298
|
+
id bigserial PRIMARY KEY, target text NOT NULL, kid text NOT NULL, statement json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
|
|
6299
|
+
);`;
|
|
6300
|
+
return {
|
|
6301
|
+
schema,
|
|
6302
|
+
async migrate() {
|
|
6303
|
+
await pool.query(schema);
|
|
6304
|
+
},
|
|
6305
|
+
async certificates() {
|
|
6306
|
+
return (await pool.query(`SELECT certificate FROM ${p}_certificates ORDER BY added_at, fingerprint`)).rows.map((r) => r.certificate);
|
|
6307
|
+
},
|
|
6308
|
+
async addCertificate(c) {
|
|
6309
|
+
await pool.query(
|
|
6310
|
+
`INSERT INTO ${p}_certificates (fingerprint, child_kid, parent_kid, certificate) VALUES ($1, $2, $3, $4::json) ON CONFLICT (fingerprint) DO NOTHING`,
|
|
6311
|
+
[fingerprint(c), c.child_kid, c.parent_kid, JSON.stringify(c)]
|
|
6312
|
+
);
|
|
6313
|
+
},
|
|
6314
|
+
async statements() {
|
|
6315
|
+
return (await pool.query(`SELECT statement FROM ${p}_statements ORDER BY id`)).rows.map((r) => r.statement);
|
|
6316
|
+
},
|
|
6317
|
+
async addStatement(s) {
|
|
6318
|
+
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)]);
|
|
6319
|
+
}
|
|
6320
|
+
};
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6323
|
+
// src/act/passkeys.ts
|
|
6324
|
+
function cbor(b) {
|
|
6325
|
+
let i = 0;
|
|
6326
|
+
const len = (info) => {
|
|
6327
|
+
if (info < 24) return info;
|
|
6328
|
+
const n = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : -1;
|
|
6329
|
+
if (n < 0) throw new Error("unsupported CBOR length");
|
|
6330
|
+
let v2 = 0;
|
|
6331
|
+
for (let k = 0; k < n; k++) v2 = v2 * 256 + b[i++];
|
|
6332
|
+
return v2;
|
|
6333
|
+
};
|
|
6334
|
+
const item = (depth) => {
|
|
6335
|
+
if (depth > 16 || i >= b.length) throw new Error("malformed CBOR");
|
|
6336
|
+
const head = b[i++], major = head >> 5, info = head & 31;
|
|
6337
|
+
switch (major) {
|
|
6338
|
+
case 0:
|
|
6339
|
+
return len(info);
|
|
6340
|
+
case 1:
|
|
6341
|
+
return -1 - len(info);
|
|
6342
|
+
case 2: {
|
|
6343
|
+
const n = len(info);
|
|
6344
|
+
const v2 = b.slice(i, i + n);
|
|
6345
|
+
i += n;
|
|
6346
|
+
if (v2.length !== n) throw new Error("truncated CBOR");
|
|
6347
|
+
return v2;
|
|
6348
|
+
}
|
|
6349
|
+
case 3: {
|
|
6350
|
+
const n = len(info);
|
|
6351
|
+
const v2 = fromUtf82(b.slice(i, i + n));
|
|
6352
|
+
i += n;
|
|
6353
|
+
return v2;
|
|
6354
|
+
}
|
|
6355
|
+
case 4: {
|
|
6356
|
+
const n = len(info), a = [];
|
|
6357
|
+
for (let k = 0; k < n; k++) a.push(item(depth + 1));
|
|
6358
|
+
return a;
|
|
6359
|
+
}
|
|
6360
|
+
case 5: {
|
|
6361
|
+
const n = len(info), m = /* @__PURE__ */ new Map();
|
|
6362
|
+
for (let k = 0; k < n; k++) m.set(item(depth + 1), item(depth + 1));
|
|
6363
|
+
return m;
|
|
6364
|
+
}
|
|
6365
|
+
case 7:
|
|
6366
|
+
if (info === 20) return false;
|
|
6367
|
+
if (info === 21) return true;
|
|
6368
|
+
if (info === 22) return null;
|
|
6369
|
+
throw new Error("unsupported CBOR simple value");
|
|
6370
|
+
default:
|
|
6371
|
+
throw new Error("unsupported CBOR type");
|
|
6372
|
+
}
|
|
6373
|
+
};
|
|
6374
|
+
const v = item(0);
|
|
6375
|
+
return { value: v, end: i };
|
|
6376
|
+
}
|
|
6377
|
+
function readAttestation(attestationObject) {
|
|
6378
|
+
const att = cbor(attestationObject).value;
|
|
6379
|
+
if (!(att instanceof Map) || !(att.get("authData") instanceof Uint8Array)) throw new Error("not an attestation object");
|
|
6380
|
+
const a = att.get("authData");
|
|
6381
|
+
if (a.length < 55 || !(a[32] & 64)) throw new Error("no attested credential data");
|
|
6382
|
+
const idLen = a[53] << 8 | a[54];
|
|
6383
|
+
const credentialId = a.slice(55, 55 + idLen);
|
|
6384
|
+
const cose = cbor(a.slice(55 + idLen)).value;
|
|
6385
|
+
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)");
|
|
6386
|
+
const x = cose.get(-2), y = cose.get(-3);
|
|
6387
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) throw new Error("malformed COSE key");
|
|
6388
|
+
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);
|
|
6389
|
+
const publicKeyPem = pem("PUBLIC KEY", spki);
|
|
6390
|
+
if (!kidOf(publicKeyPem)) throw new Error("the passkey's key is not a valid P-256 point");
|
|
6391
|
+
return { authData: a, flags: a[32], credentialId, publicKeyPem };
|
|
6392
|
+
}
|
|
6393
|
+
async function signEnrolmentToken(authority, c) {
|
|
6394
|
+
if (!/^[^,\s]+$/.test(c.approver)) throw new Error("An approver id has no commas or spaces (NOMOS-SPEC-007 rev. 1.9 \xA73.7)");
|
|
6395
|
+
const claims = { approver: c.approver, roles: [...c.roles], origin: c.origin.replace(/\/$/, ""), expires_at: c.expiresAt.toISOString(), nonce: toBase64url2(randomBytes3(16)) };
|
|
6396
|
+
const token = { ...claims, kid: authority.kid, algorithm: authority.algorithm, signature: await authority.sign(jcs(claims)) };
|
|
6397
|
+
return toBase64url2(utf82(JSON.stringify(token)));
|
|
6398
|
+
}
|
|
6399
|
+
function verifyEnrolmentToken(token, authorityPem, origin, now) {
|
|
6400
|
+
let t;
|
|
6401
|
+
try {
|
|
6402
|
+
t = JSON.parse(fromUtf82(fromBase64url2(token)));
|
|
6403
|
+
} catch {
|
|
6404
|
+
return { error: "not an enrolment link" };
|
|
6405
|
+
}
|
|
6406
|
+
const claims = { approver: t?.approver, roles: t?.roles, origin: t?.origin, expires_at: t?.expires_at, nonce: t?.nonce };
|
|
6407
|
+
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" };
|
|
6408
|
+
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" };
|
|
6409
|
+
if (claims.origin !== origin) return { error: "this enrolment link is for another service" };
|
|
6410
|
+
if (!(Date.parse(claims.expires_at) > now.getTime())) return { error: "this enrolment link has expired" };
|
|
6411
|
+
return claims;
|
|
6412
|
+
}
|
|
6413
|
+
function memoryPasskeyStore() {
|
|
6414
|
+
const used = /* @__PURE__ */ new Set(), challenges = /* @__PURE__ */ new Map(), creds = [], pend = /* @__PURE__ */ new Map();
|
|
6415
|
+
return {
|
|
6416
|
+
async claimToken(n) {
|
|
6417
|
+
if (used.has(n)) return false;
|
|
6418
|
+
used.add(n);
|
|
6419
|
+
return true;
|
|
6420
|
+
},
|
|
6421
|
+
async tokenUsed(n) {
|
|
6422
|
+
return used.has(n);
|
|
6423
|
+
},
|
|
6424
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6425
|
+
if (!used.has(n)) challenges.set(n, c);
|
|
6426
|
+
},
|
|
6427
|
+
async enrolmentChallenge(n) {
|
|
6428
|
+
return used.has(n) ? null : challenges.get(n) ?? null;
|
|
6429
|
+
},
|
|
6430
|
+
async addCredential(c) {
|
|
6431
|
+
creds.push(structuredClone(c));
|
|
6432
|
+
},
|
|
6433
|
+
async credentials() {
|
|
6434
|
+
return structuredClone(creds);
|
|
6435
|
+
},
|
|
6436
|
+
async createPending(p) {
|
|
6437
|
+
pend.set(p.id, structuredClone(p));
|
|
6438
|
+
},
|
|
6439
|
+
async pending(id) {
|
|
6440
|
+
const p = pend.get(id);
|
|
6441
|
+
return p ? structuredClone(p) : null;
|
|
6442
|
+
},
|
|
6443
|
+
async addAttempt(id, c, a) {
|
|
6444
|
+
const p = pend.get(id);
|
|
6445
|
+
if (!p || p.status !== "pending") return false;
|
|
6446
|
+
p.attempts[c] = { ...a };
|
|
6447
|
+
return true;
|
|
6448
|
+
},
|
|
6449
|
+
async resolvePending(id, out) {
|
|
6450
|
+
const p = pend.get(id);
|
|
6451
|
+
if (!p || p.status !== "pending") return false;
|
|
6452
|
+
Object.assign(p, structuredClone(out));
|
|
6453
|
+
return true;
|
|
6454
|
+
}
|
|
6455
|
+
};
|
|
6456
|
+
}
|
|
6457
|
+
function postgresPasskeyStore(pool, opts = {}) {
|
|
6458
|
+
const p = opts.prefix ?? "nomos_passkeys";
|
|
6459
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new Error(`"${p}" is not a plain lower-case prefix`);
|
|
6460
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_tokens (nonce text PRIMARY KEY, challenge text, used boolean NOT NULL DEFAULT false);
|
|
6461
|
+
CREATE TABLE IF NOT EXISTS ${p}_credentials (credential_id text PRIMARY KEY, credential json NOT NULL, added_at timestamptz NOT NULL DEFAULT now());
|
|
6462
|
+
CREATE TABLE IF NOT EXISTS ${p}_pending (id text PRIMARY KEY, approval jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now());`;
|
|
6463
|
+
return {
|
|
6464
|
+
schema,
|
|
6465
|
+
async migrate() {
|
|
6466
|
+
await pool.query(schema);
|
|
6467
|
+
},
|
|
6468
|
+
async claimToken(n) {
|
|
6469
|
+
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]);
|
|
6470
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6471
|
+
},
|
|
6472
|
+
async tokenUsed(n) {
|
|
6473
|
+
return (await pool.query(`SELECT 1 FROM ${p}_tokens WHERE nonce = $1 AND used = true`, [n])).rows.length > 0;
|
|
6474
|
+
},
|
|
6475
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6476
|
+
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]);
|
|
6477
|
+
},
|
|
6478
|
+
async enrolmentChallenge(n) {
|
|
6479
|
+
return (await pool.query(`SELECT challenge FROM ${p}_tokens WHERE nonce = $1 AND used = false`, [n])).rows[0]?.challenge ?? null;
|
|
6480
|
+
},
|
|
6481
|
+
async addCredential(c) {
|
|
6482
|
+
await pool.query(`INSERT INTO ${p}_credentials (credential_id, credential) VALUES ($1, $2::json)`, [c.credential_id, JSON.stringify(c)]);
|
|
6483
|
+
},
|
|
6484
|
+
async credentials() {
|
|
6485
|
+
return (await pool.query(`SELECT credential FROM ${p}_credentials ORDER BY added_at`)).rows.map((r) => r.credential);
|
|
6486
|
+
},
|
|
6487
|
+
async createPending(a) {
|
|
6488
|
+
await pool.query(`INSERT INTO ${p}_pending (id, approval) VALUES ($1, $2::jsonb)`, [a.id, JSON.stringify(a)]);
|
|
6489
|
+
},
|
|
6490
|
+
async pending(id) {
|
|
6491
|
+
return (await pool.query(`SELECT approval FROM ${p}_pending WHERE id = $1`, [id])).rows[0]?.approval ?? null;
|
|
6492
|
+
},
|
|
6493
|
+
// Each change is one conditional statement: Postgres serializes them on the row, and none
|
|
6494
|
+
// rewrites the whole record, so a stale reader cannot write an old status back.
|
|
6495
|
+
async addAttempt(id, c, a) {
|
|
6496
|
+
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)]);
|
|
6497
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6498
|
+
},
|
|
6499
|
+
async resolvePending(id, out) {
|
|
6500
|
+
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)]);
|
|
6501
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6502
|
+
}
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6505
|
+
var MAX_ATTEMPTS = 50;
|
|
6506
|
+
var esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6507
|
+
function passkeyApprover(o) {
|
|
6508
|
+
const base = o.origin.replace(/\/$/, "");
|
|
6509
|
+
const rpId = o.rpId ?? new URL(base).hostname;
|
|
6510
|
+
const store = o.store ?? memoryPasskeyStore();
|
|
6511
|
+
if (!o.store) console.warn("[nomos] passkeyApprover(): using the in-memory store. Enrolled passkeys are lost on restart. Development only; use postgresPasskeyStore() in production.");
|
|
6512
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6513
|
+
const rpIdHash = sha256(utf82(rpId));
|
|
6514
|
+
const expired = (p) => p.status === "pending" && Date.parse(p.act.expires_at) <= now().getTime();
|
|
6515
|
+
const statusOf = (p) => expired(p) ? "expired" : p.status;
|
|
6516
|
+
function clientData(b64, type, challenge) {
|
|
6517
|
+
let c;
|
|
6518
|
+
try {
|
|
6519
|
+
c = JSON.parse(fromUtf82(fromBase64url2(b64)));
|
|
6520
|
+
} catch {
|
|
6521
|
+
return "unreadable client data";
|
|
6522
|
+
}
|
|
6523
|
+
if (c?.type !== type) return `expected a ${type} ceremony`;
|
|
6524
|
+
if (c?.challenge !== challenge) return "the challenge does not match";
|
|
6525
|
+
if (c?.origin !== base) return `the ceremony ran on ${c?.origin}, not ${base}`;
|
|
6526
|
+
return null;
|
|
6527
|
+
}
|
|
6528
|
+
async function route(method, path, json) {
|
|
6529
|
+
if (method === "POST" && path === "/nomos/approvals") {
|
|
6530
|
+
const { role, act: act2 } = await json();
|
|
6531
|
+
if (!o.roles.includes(role)) return { status: 404, body: { error: `This service does not approve as ${role}.` } };
|
|
6532
|
+
if (typeof act2?.expires_at !== "string" || !act2?.action) return { status: 400, body: { error: "The request needs the Act." } };
|
|
6533
|
+
const id = randomUUID();
|
|
6534
|
+
await store.createPending({ id, role, act: act2, status: "pending", created_at: now().toISOString(), attempts: {} });
|
|
6535
|
+
await o.onPending?.({ id, role, act: act2, approvalUrl: `${base}/approve/${id}` });
|
|
6536
|
+
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
6537
|
+
}
|
|
6538
|
+
let m = path.match(/^\/nomos\/approvals\/([0-9a-f-]{36})$/);
|
|
6539
|
+
if (method === "GET" && m) {
|
|
6540
|
+
const p = await store.pending(m[1]);
|
|
6541
|
+
if (!p) return null;
|
|
6542
|
+
const s = statusOf(p);
|
|
6543
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...p.key_certs?.length ? { key_certs: p.key_certs } : {} } : {} } };
|
|
6544
|
+
}
|
|
6545
|
+
m = path.match(/^\/approve\/([0-9a-f-]{36})(?:\/(devices|options|assertion|refuse))?$/);
|
|
6546
|
+
if (m) {
|
|
6547
|
+
const p = await store.pending(m[1]);
|
|
6548
|
+
if (!p) return null;
|
|
6549
|
+
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) };
|
|
6550
|
+
if (method !== "POST" || !m[2]) return null;
|
|
6551
|
+
if (statusOf(p) !== "pending") return { status: 409, body: { error: `This request is ${statusOf(p)}.` } };
|
|
6552
|
+
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." } };
|
|
6553
|
+
const req = await json();
|
|
6554
|
+
if (m[2] === "devices") {
|
|
6555
|
+
const mine = (await store.credentials()).filter((c) => c.approver === String(req?.approver ?? "") && c.roles.includes(p.role));
|
|
6556
|
+
return { status: 200, body: { devices: mine.map((c) => ({ credential_id: c.credential_id, label: c.label })) } };
|
|
6557
|
+
}
|
|
6558
|
+
const cred = (await store.credentials()).find((c) => c.credential_id === req?.credential_id);
|
|
6559
|
+
if (!cred || !cred.roles.includes(p.role)) return { status: 403, body: { error: `That passkey does not approve as ${p.role}.` } };
|
|
6560
|
+
if (m[2] === "options") {
|
|
6561
|
+
const signed_at = now().toISOString();
|
|
6562
|
+
const payload2 = consentPayload({ binding_digest: bindingDigest(p.act), role: p.role, decision: "approve", signed_at, kid: cred.kid });
|
|
6563
|
+
const challenge = toBase64url2(sha256(jcs(payload2)));
|
|
6564
|
+
if (Object.keys(p.attempts).length >= MAX_ATTEMPTS) return { status: 429, body: { error: "Too many approval ceremonies for this request." } };
|
|
6565
|
+
const expires_at = new Date(Math.min(Date.parse(p.act.expires_at), now().getTime() + 5 * 6e4)).toISOString();
|
|
6566
|
+
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." } };
|
|
6567
|
+
return { status: 200, body: { challenge, rpId, allowCredentials: [{ type: "public-key", id: cred.credential_id }], userVerification: "required", timeout: 12e4 } };
|
|
6568
|
+
}
|
|
6569
|
+
let offered;
|
|
6570
|
+
try {
|
|
6571
|
+
offered = JSON.parse(fromUtf82(fromBase64url2(String(req.client_data_json ?? "")))).challenge;
|
|
6572
|
+
} catch {
|
|
6573
|
+
return { status: 400, body: { error: "unreadable client data" } };
|
|
6574
|
+
}
|
|
6575
|
+
const attempt = p.attempts[offered];
|
|
6576
|
+
if (!attempt || attempt.credential_id !== cred.credential_id) return { status: 400, body: { error: "This ceremony was not offered for this passkey." } };
|
|
6577
|
+
if (Date.parse(attempt.expires_at) <= now().getTime()) return { status: 400, body: { error: "This ceremony has expired; start again." } };
|
|
6578
|
+
const consent = {
|
|
6579
|
+
binding_digest: bindingDigest(p.act),
|
|
6580
|
+
role: p.role,
|
|
6581
|
+
decision: "approve",
|
|
6582
|
+
signed_at: attempt.signed_at,
|
|
6583
|
+
kid: cred.kid,
|
|
6584
|
+
algorithm: "WebAuthn-ES256",
|
|
6585
|
+
authenticator_data: String(req.authenticator_data ?? ""),
|
|
6586
|
+
client_data_json: String(req.client_data_json ?? ""),
|
|
6587
|
+
signature: String(req.signature ?? "")
|
|
6588
|
+
};
|
|
6589
|
+
const payload = jcs(consentPayload(consent));
|
|
6590
|
+
const cd = clientData(consent.client_data_json, "webauthn.get", toBase64url2(sha256(payload)));
|
|
6591
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6592
|
+
const ad = fromBase64url2(consent.authenticator_data);
|
|
6593
|
+
if (ad.length < 37 || !equal2(ad.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The assertion is for another relying-party id." } };
|
|
6594
|
+
const v = verifySuite("WebAuthn-ES256", cred.public_key_pem, payload, consent);
|
|
6595
|
+
if (!v.ok) return { status: 400, body: { error: `The passkey's signature does not verify (${v.reason}).` } };
|
|
6596
|
+
const key_certs = await o.registry.chainFor(cred.kid);
|
|
6597
|
+
if (!key_certs.length) return { status: 409, body: { error: "This passkey is no longer certified (revoked or expired)." } };
|
|
6598
|
+
if (!await store.resolvePending(p.id, { status: "approved", consent, key_certs })) return { status: 409, body: { error: "This request is no longer pending." } };
|
|
6599
|
+
return { status: 200, body: { status: "approved" } };
|
|
6600
|
+
}
|
|
6601
|
+
if (method === "GET" && path === "/enroll") return { status: 200, type: "text/html", body: enrolPage(o.rpName ?? rpId) };
|
|
6602
|
+
if (method === "POST" && (path === "/nomos/enrol/begin" || path === "/nomos/enrol/finish")) {
|
|
6603
|
+
const req = await json();
|
|
6604
|
+
const claims = verifyEnrolmentToken(String(req?.token ?? ""), o.enrolmentAuthority, base, now());
|
|
6605
|
+
if ("error" in claims) return { status: 403, body: { error: claims.error } };
|
|
6606
|
+
if (!claims.roles.every((r) => o.roles.includes(r))) return { status: 403, body: { error: `This service does not enrol for ${claims.roles.join(", ")}.` } };
|
|
6607
|
+
if (path === "/nomos/enrol/begin") {
|
|
6608
|
+
if (await store.tokenUsed(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6609
|
+
const challenge2 = toBase64url2(randomBytes3(32));
|
|
6610
|
+
await store.saveEnrolmentChallenge(claims.nonce, challenge2);
|
|
6611
|
+
const existing = (await store.credentials()).filter((c) => c.approver === claims.approver);
|
|
6612
|
+
return { status: 200, body: {
|
|
6613
|
+
challenge: challenge2,
|
|
6614
|
+
rp: { id: rpId, name: o.rpName ?? rpId },
|
|
6615
|
+
user: { id: toBase64url2(utf82(claims.approver)), name: claims.approver, displayName: claims.approver },
|
|
6616
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
6617
|
+
authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
|
|
6618
|
+
attestation: "none",
|
|
6619
|
+
timeout: 12e4,
|
|
6620
|
+
excludeCredentials: existing.map((c) => ({ type: "public-key", id: c.credential_id })),
|
|
6621
|
+
approver: claims.approver,
|
|
6622
|
+
roles: claims.roles
|
|
6623
|
+
} };
|
|
6624
|
+
}
|
|
6625
|
+
const challenge = await store.enrolmentChallenge(claims.nonce);
|
|
6626
|
+
if (!challenge) return { status: 403, body: { error: "Start enrolment again: this link has been used or has no ceremony in progress." } };
|
|
6627
|
+
const cd = clientData(String(req.client_data_json ?? ""), "webauthn.create", challenge);
|
|
6628
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6629
|
+
let att;
|
|
6630
|
+
try {
|
|
6631
|
+
att = readAttestation(fromBase64url2(String(req.attestation_object ?? "")));
|
|
6632
|
+
} catch (e) {
|
|
6633
|
+
return { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6634
|
+
}
|
|
6635
|
+
if (!equal2(att.authData.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The passkey was created for another relying-party id." } };
|
|
6636
|
+
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)." } };
|
|
6637
|
+
if (toBase64url2(att.credentialId) !== req.credential_id) return { status: 400, body: { error: "The credential id does not match the attestation." } };
|
|
6638
|
+
if (!await store.claimToken(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6639
|
+
const kid = kidOf(att.publicKeyPem);
|
|
6640
|
+
const certificate = await o.registry.issue({
|
|
6641
|
+
parent: o.enroller,
|
|
6642
|
+
child: { kid, publicKeyPem: att.publicKeyPem },
|
|
6643
|
+
scope: `consent:${claims.roles.join(",")} approver:${claims.approver} suite:WebAuthn-ES256`,
|
|
6644
|
+
days: o.certificateDays ?? 365
|
|
6645
|
+
});
|
|
6646
|
+
const label = String(req.label ?? "").slice(0, 60) || "Passkey";
|
|
6647
|
+
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() });
|
|
6648
|
+
return { status: 200, body: { enrolled: true, approver: claims.approver, roles: claims.roles, kid, certificate } };
|
|
6649
|
+
}
|
|
6650
|
+
return null;
|
|
6651
|
+
}
|
|
6652
|
+
async function fetchHandler(request) {
|
|
6653
|
+
const out = await route(request.method, new URL(request.url).pathname, () => request.json()).catch((e) => ({ status: 400, body: { error: String(e?.message ?? e) } }));
|
|
6654
|
+
if (!out) return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });
|
|
6655
|
+
const html = out.type === "text/html";
|
|
6656
|
+
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() } : {} } });
|
|
6657
|
+
}
|
|
6658
|
+
function node() {
|
|
6659
|
+
return async (req, res, next) => {
|
|
6660
|
+
const path = String(req.originalUrl ?? req.url ?? "").split("?")[0];
|
|
6661
|
+
const json = async () => {
|
|
6662
|
+
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
6663
|
+
const chunks = [];
|
|
6664
|
+
for await (const c of req) chunks.push(c);
|
|
6665
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
6666
|
+
};
|
|
6667
|
+
let out;
|
|
6668
|
+
try {
|
|
6669
|
+
out = await route(req.method, path, json);
|
|
6670
|
+
} catch (e) {
|
|
6671
|
+
out = { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6672
|
+
}
|
|
6673
|
+
if (!out) {
|
|
6674
|
+
if (next) return next();
|
|
6675
|
+
res.statusCode = 404;
|
|
6676
|
+
return res.end(JSON.stringify({ error: "Not found" }));
|
|
6677
|
+
}
|
|
6678
|
+
res.statusCode = out.status;
|
|
6679
|
+
const html = out.type === "text/html";
|
|
6680
|
+
res.setHeader("content-type", html ? "text/html; charset=utf-8" : "application/json");
|
|
6681
|
+
if (html) res.setHeader("content-security-policy", CSP());
|
|
6682
|
+
res.end(html ? String(out.body) : JSON.stringify(out.body));
|
|
6683
|
+
};
|
|
6684
|
+
}
|
|
6685
|
+
return { store, fetch: fetchHandler, express: node, node };
|
|
6686
|
+
}
|
|
6687
|
+
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}
|
|
6688
|
+
@media (prefers-color-scheme:dark){body{color:#eee;background:#111}table td{border-color:#333}}
|
|
6689
|
+
h2{margin:0 0 12px}table{border-collapse:collapse;margin:12px 0}td{padding:6px 10px;border-bottom:1px solid #ddd}
|
|
6690
|
+
button{font:inherit;padding:10px 18px;border-radius:8px;border:1px solid #888;background:transparent;color:inherit;cursor:pointer;margin:4px 8px 4px 0}
|
|
6691
|
+
button.primary{background:#2563eb;border-color:#2563eb;color:#fff}.muted{color:#777;font-size:13px}#msg{margin-top:16px;font-weight:600}`;
|
|
6692
|
+
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))};
|
|
6693
|
+
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(()=>({}))}));
|
|
6694
|
+
const say=t=>{document.getElementById('msg').textContent=t};`;
|
|
6695
|
+
function approvalPage(p, status, devices) {
|
|
6696
|
+
const a = p.act.action ?? {};
|
|
6697
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc(k)}</td><td><b>${esc(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
6698
|
+
const device = (d) => `<button class="primary" data-cred="${esc(d.credential_id)}">Approve${d.approver ? ` as ${esc(d.approver)}` : ""} \xB7 ${esc(d.label)}</button>`;
|
|
6699
|
+
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>`;
|
|
6700
|
+
const body = status === "pending" ? `${choose}<button id="refuse">Refuse</button>` : `<p><b>${esc(status)}</b></p>`;
|
|
6701
|
+
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>
|
|
6702
|
+
<h2>Approve as ${esc(p.role)}?</h2>
|
|
6703
|
+
<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>
|
|
6704
|
+
<table>${params}</table>
|
|
6705
|
+
<p class="muted">Your passkey signs this exact change and nothing else. It expires at ${esc(p.act.expires_at)}.</p>
|
|
6706
|
+
${body}<div id="msg" data-id="${esc(p.id)}"></div>
|
|
6707
|
+
<script>${APPROVE_JS}</script>`;
|
|
6708
|
+
}
|
|
6709
|
+
var APPROVE_JS = `${B642}
|
|
6710
|
+
const id=document.getElementById('msg').dataset.id;
|
|
6711
|
+
async function approve(credential_id){
|
|
6712
|
+
try{
|
|
6713
|
+
const o=await post('/approve/'+id+'/options',{credential_id});
|
|
6714
|
+
if(!o.ok) return say(o.body.error||'Not allowed.');
|
|
6715
|
+
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}});
|
|
6716
|
+
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)});
|
|
6717
|
+
say(r.ok?'Approved. You can close this page.':(r.body.error||'Not approved.'));
|
|
6718
|
+
if(r.ok) document.querySelectorAll('button').forEach(b=>b.disabled=true);
|
|
6719
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6720
|
+
}
|
|
6721
|
+
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)}
|
|
6722
|
+
function wire(root){root.querySelectorAll('[data-cred]').forEach(b=>b.addEventListener('click',()=>approve(b.dataset.cred)))}
|
|
6723
|
+
wire(document);
|
|
6724
|
+
document.getElementById('refuse')?.addEventListener('click',refuse);
|
|
6725
|
+
document.getElementById('find')?.addEventListener('click',async()=>{
|
|
6726
|
+
const r=await post('/approve/'+id+'/devices',{approver:document.getElementById('who').value});
|
|
6727
|
+
const box=document.getElementById('devices');box.textContent='';
|
|
6728
|
+
if(!r.body.devices?.length) return say('No passkey of yours approves this.');
|
|
6729
|
+
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)}
|
|
6730
|
+
wire(box);
|
|
6731
|
+
});`;
|
|
6732
|
+
function enrolPage(rpName) {
|
|
6733
|
+
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>
|
|
6734
|
+
<h2>Enrol a passkey for ${esc(rpName)}</h2>
|
|
6735
|
+
<p id="who" class="muted">Checking your enrolment link\u2026</p>
|
|
6736
|
+
<p><label>Name this device <input id="label" value="" placeholder="e.g. iPhone"></label></p>
|
|
6737
|
+
<button class="primary" id="go" disabled>Create passkey</button>
|
|
6738
|
+
<div id="msg"></div>
|
|
6739
|
+
<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>
|
|
6740
|
+
<script>${ENROL_JS}</script>`;
|
|
6741
|
+
}
|
|
6742
|
+
var ENROL_JS = `${B642}
|
|
6743
|
+
const token=location.hash.slice(1);let opts=null;
|
|
6744
|
+
(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}
|
|
6745
|
+
opts=r.body;document.getElementById('who').textContent='Enrolling '+opts.approver+' to approve as '+opts.roles.join(', ')+'.';document.getElementById('go').disabled=false})();
|
|
6746
|
+
async function enrol(){
|
|
6747
|
+
try{
|
|
6748
|
+
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)}))}});
|
|
6749
|
+
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)});
|
|
6750
|
+
say(r.ok?'Enrolled. This device can now approve as '+r.body.roles.join(', ')+'.':(r.body.error||'Not enrolled.'));
|
|
6751
|
+
if(r.ok) document.getElementById('go').disabled=true;
|
|
6752
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6753
|
+
}
|
|
6754
|
+
document.getElementById('go').addEventListener('click',enrol);`;
|
|
6755
|
+
var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js))))}'`;
|
|
6756
|
+
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'`;
|
|
6757
|
+
|
|
5704
6758
|
// src/act/carrier.ts
|
|
5705
6759
|
var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
|
|
5706
6760
|
var NomosActError = class extends Error {
|
|
@@ -5737,6 +6791,7 @@ async function act(o) {
|
|
|
5737
6791
|
}
|
|
5738
6792
|
}
|
|
5739
6793
|
const testimony = [];
|
|
6794
|
+
const available = [...issuer2.key_certs ?? []];
|
|
5740
6795
|
const unavailable = [];
|
|
5741
6796
|
for (const claim of witnessed) {
|
|
5742
6797
|
const w = witnesses.find((x) => x.claims.includes(claim));
|
|
@@ -5760,8 +6815,10 @@ async function act(o) {
|
|
|
5760
6815
|
continue;
|
|
5761
6816
|
}
|
|
5762
6817
|
testimony.push(t);
|
|
6818
|
+
if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
|
|
5763
6819
|
}
|
|
5764
|
-
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts:
|
|
6820
|
+
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
|
|
6821
|
+
present(theAct, available);
|
|
5765
6822
|
let { status, body } = await submit(f, rp.act_endpoint, theAct);
|
|
5766
6823
|
let approvalUrl;
|
|
5767
6824
|
while (status === 202 && body.decision === "ESCALATED") {
|
|
@@ -5769,16 +6826,18 @@ async function act(o) {
|
|
|
5769
6826
|
const endpoint = (issuer2.approvals ?? []).find((a) => a.role === role)?.endpoint;
|
|
5770
6827
|
if (!endpoint) return done("escalated", body, theAct, { summary: `Needs approval from ${role}, and ${issuer2.issuer_id} publishes no approval endpoint for that role.` });
|
|
5771
6828
|
const deadline = Math.min(Date.parse(theAct.expires_at), Date.now() + (o.approvalTimeoutMs ?? Infinity));
|
|
5772
|
-
const
|
|
6829
|
+
const answer2 = await requestConsent(f, endpoint, role, theAct, deadline, o.pollMs ?? 1e3, (url) => {
|
|
5773
6830
|
approvalUrl = url;
|
|
5774
6831
|
o.onApproval?.({ role, url, actDigest: body.act_digest });
|
|
5775
6832
|
});
|
|
5776
|
-
if (
|
|
5777
|
-
if (!
|
|
6833
|
+
if (answer2 === "refused") return done("denied", body, theAct, { summary: `The ${role} refused this action.` });
|
|
6834
|
+
if (!answer2) {
|
|
5778
6835
|
const expired = Date.now() >= Date.parse(theAct.expires_at);
|
|
5779
6836
|
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}.` });
|
|
5780
6837
|
}
|
|
5781
|
-
theAct.consents.push(consent);
|
|
6838
|
+
theAct.consents.push(answer2.consent);
|
|
6839
|
+
if (Array.isArray(answer2.keyCerts)) available.push(...answer2.keyCerts);
|
|
6840
|
+
present(theAct, available);
|
|
5782
6841
|
const before = Number(body.consents_valid ?? 0);
|
|
5783
6842
|
({ status, body } = await submit(f, rp.act_endpoint, theAct));
|
|
5784
6843
|
if (status === 202 && Number(body.consents_valid ?? 0) <= before) {
|
|
@@ -5799,6 +6858,13 @@ async function act(o) {
|
|
|
5799
6858
|
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 : ""}`
|
|
5800
6859
|
});
|
|
5801
6860
|
}
|
|
6861
|
+
function present(theAct, available) {
|
|
6862
|
+
const set = presentedCertificates(available, theAct);
|
|
6863
|
+
if (set.length > MAX_PRESENTED) {
|
|
6864
|
+
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.`);
|
|
6865
|
+
}
|
|
6866
|
+
theAct.authority.key_certs = set;
|
|
6867
|
+
}
|
|
5802
6868
|
function done(status, verdict, theAct, extra) {
|
|
5803
6869
|
return { status, verdict, act: theAct, ...extra };
|
|
5804
6870
|
}
|
|
@@ -5816,13 +6882,13 @@ async function submit(f, endpoint, theAct) {
|
|
|
5816
6882
|
async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPending) {
|
|
5817
6883
|
const res = await f(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ role, act: { ...theAct, consents: [] } }) });
|
|
5818
6884
|
const body = await res.json().catch(() => ({}));
|
|
5819
|
-
if (res.status === 200 && body.status === "approved") return body.consent;
|
|
6885
|
+
if (res.status === 200 && body.status === "approved") return { consent: body.consent, keyCerts: body.key_certs };
|
|
5820
6886
|
if (res.status !== 202 || !body.poll_url) return null;
|
|
5821
6887
|
onPending(body.approval_url);
|
|
5822
6888
|
while (Date.now() < deadline) {
|
|
5823
6889
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
5824
6890
|
const p = await (await f(body.poll_url)).json().catch(() => ({}));
|
|
5825
|
-
if (p.status === "approved") return p.consent;
|
|
6891
|
+
if (p.status === "approved") return { consent: p.consent, keyCerts: p.key_certs };
|
|
5826
6892
|
if (p.status === "refused") return "refused";
|
|
5827
6893
|
if (p.status === "expired") return null;
|
|
5828
6894
|
}
|
|
@@ -5830,7 +6896,6 @@ async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPen
|
|
|
5830
6896
|
}
|
|
5831
6897
|
|
|
5832
6898
|
// src/act/roles.ts
|
|
5833
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
5834
6899
|
function serve(route) {
|
|
5835
6900
|
async function fetchHandler(request) {
|
|
5836
6901
|
const out = await route({ method: request.method, path: new URL(request.url).pathname, json: () => request.json() });
|
|
@@ -5845,7 +6910,7 @@ function serve(route) {
|
|
|
5845
6910
|
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
5846
6911
|
const chunks = [];
|
|
5847
6912
|
for await (const c of req) chunks.push(c);
|
|
5848
|
-
return JSON.parse(
|
|
6913
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
5849
6914
|
};
|
|
5850
6915
|
let out;
|
|
5851
6916
|
try {
|
|
@@ -5869,7 +6934,9 @@ var day = 864e5;
|
|
|
5869
6934
|
function issuer(o) {
|
|
5870
6935
|
const byId = new Map(o.authorities.map((a) => [a.meta.artifact_id, a]));
|
|
5871
6936
|
const base = o.origin.replace(/\/$/, "");
|
|
5872
|
-
const
|
|
6937
|
+
const pool = async () => typeof o.keyCerts === "function" ? await o.keyCerts() : o.keyCerts;
|
|
6938
|
+
const revocationsUrl = o.revocations ? `${base}/nomos/revocations` : o.revocationsUrl;
|
|
6939
|
+
const build = (keyCerts) => ({
|
|
5873
6940
|
nomos_document: "issuer",
|
|
5874
6941
|
version: "1",
|
|
5875
6942
|
issuer_id: o.issuerId,
|
|
@@ -5880,17 +6947,24 @@ function issuer(o) {
|
|
|
5880
6947
|
artifact_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}.nomos`,
|
|
5881
6948
|
in_force_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}/in-force`
|
|
5882
6949
|
})),
|
|
5883
|
-
key_certs:
|
|
6950
|
+
key_certs: keyCerts,
|
|
5884
6951
|
...o.witnesses ? { witnesses: o.witnesses } : {},
|
|
5885
6952
|
...o.approvals ? { approvals: o.approvals } : {},
|
|
5886
|
-
...
|
|
6953
|
+
...revocationsUrl ? { revocations_url: revocationsUrl } : {},
|
|
5887
6954
|
valid_until: new Date(Date.now() + day).toISOString()
|
|
5888
6955
|
});
|
|
6956
|
+
const document = () => {
|
|
6957
|
+
if (typeof o.keyCerts === "function") throw new Error("issuer(): keyCerts is a function, so the document must be read with `await resolveDocument()`");
|
|
6958
|
+
return build(o.keyCerts);
|
|
6959
|
+
};
|
|
6960
|
+
const resolveDocument = async () => build(await pool());
|
|
5889
6961
|
return {
|
|
5890
6962
|
document,
|
|
6963
|
+
resolveDocument,
|
|
5891
6964
|
...serve(async ({ method, path }) => {
|
|
5892
6965
|
if (method !== "GET") return null;
|
|
5893
|
-
if (path === WELL_KNOWN.issuer) return { status: 200, body:
|
|
6966
|
+
if (path === WELL_KNOWN.issuer) return { status: 200, body: await resolveDocument() };
|
|
6967
|
+
if (path === "/nomos/revocations" && o.revocations) return { status: 200, body: await o.revocations() };
|
|
5894
6968
|
const m = path.match(/^\/nomos\/authorities\/([^/]+?)(\.nomos|\/in-force)$/);
|
|
5895
6969
|
const art = m && byId.get(decodeURIComponent(m[1]));
|
|
5896
6970
|
if (!art) return null;
|
|
@@ -5925,13 +6999,14 @@ function witness(o) {
|
|
|
5925
6999
|
if (value2 === void 0) return { status: 404, body: { error: `${o.witnessId} knows nothing about ${about}.` } };
|
|
5926
7000
|
const max = o.maxValiditySeconds ?? 900;
|
|
5927
7001
|
const seconds = Math.max(1, Math.min(max, Number(req?.valid_for_seconds) || max));
|
|
5928
|
-
return { status: 200, body: { testimony: await signTestimony(o.signer, { claim, value: value2, about, validUntil: new Date(Date.now() + seconds * 1e3) }) } };
|
|
7002
|
+
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 } : {} } };
|
|
5929
7003
|
})
|
|
5930
7004
|
};
|
|
5931
7005
|
}
|
|
5932
7006
|
function approver(o) {
|
|
5933
7007
|
const base = o.origin.replace(/\/$/, "");
|
|
5934
7008
|
const pending = /* @__PURE__ */ new Map();
|
|
7009
|
+
const chain = o.keyCerts?.length ? { key_certs: o.keyCerts } : {};
|
|
5935
7010
|
const statusOf = (p) => p.status === "pending" && Date.parse(p.act.expires_at) < Date.now() ? "expired" : p.status;
|
|
5936
7011
|
return {
|
|
5937
7012
|
pending,
|
|
@@ -5941,9 +7016,9 @@ function approver(o) {
|
|
|
5941
7016
|
if (!o.roles.includes(role)) return { status: 404, body: { error: `This approver does not consent as ${role}.` } };
|
|
5942
7017
|
if (o.decide === "auto" || typeof o.decide === "function") {
|
|
5943
7018
|
const ok = o.decide === "auto" ? true : await o.decide({ role, act: act2 });
|
|
5944
|
-
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role) } } : { status: 200, body: { status: "refused" } };
|
|
7019
|
+
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role), ...chain } } : { status: 200, body: { status: "refused" } };
|
|
5945
7020
|
}
|
|
5946
|
-
const id =
|
|
7021
|
+
const id = randomUUID();
|
|
5947
7022
|
pending.set(id, { role, act: act2, status: "pending" });
|
|
5948
7023
|
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
5949
7024
|
}
|
|
@@ -5952,7 +7027,7 @@ function approver(o) {
|
|
|
5952
7027
|
const p = pending.get(m[1]);
|
|
5953
7028
|
if (!p) return null;
|
|
5954
7029
|
const s = statusOf(p);
|
|
5955
|
-
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent } : {} } };
|
|
7030
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...chain } : {} } };
|
|
5956
7031
|
}
|
|
5957
7032
|
m = path.match(/^\/approve\/([0-9a-f-]+)(\/(approve|refuse))?$/);
|
|
5958
7033
|
if (m) {
|
|
@@ -5972,22 +7047,23 @@ function approver(o) {
|
|
|
5972
7047
|
})
|
|
5973
7048
|
};
|
|
5974
7049
|
}
|
|
5975
|
-
var
|
|
7050
|
+
var esc2 = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
5976
7051
|
function page(p, status, id) {
|
|
5977
7052
|
const a = p.act.action ?? {};
|
|
5978
|
-
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${
|
|
7053
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc2(k)}</td><td><b>${esc2(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
5979
7054
|
const buttons = status === "pending" ? `<form method="post" action="/approve/${id}/approve" style="display:inline"><button style="padding:10px 18px">Approve</button></form>
|
|
5980
|
-
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${
|
|
5981
|
-
return `<!doctype html><meta charset="utf-8"><title>Approve: ${
|
|
7055
|
+
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${esc2(status)}</b></p>`;
|
|
7056
|
+
return `<!doctype html><meta charset="utf-8"><title>Approve: ${esc2(a.type)}</title>
|
|
5982
7057
|
<body style="font:15px system-ui;max-width:560px;margin:40px auto;padding:0 16px">
|
|
5983
|
-
<h2>Approve as ${
|
|
5984
|
-
<p><b>${
|
|
7058
|
+
<h2>Approve as ${esc2(p.role)}?</h2>
|
|
7059
|
+
<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>
|
|
5985
7060
|
<table style="border-collapse:collapse" cellpadding="4">${params}</table>
|
|
5986
|
-
<p style="color:#666">Your approval signs this exact change (digest ${
|
|
7061
|
+
<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>
|
|
5987
7062
|
${buttons}</body>`;
|
|
5988
7063
|
}
|
|
5989
7064
|
export {
|
|
5990
7065
|
ACT_MEDIA_TYPE,
|
|
7066
|
+
MAX_PRESENTED,
|
|
5991
7067
|
Nomos,
|
|
5992
7068
|
NomosAPIError,
|
|
5993
7069
|
NomosActError,
|
|
@@ -6002,31 +7078,53 @@ export {
|
|
|
6002
7078
|
NomosRateLimitError,
|
|
6003
7079
|
NomosValidationError,
|
|
6004
7080
|
NomosWebhookSignatureError,
|
|
7081
|
+
RegistryError,
|
|
7082
|
+
RevocationUnavailable,
|
|
6005
7083
|
WELL_KNOWN,
|
|
6006
7084
|
accept,
|
|
6007
7085
|
act,
|
|
7086
|
+
applyToolBindings,
|
|
6008
7087
|
approver,
|
|
6009
7088
|
bindingDigest,
|
|
6010
7089
|
buildAct,
|
|
6011
7090
|
can,
|
|
7091
|
+
certificateRegistry,
|
|
6012
7092
|
certify,
|
|
7093
|
+
describeSuggestion,
|
|
6013
7094
|
fetchDocument,
|
|
6014
7095
|
generateSigner,
|
|
6015
7096
|
issuer,
|
|
6016
7097
|
kidOf2 as kidOf,
|
|
6017
7098
|
localSigner,
|
|
7099
|
+
memoryPasskeyStore,
|
|
7100
|
+
memoryRegistryStore,
|
|
6018
7101
|
memoryStore,
|
|
6019
7102
|
nomos,
|
|
6020
7103
|
originOf,
|
|
7104
|
+
parseToolBindings,
|
|
7105
|
+
passkeyApprover,
|
|
7106
|
+
postgresPasskeyStore,
|
|
7107
|
+
postgresRegistryStore,
|
|
7108
|
+
postgresStore,
|
|
7109
|
+
presentedCertificates,
|
|
7110
|
+
readAttestation,
|
|
7111
|
+
reliedKids,
|
|
7112
|
+
revocationListCertificates,
|
|
7113
|
+
revocationSource,
|
|
6021
7114
|
seal,
|
|
6022
7115
|
seededSigner,
|
|
6023
7116
|
signConsent,
|
|
7117
|
+
signEnrolmentToken,
|
|
6024
7118
|
signInForce,
|
|
7119
|
+
signRevocation,
|
|
7120
|
+
signRevocationList,
|
|
6025
7121
|
signTestimony,
|
|
6026
7122
|
statusFor,
|
|
7123
|
+
suggestToolBindings,
|
|
6027
7124
|
verifyAct,
|
|
6028
7125
|
verifyArtifact,
|
|
6029
7126
|
verifyChain,
|
|
7127
|
+
verifyEnrolmentToken,
|
|
6030
7128
|
verifySeal,
|
|
6031
7129
|
witness
|
|
6032
7130
|
};
|