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.
Files changed (76) hide show
  1. package/CHANGELOG.md +38 -13
  2. package/README.md +20 -5
  3. package/dist/cjs/attestation.js +23 -1
  4. package/dist/cjs/auth.js +197 -18
  5. package/dist/cjs/boundary.js +20 -2
  6. package/dist/cjs/canonical.js +89 -0
  7. package/dist/cjs/client.js +103 -37
  8. package/dist/cjs/errors.js +43 -1
  9. package/dist/cjs/evidence_store.js +172 -0
  10. package/dist/cjs/execution.js +121 -0
  11. package/dist/cjs/governance.js +177 -0
  12. package/dist/cjs/index.js +70 -2
  13. package/dist/cjs/package.json +1 -0
  14. package/dist/cjs/policy.js +47 -0
  15. package/dist/cjs/types.js +1 -1
  16. package/dist/cjs/validation.js +103 -0
  17. package/dist/cjs/verify.js +299 -0
  18. package/dist/esm/attestation.js +23 -1
  19. package/dist/esm/attestation.js.map +1 -1
  20. package/dist/esm/auth.js +187 -19
  21. package/dist/esm/auth.js.map +1 -1
  22. package/dist/esm/boundary.js +20 -2
  23. package/dist/esm/boundary.js.map +1 -1
  24. package/dist/esm/canonical.js +73 -0
  25. package/dist/esm/canonical.js.map +1 -0
  26. package/dist/esm/client.js +104 -39
  27. package/dist/esm/client.js.map +1 -1
  28. package/dist/esm/errors.js +39 -0
  29. package/dist/esm/errors.js.map +1 -1
  30. package/dist/esm/evidence_store.js +169 -0
  31. package/dist/esm/evidence_store.js.map +1 -0
  32. package/dist/esm/execution.js +116 -0
  33. package/dist/esm/execution.js.map +1 -0
  34. package/dist/esm/governance.js +170 -0
  35. package/dist/esm/governance.js.map +1 -0
  36. package/dist/esm/index.js +21 -4
  37. package/dist/esm/index.js.map +1 -1
  38. package/dist/esm/policy.js +44 -0
  39. package/dist/esm/policy.js.map +1 -0
  40. package/dist/esm/types.js +1 -1
  41. package/dist/esm/validation.js +99 -0
  42. package/dist/esm/validation.js.map +1 -0
  43. package/dist/esm/verify.js +287 -0
  44. package/dist/esm/verify.js.map +1 -0
  45. package/dist/types/attestation.d.ts +21 -1
  46. package/dist/types/attestation.d.ts.map +1 -1
  47. package/dist/types/auth.d.ts +52 -9
  48. package/dist/types/auth.d.ts.map +1 -1
  49. package/dist/types/boundary.d.ts +40 -2
  50. package/dist/types/boundary.d.ts.map +1 -1
  51. package/dist/types/canonical.d.ts +29 -0
  52. package/dist/types/canonical.d.ts.map +1 -0
  53. package/dist/types/client.d.ts +35 -9
  54. package/dist/types/client.d.ts.map +1 -1
  55. package/dist/types/errors.d.ts +14 -0
  56. package/dist/types/errors.d.ts.map +1 -1
  57. package/dist/types/evidence_store.d.ts +94 -0
  58. package/dist/types/evidence_store.d.ts.map +1 -0
  59. package/dist/types/execution.d.ts +52 -0
  60. package/dist/types/execution.d.ts.map +1 -0
  61. package/dist/types/governance.d.ts +128 -0
  62. package/dist/types/governance.d.ts.map +1 -0
  63. package/dist/types/index.d.ts +21 -5
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/policy.d.ts +32 -0
  66. package/dist/types/policy.d.ts.map +1 -0
  67. package/dist/types/types.d.ts +399 -12
  68. package/dist/types/types.d.ts.map +1 -1
  69. package/dist/types/validation.d.ts +12 -0
  70. package/dist/types/validation.d.ts.map +1 -0
  71. package/dist/types/verify.d.ts +51 -0
  72. package/dist/types/verify.d.ts.map +1 -0
  73. package/docs/evidence-store.md +67 -0
  74. package/docs/governance.md +77 -0
  75. package/docs/offline-verification.md +80 -0
  76. 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] — 2026-06-29
61
+ ## [0.53.0] - 2026-06-29
37
62
 
38
63
  ### Added
39
64
 
40
- - `GenesisMeshClient` — unified entry point with 7 domain sub-clients
41
- - `AgreementClient` — capability offer, counter, accept, verify
42
- - `BoundaryClient` — boundary decision and verification
43
- - `EvidenceClient` — trust evidence build and verify
44
- - `AttestationClient` — membership attestation issue, revoke, recognition policy
45
- - `DisclosureClient` — selective Merkle capability disclosure, nullifier
46
- - `ConsensusClient` — validator vote, consensus proof assembly and verify
47
- - `DataUsageClient` — data license policy, access intent, verify
48
- - `src/auth.ts` — `canonicalJson`, `signBytes`, `buildAdminHeaders` (Ed25519 / PKCS8-DER)
49
- - `src/client.ts` — `HttpTransport` with fetch, timeout, and typed error mapping
50
- - `src/errors.ts` — `GenesisMeshError` and typed subclasses for all NA error codes
51
- - `src/types.ts` — 30+ protocol interfaces matching the NA JSON wire format
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 ≥ 20 required. Zero runtime dependencies.**
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 — requires signing key)
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: JSON.stringify(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 # run 74 Jest tests
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
@@ -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
- * Admin authentication for Genesis Mesh NA endpoints.
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 — which operator key was used
7
- * X-Admin-Signature — Ed25519(canonicalJson({body,key_id,nonce,timestamp}))
8
- * X-Admin-Timestamp — ISO 8601 UTC timestamp
9
- * X-Admin-Nonce — UUID v4 replay-protection token
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 are recursively sorted, output is compact with no whitespace.
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
- /** Recursively produce compact sorted JSON — stable across Python and Node.js. */
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 || typeof value !== 'object')
22
- return JSON.stringify(value);
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(canonicalJson).join(',') + ']';
25
- const keys = Object.keys(value).sort();
26
- const pairs = keys.map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`);
27
- return '{' + pairs.join(',') + '}';
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
- /** Build the four admin auth headers for a given request body. */
43
- function buildAdminHeaders(body, keyId, signingKeyBase64) {
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
- const canonical = canonicalJson({ body, key_id: keyId, nonce, timestamp });
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
+ }
@@ -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
- /** Issue a signed boundary decision for a capability request (admin). */
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
+ }