genesis-mesh-sdk 0.58.1 → 0.59.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/CHANGELOG.md +38 -13
- package/README.md +20 -5
- package/dist/cjs/attestation.js +23 -1
- package/dist/cjs/auth.js +197 -18
- package/dist/cjs/boundary.js +20 -2
- package/dist/cjs/canonical.js +89 -0
- package/dist/cjs/client.js +103 -37
- package/dist/cjs/errors.js +43 -1
- package/dist/cjs/evidence_store.js +172 -0
- package/dist/cjs/execution.js +121 -0
- package/dist/cjs/governance.js +177 -0
- package/dist/cjs/index.js +70 -2
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/policy.js +47 -0
- package/dist/cjs/types.js +1 -1
- package/dist/cjs/validation.js +103 -0
- package/dist/cjs/verify.js +299 -0
- package/dist/esm/attestation.js +23 -1
- package/dist/esm/attestation.js.map +1 -1
- package/dist/esm/auth.js +187 -19
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/boundary.js +20 -2
- package/dist/esm/boundary.js.map +1 -1
- package/dist/esm/canonical.js +73 -0
- package/dist/esm/canonical.js.map +1 -0
- package/dist/esm/client.js +104 -39
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/errors.js +39 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/evidence_store.js +169 -0
- package/dist/esm/evidence_store.js.map +1 -0
- package/dist/esm/execution.js +116 -0
- package/dist/esm/execution.js.map +1 -0
- package/dist/esm/governance.js +170 -0
- package/dist/esm/governance.js.map +1 -0
- package/dist/esm/index.js +21 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/policy.js +44 -0
- package/dist/esm/policy.js.map +1 -0
- package/dist/esm/types.js +1 -1
- package/dist/esm/validation.js +99 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/verify.js +287 -0
- package/dist/esm/verify.js.map +1 -0
- package/dist/types/attestation.d.ts +21 -1
- package/dist/types/attestation.d.ts.map +1 -1
- package/dist/types/auth.d.ts +52 -9
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/boundary.d.ts +40 -2
- package/dist/types/boundary.d.ts.map +1 -1
- package/dist/types/canonical.d.ts +29 -0
- package/dist/types/canonical.d.ts.map +1 -0
- package/dist/types/client.d.ts +35 -9
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/evidence_store.d.ts +94 -0
- package/dist/types/evidence_store.d.ts.map +1 -0
- package/dist/types/execution.d.ts +52 -0
- package/dist/types/execution.d.ts.map +1 -0
- package/dist/types/governance.d.ts +128 -0
- package/dist/types/governance.d.ts.map +1 -0
- package/dist/types/index.d.ts +21 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/policy.d.ts +32 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/types.d.ts +399 -12
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/validation.d.ts +12 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/verify.d.ts +51 -0
- package/dist/types/verify.d.ts.map +1 -0
- package/docs/evidence-store.md +67 -0
- package/docs/governance.md +77 -0
- package/docs/offline-verification.md +80 -0
- package/package.json +16 -10
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ Versions align with the [Genesis Mesh release sequence](https://github.com/Genes
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.59.1] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
Coordinated Genesis Mesh v0.59.1 release: TypeScript support for governed
|
|
13
|
+
secret lifecycles (attestation-backed evaluation, policy lifecycle, evidence
|
|
14
|
+
store, offline verification).
|
|
15
|
+
|
|
16
|
+
- Add attestation-backed evaluation, attestation queries, recognition policy reads
|
|
17
|
+
and signed revocation feeds.
|
|
18
|
+
- Add boundary-policy management, the execution evidence-store client, signed
|
|
19
|
+
execution recording, governed actions and inventory reconciliation.
|
|
20
|
+
- Add Python-compatible canonical JSON, model digests and offline verification
|
|
21
|
+
of signed artifacts and complete evidence exports.
|
|
22
|
+
- Require verified decisions and expected bindings before governed callbacks;
|
|
23
|
+
guard evaluation and execution metadata before sending it to the NA.
|
|
24
|
+
- Add async signers, signed admin GETs, NDJSON paging, bounded opt-in retries and
|
|
25
|
+
typed errors carrying NA details and request IDs.
|
|
26
|
+
- Correct wire types and CommonJS package loading; require Node.js 22 or newer.
|
|
27
|
+
- Add Python-vector, negative-path, public-route and live local-NA tests, plus
|
|
28
|
+
typechecking of the test suite and package entry-point smoke checks.
|
|
29
|
+
|
|
30
|
+
## [0.59.0] - 2026-10-01
|
|
31
|
+
|
|
32
|
+
Coordinated Genesis Mesh v0.59.0 release. No functional changes; the core adds
|
|
33
|
+
the Network Authority evidence store, which this SDK does not wrap yet.
|
|
34
|
+
|
|
10
35
|
## [0.58.1] - 2026-09-29
|
|
11
36
|
|
|
12
37
|
Coordinated Genesis Mesh v0.58.1 release. No functional changes; the core adds
|
|
@@ -33,22 +58,22 @@ across the train (see the core `docs/development/versioning.md`).
|
|
|
33
58
|
|
|
34
59
|
---
|
|
35
60
|
|
|
36
|
-
## [0.53.0]
|
|
61
|
+
## [0.53.0] - 2026-06-29
|
|
37
62
|
|
|
38
63
|
### Added
|
|
39
64
|
|
|
40
|
-
- `GenesisMeshClient`
|
|
41
|
-
- `AgreementClient`
|
|
42
|
-
- `BoundaryClient`
|
|
43
|
-
- `EvidenceClient`
|
|
44
|
-
- `AttestationClient`
|
|
45
|
-
- `DisclosureClient`
|
|
46
|
-
- `ConsensusClient`
|
|
47
|
-
- `DataUsageClient`
|
|
48
|
-
- `src/auth.ts`
|
|
49
|
-
- `src/client.ts`
|
|
50
|
-
- `src/errors.ts`
|
|
51
|
-
- `src/types.ts`
|
|
65
|
+
- `GenesisMeshClient` - unified entry point with 7 domain sub-clients
|
|
66
|
+
- `AgreementClient` - capability offer, counter, accept, verify
|
|
67
|
+
- `BoundaryClient` - boundary decision and verification
|
|
68
|
+
- `EvidenceClient` - trust evidence build and verify
|
|
69
|
+
- `AttestationClient` - membership attestation issue, revoke, recognition policy
|
|
70
|
+
- `DisclosureClient` - selective Merkle capability disclosure, nullifier
|
|
71
|
+
- `ConsensusClient` - validator vote, consensus proof assembly and verify
|
|
72
|
+
- `DataUsageClient` - data license policy, access intent, verify
|
|
73
|
+
- `src/auth.ts` - `canonicalJson`, `signBytes`, `buildAdminHeaders` (Ed25519 / PKCS8-DER)
|
|
74
|
+
- `src/client.ts` - `HttpTransport` with fetch, timeout, and typed error mapping
|
|
75
|
+
- `src/errors.ts` - `GenesisMeshError` and typed subclasses for all NA error codes
|
|
76
|
+
- `src/types.ts` - 30+ protocol interfaces matching the NA JSON wire format
|
|
52
77
|
- ESM and CJS dual build (`dist/esm/`, `dist/cjs/`, `dist/types/`)
|
|
53
78
|
- 74 Jest unit tests covering all sub-clients
|
|
54
79
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for the Genesis Mesh Network Authority HTTP API.
|
|
4
4
|
|
|
5
|
-
**Node.js ≥
|
|
5
|
+
**Node.js ≥ 22 required. Zero runtime dependencies.**
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -29,6 +29,19 @@ const client = new GenesisMeshClient({
|
|
|
29
29
|
});
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Governed lifecycle SDK
|
|
33
|
+
|
|
34
|
+
The SDK supports attestation-backed evaluation, boundary-policy management,
|
|
35
|
+
execution recording, evidence-store queries and offline verification.
|
|
36
|
+
|
|
37
|
+
- [Governed actions and reconciliation](docs/governance.md)
|
|
38
|
+
- [Evidence store, policies, signers and retries](docs/evidence-store.md)
|
|
39
|
+
- [Offline verification and integration tests](docs/offline-verification.md)
|
|
40
|
+
|
|
41
|
+
`governedAction` requires verification keys and explicit expected policies. It
|
|
42
|
+
verifies the signed response and its request context before calling the supplied
|
|
43
|
+
action. Attestation-backed ALLOWs also require the expected attestation.
|
|
44
|
+
|
|
32
45
|
## Sub-clients
|
|
33
46
|
|
|
34
47
|
### agreement
|
|
@@ -38,7 +51,7 @@ const client = new GenesisMeshClient({
|
|
|
38
51
|
> [Raw admin calls](#raw-admin-calls) below).
|
|
39
52
|
|
|
40
53
|
```typescript
|
|
41
|
-
// Create a capability offer (admin
|
|
54
|
+
// Create a capability offer (admin - requires signing key)
|
|
42
55
|
const offer = await client.agreement.offer({
|
|
43
56
|
responder_sovereign_id: 'BETA-NA',
|
|
44
57
|
capabilities: ['read:data', 'write:log'],
|
|
@@ -212,7 +225,7 @@ For NA routes not yet covered by a sub-client (e.g. `/admin/recognition-treaties
|
|
|
212
225
|
use `buildAdminHeaders` directly:
|
|
213
226
|
|
|
214
227
|
```typescript
|
|
215
|
-
import { buildAdminHeaders } from 'genesis-mesh-sdk';
|
|
228
|
+
import { buildAdminHeaders, canonicalJson } from 'genesis-mesh-sdk';
|
|
216
229
|
|
|
217
230
|
const body = {
|
|
218
231
|
subject_sovereign_id: 'BETA-NA',
|
|
@@ -225,7 +238,7 @@ const headers = buildAdminHeaders(body, keyId, signingKeyBase64);
|
|
|
225
238
|
const res = await fetch(`${baseUrl}/admin/recognition-treaties`, {
|
|
226
239
|
method: 'POST',
|
|
227
240
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
228
|
-
body:
|
|
241
|
+
body: canonicalJson(body),
|
|
229
242
|
});
|
|
230
243
|
```
|
|
231
244
|
|
|
@@ -268,7 +281,9 @@ The SDK handles all of this automatically when `signingKeyBase64` is provided.
|
|
|
268
281
|
```bash
|
|
269
282
|
npm run build # ESM → dist/esm/ and CJS → dist/cjs/
|
|
270
283
|
npm run typecheck # type-check without emitting
|
|
271
|
-
npm test #
|
|
284
|
+
npm test # unit tests and Python-vector verification
|
|
285
|
+
npm run test:package # validate ESM and CommonJS entry points after building
|
|
286
|
+
npm run test:e2e # disposable local Python NA (see verification guide)
|
|
272
287
|
```
|
|
273
288
|
|
|
274
289
|
## License
|
package/dist/cjs/attestation.js
CHANGED
|
@@ -12,11 +12,33 @@ class AttestationClient {
|
|
|
12
12
|
}
|
|
13
13
|
/** Revoke a membership attestation by ID (admin). */
|
|
14
14
|
revoke(attestationId, params = {}) {
|
|
15
|
-
return this.http.adminPost(`/admin/attestations/${attestationId}/revoke`, params);
|
|
15
|
+
return this.http.adminPost(`/admin/attestations/${encodeURIComponent(attestationId)}/revoke`, params);
|
|
16
16
|
}
|
|
17
17
|
/** Set the active recognition policy for this sovereign (admin). */
|
|
18
18
|
savePolicy(params) {
|
|
19
19
|
return this.http.adminPost('/admin/recognition-policy', params);
|
|
20
20
|
}
|
|
21
|
+
/** A stored attestation with its current status (unauthenticated). */
|
|
22
|
+
get(attestationId) {
|
|
23
|
+
return this.http.publicGet(`/attestations/${encodeURIComponent(attestationId)}`);
|
|
24
|
+
}
|
|
25
|
+
/** Stored attestations, optionally filtered by issuer, subject or status (unauthenticated). */
|
|
26
|
+
list(params = {}) {
|
|
27
|
+
return this.http.publicGet('/attestations', { ...params });
|
|
28
|
+
}
|
|
29
|
+
/** Verify an attestation against a recognition policy (unauthenticated). */
|
|
30
|
+
verify(params) {
|
|
31
|
+
return this.http.publicPost('/attestations/verify', params, true);
|
|
32
|
+
}
|
|
33
|
+
/** The NA's active recognition policy (unauthenticated). */
|
|
34
|
+
getPolicy() {
|
|
35
|
+
return this.http.publicGet('/recognition-policy');
|
|
36
|
+
}
|
|
37
|
+
/** Signed feed of revoked attestations for an issuer; defaults to this NA (unauthenticated). */
|
|
38
|
+
revocationFeed(issuerSovereignId) {
|
|
39
|
+
return this.http.publicGet('/sovereign-revocation-feed', {
|
|
40
|
+
issuer_sovereign_id: issuerSovereignId,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
21
43
|
}
|
|
22
44
|
exports.AttestationClient = AttestationClient;
|
package/dist/cjs/auth.js
CHANGED
|
@@ -1,30 +1,135 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Key operations for Genesis Mesh: canonical JSON, digests, Ed25519 signing
|
|
4
|
+
* and verification, and admin request authentication.
|
|
4
5
|
*
|
|
5
|
-
* The NA verifies requests via four HTTP headers:
|
|
6
|
-
* X-Admin-Key-Id
|
|
7
|
-
* X-Admin-Signature
|
|
8
|
-
* X-Admin-Timestamp
|
|
9
|
-
* X-Admin-Nonce
|
|
6
|
+
* The NA verifies admin requests via four HTTP headers:
|
|
7
|
+
* X-Admin-Key-Id - which operator key was used
|
|
8
|
+
* X-Admin-Signature - Ed25519(canonicalJson({body,key_id,nonce,timestamp}))
|
|
9
|
+
* X-Admin-Timestamp - ISO 8601 UTC timestamp
|
|
10
|
+
* X-Admin-Nonce - UUID v4 replay-protection token
|
|
10
11
|
*
|
|
11
|
-
* Canonical JSON matches Python json.dumps(..., sort_keys=True, separators=(",",":"))
|
|
12
|
-
* keys
|
|
12
|
+
* Canonical JSON matches Python json.dumps(..., sort_keys=True, separators=(",",":"))
|
|
13
|
+
* applied to the value the NA parses: keys recursively sorted, compact output,
|
|
14
|
+
* non-ASCII escaped (ensure_ascii), and non-integer numbers in Python float repr.
|
|
13
15
|
*/
|
|
14
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.parseJson = parseJson;
|
|
18
|
+
exports.compareCodePoints = compareCodePoints;
|
|
15
19
|
exports.canonicalJson = canonicalJson;
|
|
20
|
+
exports.sha256Hex = sha256Hex;
|
|
21
|
+
exports.canonicalDigest = canonicalDigest;
|
|
16
22
|
exports.signBytes = signBytes;
|
|
23
|
+
exports.verifyBytes = verifyBytes;
|
|
24
|
+
exports.publicKeyFromSeed = publicKeyFromSeed;
|
|
25
|
+
exports.seedSigner = seedSigner;
|
|
26
|
+
exports.signCanonical = signCanonical;
|
|
27
|
+
exports.verifyCanonical = verifyCanonical;
|
|
28
|
+
exports.pythonTimestamp = pythonTimestamp;
|
|
17
29
|
exports.buildAdminHeaders = buildAdminHeaders;
|
|
30
|
+
exports.buildAdminHeadersWithSigner = buildAdminHeadersWithSigner;
|
|
18
31
|
const node_crypto_1 = require("node:crypto");
|
|
19
|
-
/**
|
|
32
|
+
/** Escape every non-ASCII UTF-16 unit as \uXXXX, as Python's ensure_ascii does. */
|
|
33
|
+
function asciiString(value) {
|
|
34
|
+
return JSON.stringify(value).replace(/[^\x00-\x7e]/g, ch => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
35
|
+
}
|
|
36
|
+
function pythonFloatRepr(value) {
|
|
37
|
+
if (Object.is(value, -0))
|
|
38
|
+
return '-0.0';
|
|
39
|
+
const [mantissa, expText] = value.toExponential().split('e');
|
|
40
|
+
const exp = Number(expText);
|
|
41
|
+
if (exp >= -4 && exp < 16) {
|
|
42
|
+
const negative = mantissa.startsWith('-');
|
|
43
|
+
const digits = mantissa.replace('-', '').replace('.', '');
|
|
44
|
+
const point = exp + 1;
|
|
45
|
+
let fixed;
|
|
46
|
+
if (point <= 0)
|
|
47
|
+
fixed = '0.' + '0'.repeat(-point) + digits;
|
|
48
|
+
else if (point >= digits.length)
|
|
49
|
+
fixed = digits + '0'.repeat(point - digits.length) + '.0';
|
|
50
|
+
else
|
|
51
|
+
fixed = digits.slice(0, point) + '.' + digits.slice(point);
|
|
52
|
+
return (negative ? '-' : '') + fixed;
|
|
53
|
+
}
|
|
54
|
+
const sign = exp < 0 ? '-' : '+';
|
|
55
|
+
return `${mantissa}e${sign}${String(Math.abs(exp)).padStart(2, '0')}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The text Python's json.dumps produces for the value the NA parses from this
|
|
59
|
+
* number's JSON. Integral text stays an int; anything else is a Python float.
|
|
60
|
+
* `pythonFloat` forces float repr for an integral value the NA sent as a float.
|
|
61
|
+
*/
|
|
62
|
+
function pythonNumber(value, pythonFloat = false) {
|
|
63
|
+
if (!Number.isFinite(value))
|
|
64
|
+
throw new Error('canonical JSON cannot encode a non-finite number');
|
|
65
|
+
const text = JSON.stringify(value);
|
|
66
|
+
if (!pythonFloat && /^-?\d+$/.test(text))
|
|
67
|
+
return text;
|
|
68
|
+
return pythonFloatRepr(value);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Holder objects/arrays → keys whose value the NA sent as an integral float
|
|
72
|
+
* (`90.0`). JSON.parse yields the number 90; canonical JSON must emit 90.0 or
|
|
73
|
+
* an NA signature over it would not verify.
|
|
74
|
+
*/
|
|
75
|
+
const PYTHON_FLOATS = new WeakMap();
|
|
76
|
+
/**
|
|
77
|
+
* JSON.parse that remembers integral-valued floats so canonicalJson reproduces
|
|
78
|
+
* them. Use it for any NA JSON that will be verified or digested. Requires
|
|
79
|
+
* JSON.parse source text access (Node.js 22+).
|
|
80
|
+
*/
|
|
81
|
+
function parseJson(text) {
|
|
82
|
+
return JSON.parse(text, function (key, value, context) {
|
|
83
|
+
if (typeof value === 'number' && Number.isInteger(value) && context?.source && /[.eE]/.test(context.source)) {
|
|
84
|
+
let keys = PYTHON_FLOATS.get(this);
|
|
85
|
+
if (!keys)
|
|
86
|
+
PYTHON_FLOATS.set(this, (keys = new Set()));
|
|
87
|
+
keys.add(key);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function canonicalMember(holder, key, value) {
|
|
93
|
+
if (typeof value === 'number')
|
|
94
|
+
return pythonNumber(value, PYTHON_FLOATS.get(holder)?.has(key) ?? false);
|
|
95
|
+
return canonicalJson(value);
|
|
96
|
+
}
|
|
97
|
+
/** Python orders strings by Unicode code point, not UTF-16 code unit. */
|
|
98
|
+
function compareCodePoints(a, b) {
|
|
99
|
+
const left = Array.from(a, c => c.codePointAt(0));
|
|
100
|
+
const right = Array.from(b, c => c.codePointAt(0));
|
|
101
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
102
|
+
if (left[i] !== right[i])
|
|
103
|
+
return left[i] - right[i];
|
|
104
|
+
}
|
|
105
|
+
return left.length - right.length;
|
|
106
|
+
}
|
|
107
|
+
/** Compact sorted JSON, byte-identical to the Python canonical form. */
|
|
20
108
|
function canonicalJson(value) {
|
|
21
|
-
if (value === null ||
|
|
22
|
-
return
|
|
109
|
+
if (value === null || value === undefined)
|
|
110
|
+
return 'null';
|
|
111
|
+
if (typeof value === 'string')
|
|
112
|
+
return asciiString(value);
|
|
113
|
+
if (typeof value === 'number')
|
|
114
|
+
return pythonNumber(value);
|
|
115
|
+
if (typeof value === 'boolean')
|
|
116
|
+
return value ? 'true' : 'false';
|
|
23
117
|
if (Array.isArray(value))
|
|
24
|
-
return '[' + value.map(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
118
|
+
return '[' + value.map((v, i) => canonicalMember(value, String(i), v)).join(',') + ']';
|
|
119
|
+
if (typeof value === 'object') {
|
|
120
|
+
const record = value;
|
|
121
|
+
const keys = Object.keys(record).filter(k => record[k] !== undefined).sort(compareCodePoints);
|
|
122
|
+
return '{' + keys.map(k => `${asciiString(k)}:${canonicalMember(record, k, record[k])}`).join(',') + '}';
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`canonical JSON cannot encode a ${typeof value}`);
|
|
125
|
+
}
|
|
126
|
+
/** SHA-256 hex of a string's UTF-8 bytes. */
|
|
127
|
+
function sha256Hex(text) {
|
|
128
|
+
return (0, node_crypto_1.createHash)('sha256').update(text, 'utf-8').digest('hex');
|
|
129
|
+
}
|
|
130
|
+
/** SHA-256 hex of a value's canonical JSON - every GM `digest()`. */
|
|
131
|
+
function canonicalDigest(value) {
|
|
132
|
+
return sha256Hex(canonicalJson(value));
|
|
28
133
|
}
|
|
29
134
|
/**
|
|
30
135
|
* PKCS8 DER prefix for an Ed25519 private key.
|
|
@@ -32,18 +137,81 @@ function canonicalJson(value) {
|
|
|
32
137
|
* Concatenate with the 32-byte seed to form a valid PKCS8 DER buffer.
|
|
33
138
|
*/
|
|
34
139
|
const ED25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
140
|
+
/**
|
|
141
|
+
* SPKI DER prefix for an Ed25519 public key.
|
|
142
|
+
* Layout: SEQUENCE { SEQUENCE { OID 1.3.101.112 }, BIT STRING { <32-byte key> } }
|
|
143
|
+
*/
|
|
144
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
35
145
|
/** Sign raw bytes with an Ed25519 seed (32-byte base64-encoded private key seed). */
|
|
36
146
|
function signBytes(message, seedBase64) {
|
|
37
147
|
const seed = Buffer.from(seedBase64, 'base64');
|
|
148
|
+
if (seed.length !== 32)
|
|
149
|
+
throw new Error('Ed25519 seed must be 32 bytes');
|
|
38
150
|
const pkcs8 = Buffer.concat([ED25519_PKCS8_PREFIX, seed]);
|
|
39
151
|
const privateKey = (0, node_crypto_1.createPrivateKey)({ key: pkcs8, format: 'der', type: 'pkcs8' });
|
|
40
152
|
return (0, node_crypto_1.sign)(null, message, privateKey);
|
|
41
153
|
}
|
|
42
|
-
/**
|
|
43
|
-
function
|
|
154
|
+
/** Verify an Ed25519 signature (base64) with a raw 32-byte public key (base64). */
|
|
155
|
+
function verifyBytes(message, signatureBase64, publicKeyBase64) {
|
|
156
|
+
const raw = Buffer.from(publicKeyBase64, 'base64');
|
|
157
|
+
const sig = Buffer.from(signatureBase64, 'base64');
|
|
158
|
+
if (raw.length !== 32 || sig.length !== 64)
|
|
159
|
+
return false;
|
|
160
|
+
const publicKey = (0, node_crypto_1.createPublicKey)({
|
|
161
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
|
|
162
|
+
format: 'der',
|
|
163
|
+
type: 'spki',
|
|
164
|
+
});
|
|
165
|
+
return (0, node_crypto_1.verify)(null, message, publicKey, sig);
|
|
166
|
+
}
|
|
167
|
+
/** Raw 32-byte public key (base64) for an Ed25519 seed (base64). */
|
|
168
|
+
function publicKeyFromSeed(seedBase64) {
|
|
169
|
+
const seed = Buffer.from(seedBase64, 'base64');
|
|
170
|
+
if (seed.length !== 32)
|
|
171
|
+
throw new Error('Ed25519 seed must be 32 bytes');
|
|
172
|
+
const privateKey = (0, node_crypto_1.createPrivateKey)({
|
|
173
|
+
key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
|
|
174
|
+
format: 'der',
|
|
175
|
+
type: 'pkcs8',
|
|
176
|
+
});
|
|
177
|
+
const spki = (0, node_crypto_1.createPublicKey)(privateKey).export({ type: 'spki', format: 'der' });
|
|
178
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length).toString('base64');
|
|
179
|
+
}
|
|
180
|
+
/** A Signer backed by a raw Ed25519 seed held in memory. */
|
|
181
|
+
function seedSigner(seedBase64, keyId) {
|
|
182
|
+
return { keyId, sign: (message) => signBytes(message, seedBase64) };
|
|
183
|
+
}
|
|
184
|
+
/** Sign a canonical body with a Signer; returns the GM `{key_id, sig}` pair. */
|
|
185
|
+
async function signCanonical(canonical, signer) {
|
|
186
|
+
const sig = await signer.sign(Buffer.from(canonical, 'utf-8'));
|
|
187
|
+
if (sig.length !== 64)
|
|
188
|
+
throw new Error('signer returned a signature that is not 64 bytes');
|
|
189
|
+
return { key_id: signer.keyId, sig: Buffer.from(sig).toString('base64') };
|
|
190
|
+
}
|
|
191
|
+
/** True when `sig` verifies the canonical body under any of the public keys. */
|
|
192
|
+
function verifyCanonical(canonical, sig, publicKeys) {
|
|
193
|
+
const message = Buffer.from(canonical, 'utf-8');
|
|
194
|
+
return publicKeys.some(key => verifyBytes(message, sig, key));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A UTC timestamp in the form Pydantic re-serialises unchanged: microsecond
|
|
198
|
+
* precision, `Z` suffix, fraction omitted when zero. `toISOString()` output
|
|
199
|
+
* (`.561Z`) would come back as `.561000Z` and break a signature over it.
|
|
200
|
+
*/
|
|
201
|
+
function pythonTimestamp(date = new Date()) {
|
|
202
|
+
const iso = date.toISOString();
|
|
203
|
+
const ms = date.getUTCMilliseconds();
|
|
204
|
+
const base = iso.slice(0, 19);
|
|
205
|
+
return ms === 0 ? `${base}Z` : `${base}.${String(ms).padStart(3, '0')}000Z`;
|
|
206
|
+
}
|
|
207
|
+
function adminMessage(body, keyId) {
|
|
44
208
|
const timestamp = new Date().toISOString();
|
|
45
209
|
const nonce = (0, node_crypto_1.randomUUID)();
|
|
46
|
-
|
|
210
|
+
return { canonical: canonicalJson({ body, key_id: keyId, nonce, timestamp }), timestamp, nonce };
|
|
211
|
+
}
|
|
212
|
+
/** Build the four admin auth headers for a given request body. */
|
|
213
|
+
function buildAdminHeaders(body, keyId, signingKeyBase64) {
|
|
214
|
+
const { canonical, timestamp, nonce } = adminMessage(body, keyId);
|
|
47
215
|
const sig = signBytes(Buffer.from(canonical, 'utf-8'), signingKeyBase64);
|
|
48
216
|
return {
|
|
49
217
|
'X-Admin-Key-Id': keyId,
|
|
@@ -52,3 +220,14 @@ function buildAdminHeaders(body, keyId, signingKeyBase64) {
|
|
|
52
220
|
'X-Admin-Nonce': nonce,
|
|
53
221
|
};
|
|
54
222
|
}
|
|
223
|
+
/** Build the four admin auth headers with a Signer (key id taken from the signer). */
|
|
224
|
+
async function buildAdminHeadersWithSigner(body, signer) {
|
|
225
|
+
const { canonical, timestamp, nonce } = adminMessage(body, signer.keyId);
|
|
226
|
+
const { sig } = await signCanonical(canonical, signer);
|
|
227
|
+
return {
|
|
228
|
+
'X-Admin-Key-Id': signer.keyId,
|
|
229
|
+
'X-Admin-Signature': sig,
|
|
230
|
+
'X-Admin-Timestamp': timestamp,
|
|
231
|
+
'X-Admin-Nonce': nonce,
|
|
232
|
+
};
|
|
233
|
+
}
|
package/dist/cjs/boundary.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BoundaryClient = void 0;
|
|
4
|
+
const execution_js_1 = require("./execution.js");
|
|
4
5
|
class BoundaryClient {
|
|
5
6
|
http;
|
|
6
7
|
constructor(http) {
|
|
7
8
|
this.http = http;
|
|
8
9
|
}
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Legacy agreement-only decision without policy (admin). Refused with
|
|
12
|
+
* `boundary_policy_required` when the NA requires policy enforcement; use `evaluate`.
|
|
13
|
+
*/
|
|
10
14
|
decide(params) {
|
|
11
15
|
return this.http.adminPost('/admin/boundary/decide', params);
|
|
12
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Policy-aware evaluation under exactly one basis - an `attestation_id` or an
|
|
19
|
+
* `agreement` (admin). Returns the signed decision and its justification proof.
|
|
20
|
+
* A denial is a signed decision with `authorized: false`, not an error.
|
|
21
|
+
*/
|
|
22
|
+
async evaluate(params) {
|
|
23
|
+
if ((params.attestation_id !== undefined) === (params.agreement !== undefined)) {
|
|
24
|
+
throw new Error('exactly one of agreement or attestation_id is required');
|
|
25
|
+
}
|
|
26
|
+
const reason = (0, execution_js_1.checkMetadataOnly)({ ...params.context });
|
|
27
|
+
if (reason)
|
|
28
|
+
throw new execution_js_1.SecretMaterialError(reason);
|
|
29
|
+
return this.http.adminPost('/admin/boundary/evaluate', params);
|
|
30
|
+
}
|
|
13
31
|
/** Verify a boundary decision signature (unauthenticated). */
|
|
14
32
|
verify(params) {
|
|
15
|
-
return this.http.publicPost('/boundary/verify', params);
|
|
33
|
+
return this.http.publicPost('/boundary/verify', params, true);
|
|
16
34
|
}
|
|
17
35
|
}
|
|
18
36
|
exports.BoundaryClient = BoundaryClient;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical signed bodies and digests of GM protocol models, derived from
|
|
4
|
+
* their wire JSON exactly as the Python models' to_canonical_json() and
|
|
5
|
+
* digest() derive them. Pure functions; key operations live in auth.ts.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.RESOURCE_CHAIN_FIELDS = void 0;
|
|
9
|
+
exports.decisionCanonical = decisionCanonical;
|
|
10
|
+
exports.executionCanonical = executionCanonical;
|
|
11
|
+
exports.executionDigest = executionDigest;
|
|
12
|
+
exports.attestationCanonical = attestationCanonical;
|
|
13
|
+
exports.attestationDigest = attestationDigest;
|
|
14
|
+
exports.revocationFeedCanonical = revocationFeedCanonical;
|
|
15
|
+
exports.policyCanonical = policyCanonical;
|
|
16
|
+
exports.policyDigest = policyDigest;
|
|
17
|
+
exports.policySetDigest = policySetDigest;
|
|
18
|
+
exports.justificationCanonical = justificationCanonical;
|
|
19
|
+
exports.freshnessProofCanonical = freshnessProofCanonical;
|
|
20
|
+
exports.checkpointCanonical = checkpointCanonical;
|
|
21
|
+
exports.entryDigest = entryDigest;
|
|
22
|
+
exports.payloadDigest = payloadDigest;
|
|
23
|
+
const auth_js_1 = require("./auth.js");
|
|
24
|
+
/** v0.59 resource-chain fields, omitted from the execution canonical form when absent. */
|
|
25
|
+
exports.RESOURCE_CHAIN_FIELDS = [
|
|
26
|
+
'resource_id',
|
|
27
|
+
'resource_action',
|
|
28
|
+
'resource_sequence',
|
|
29
|
+
'prev_resource_digest',
|
|
30
|
+
];
|
|
31
|
+
function without(model, always, whenNull = []) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [key, value] of Object.entries(model)) {
|
|
34
|
+
if (always.includes(key))
|
|
35
|
+
continue;
|
|
36
|
+
if (whenNull.includes(key) && (value === null || value === undefined))
|
|
37
|
+
continue;
|
|
38
|
+
out[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function decisionCanonical(decision) {
|
|
43
|
+
return (0, auth_js_1.canonicalJson)(without(decision, ['signature'], ['policy_binding', 'attestation_binding']));
|
|
44
|
+
}
|
|
45
|
+
function executionCanonical(evidence) {
|
|
46
|
+
return (0, auth_js_1.canonicalJson)(without(evidence, ['signature'], exports.RESOURCE_CHAIN_FIELDS));
|
|
47
|
+
}
|
|
48
|
+
/** `ExecutionEvidence.digest()`: links the per-decision and per-resource chains. */
|
|
49
|
+
function executionDigest(evidence) {
|
|
50
|
+
return (0, auth_js_1.canonicalDigest)(without(evidence, ['signature'], exports.RESOURCE_CHAIN_FIELDS));
|
|
51
|
+
}
|
|
52
|
+
function attestationCanonical(attestation) {
|
|
53
|
+
return (0, auth_js_1.canonicalJson)(without(attestation, ['signatures']));
|
|
54
|
+
}
|
|
55
|
+
/** `MembershipAttestation.digest()`: the value an AttestationBinding commits to. */
|
|
56
|
+
function attestationDigest(attestation) {
|
|
57
|
+
return (0, auth_js_1.canonicalDigest)(without(attestation, ['signatures']));
|
|
58
|
+
}
|
|
59
|
+
function revocationFeedCanonical(feed) {
|
|
60
|
+
return (0, auth_js_1.canonicalJson)(without(feed, ['signatures']));
|
|
61
|
+
}
|
|
62
|
+
function policyCanonical(policy) {
|
|
63
|
+
return (0, auth_js_1.canonicalJson)(without(policy, ['signature']));
|
|
64
|
+
}
|
|
65
|
+
/** `BoundaryPolicy.digest()`. */
|
|
66
|
+
function policyDigest(policy) {
|
|
67
|
+
return (0, auth_js_1.canonicalDigest)(without(policy, ['signature']));
|
|
68
|
+
}
|
|
69
|
+
/** Digest of the ordered applied-policy list carried in a PolicyBinding. */
|
|
70
|
+
function policySetDigest(policies) {
|
|
71
|
+
return (0, auth_js_1.canonicalDigest)(policies.map(p => [p.policy_id, p.version, p.policy_digest]));
|
|
72
|
+
}
|
|
73
|
+
function justificationCanonical(proof) {
|
|
74
|
+
return (0, auth_js_1.canonicalJson)(without(proof, ['signature']));
|
|
75
|
+
}
|
|
76
|
+
function freshnessProofCanonical(proof) {
|
|
77
|
+
return (0, auth_js_1.canonicalJson)(without(proof, ['signature']));
|
|
78
|
+
}
|
|
79
|
+
function checkpointCanonical(checkpoint) {
|
|
80
|
+
return (0, auth_js_1.canonicalJson)(without(checkpoint, ['signature']));
|
|
81
|
+
}
|
|
82
|
+
/** `EvidenceStoreEntry.digest()`: every envelope field. */
|
|
83
|
+
function entryDigest(entry) {
|
|
84
|
+
return (0, auth_js_1.canonicalDigest)(entry);
|
|
85
|
+
}
|
|
86
|
+
/** SHA-256 of a stored payload's canonical JSON. */
|
|
87
|
+
function payloadDigest(payload) {
|
|
88
|
+
return (0, auth_js_1.canonicalDigest)(payload);
|
|
89
|
+
}
|