genesis-mesh-sdk 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +51 -13
  2. package/README.md +21 -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 +180 -34
  8. package/dist/cjs/errors.js +63 -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/health.js +59 -0
  13. package/dist/cjs/index.js +77 -2
  14. package/dist/cjs/package.json +1 -0
  15. package/dist/cjs/policy.js +47 -0
  16. package/dist/cjs/types.js +1 -1
  17. package/dist/cjs/validation.js +103 -0
  18. package/dist/cjs/verify.js +299 -0
  19. package/dist/esm/attestation.js +23 -1
  20. package/dist/esm/attestation.js.map +1 -1
  21. package/dist/esm/auth.js +187 -19
  22. package/dist/esm/auth.js.map +1 -1
  23. package/dist/esm/boundary.js +20 -2
  24. package/dist/esm/boundary.js.map +1 -1
  25. package/dist/esm/canonical.js +73 -0
  26. package/dist/esm/canonical.js.map +1 -0
  27. package/dist/esm/client.js +181 -36
  28. package/dist/esm/client.js.map +1 -1
  29. package/dist/esm/errors.js +58 -0
  30. package/dist/esm/errors.js.map +1 -1
  31. package/dist/esm/evidence_store.js +169 -0
  32. package/dist/esm/evidence_store.js.map +1 -0
  33. package/dist/esm/execution.js +116 -0
  34. package/dist/esm/execution.js.map +1 -0
  35. package/dist/esm/governance.js +170 -0
  36. package/dist/esm/governance.js.map +1 -0
  37. package/dist/esm/health.js +56 -0
  38. package/dist/esm/health.js.map +1 -0
  39. package/dist/esm/index.js +26 -4
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/policy.js +44 -0
  42. package/dist/esm/policy.js.map +1 -0
  43. package/dist/esm/types.js +1 -1
  44. package/dist/esm/validation.js +99 -0
  45. package/dist/esm/validation.js.map +1 -0
  46. package/dist/esm/verify.js +287 -0
  47. package/dist/esm/verify.js.map +1 -0
  48. package/dist/types/attestation.d.ts +21 -1
  49. package/dist/types/attestation.d.ts.map +1 -1
  50. package/dist/types/auth.d.ts +52 -9
  51. package/dist/types/auth.d.ts.map +1 -1
  52. package/dist/types/boundary.d.ts +40 -2
  53. package/dist/types/boundary.d.ts.map +1 -1
  54. package/dist/types/canonical.d.ts +29 -0
  55. package/dist/types/canonical.d.ts.map +1 -0
  56. package/dist/types/client.d.ts +59 -12
  57. package/dist/types/client.d.ts.map +1 -1
  58. package/dist/types/errors.d.ts +25 -0
  59. package/dist/types/errors.d.ts.map +1 -1
  60. package/dist/types/evidence_store.d.ts +94 -0
  61. package/dist/types/evidence_store.d.ts.map +1 -0
  62. package/dist/types/execution.d.ts +52 -0
  63. package/dist/types/execution.d.ts.map +1 -0
  64. package/dist/types/governance.d.ts +128 -0
  65. package/dist/types/governance.d.ts.map +1 -0
  66. package/dist/types/health.d.ts +23 -0
  67. package/dist/types/health.d.ts.map +1 -0
  68. package/dist/types/index.d.ts +26 -5
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/dist/types/policy.d.ts +32 -0
  71. package/dist/types/policy.d.ts.map +1 -0
  72. package/dist/types/types.d.ts +443 -12
  73. package/dist/types/types.d.ts.map +1 -1
  74. package/dist/types/validation.d.ts +12 -0
  75. package/dist/types/validation.d.ts.map +1 -0
  76. package/dist/types/verify.d.ts +51 -0
  77. package/dist/types/verify.d.ts.map +1 -0
  78. package/docs/evidence-store.md +67 -0
  79. package/docs/governance.md +77 -0
  80. package/docs/high-availability.md +86 -0
  81. package/docs/offline-verification.md +80 -0
  82. package/package.json +16 -10
package/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ Versions align with the [Genesis Mesh release sequence](https://github.com/Genes
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.60.0] - 2026-10-01
11
+
12
+ Coordinated Genesis Mesh v0.60.0 release: client support for the Network
13
+ Authority's optional high availability.
14
+
15
+ - Add `baseUrls`: several NA instances without a load balancer. Idempotent
16
+ requests fail over on transport errors and 502/503/504. Non-idempotent
17
+ requests move only when the connection was never made, and are never
18
+ replayed after it may have been.
19
+ - Add `NetworkError.connectFailed`.
20
+ - Add `client.health`: `liveness()`, `readiness()` (typed `/readyz`, not-ready
21
+ returned as `ready: false` with the failing checks), `health()`, and
22
+ `endpoints()` to probe every configured instance.
23
+ - Add `isRetryableConflict()` and the `HaConflictCode` type for the 409 codes
24
+ an NA instance returns when it loses a race the database decided.
25
+ - Add a live HA test (`tests/e2e-ha.test.ts`): two instances on PostgreSQL
26
+ behind nginx, one killed mid-run. CI runs it against core `main`.
27
+
28
+ ## [0.59.1] - 2026-10-01
29
+
30
+ Coordinated Genesis Mesh v0.59.1 release: TypeScript support for governed
31
+ secret lifecycles (attestation-backed evaluation, policy lifecycle, evidence
32
+ store, offline verification).
33
+
34
+ - Add attestation-backed evaluation, attestation queries, recognition policy reads
35
+ and signed revocation feeds.
36
+ - Add boundary-policy management, the execution evidence-store client, signed
37
+ execution recording, governed actions and inventory reconciliation.
38
+ - Add Python-compatible canonical JSON, model digests and offline verification
39
+ of signed artifacts and complete evidence exports.
40
+ - Require verified decisions and expected bindings before governed callbacks;
41
+ guard evaluation and execution metadata before sending it to the NA.
42
+ - Add async signers, signed admin GETs, NDJSON paging, bounded opt-in retries and
43
+ typed errors carrying NA details and request IDs.
44
+ - Correct wire types and CommonJS package loading; require Node.js 22 or newer.
45
+ - Add Python-vector, negative-path, public-route and live local-NA tests, plus
46
+ typechecking of the test suite and package entry-point smoke checks.
47
+
10
48
  ## [0.59.0] - 2026-10-01
11
49
 
12
50
  Coordinated Genesis Mesh v0.59.0 release. No functional changes; the core adds
@@ -38,22 +76,22 @@ across the train (see the core `docs/development/versioning.md`).
38
76
 
39
77
  ---
40
78
 
41
- ## [0.53.0] — 2026-06-29
79
+ ## [0.53.0] - 2026-06-29
42
80
 
43
81
  ### Added
44
82
 
45
- - `GenesisMeshClient` — unified entry point with 7 domain sub-clients
46
- - `AgreementClient` — capability offer, counter, accept, verify
47
- - `BoundaryClient` — boundary decision and verification
48
- - `EvidenceClient` — trust evidence build and verify
49
- - `AttestationClient` — membership attestation issue, revoke, recognition policy
50
- - `DisclosureClient` — selective Merkle capability disclosure, nullifier
51
- - `ConsensusClient` — validator vote, consensus proof assembly and verify
52
- - `DataUsageClient` — data license policy, access intent, verify
53
- - `src/auth.ts` — `canonicalJson`, `signBytes`, `buildAdminHeaders` (Ed25519 / PKCS8-DER)
54
- - `src/client.ts` — `HttpTransport` with fetch, timeout, and typed error mapping
55
- - `src/errors.ts` — `GenesisMeshError` and typed subclasses for all NA error codes
56
- - `src/types.ts` — 30+ protocol interfaces matching the NA JSON wire format
83
+ - `GenesisMeshClient` - unified entry point with 7 domain sub-clients
84
+ - `AgreementClient` - capability offer, counter, accept, verify
85
+ - `BoundaryClient` - boundary decision and verification
86
+ - `EvidenceClient` - trust evidence build and verify
87
+ - `AttestationClient` - membership attestation issue, revoke, recognition policy
88
+ - `DisclosureClient` - selective Merkle capability disclosure, nullifier
89
+ - `ConsensusClient` - validator vote, consensus proof assembly and verify
90
+ - `DataUsageClient` - data license policy, access intent, verify
91
+ - `src/auth.ts` - `canonicalJson`, `signBytes`, `buildAdminHeaders` (Ed25519 / PKCS8-DER)
92
+ - `src/client.ts` - `HttpTransport` with fetch, timeout, and typed error mapping
93
+ - `src/errors.ts` - `GenesisMeshError` and typed subclasses for all NA error codes
94
+ - `src/types.ts` - 30+ protocol interfaces matching the NA JSON wire format
57
95
  - ESM and CJS dual build (`dist/esm/`, `dist/cjs/`, `dist/types/`)
58
96
  - 74 Jest unit tests covering all sub-clients
59
97
 
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,20 @@ 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
+ - [High availability: failover, readiness and conflicts](docs/high-availability.md)
41
+
42
+ `governedAction` requires verification keys and explicit expected policies. It
43
+ verifies the signed response and its request context before calling the supplied
44
+ action. Attestation-backed ALLOWs also require the expected attestation.
45
+
32
46
  ## Sub-clients
33
47
 
34
48
  ### agreement
@@ -38,7 +52,7 @@ const client = new GenesisMeshClient({
38
52
  > [Raw admin calls](#raw-admin-calls) below).
39
53
 
40
54
  ```typescript
41
- // Create a capability offer (admin — requires signing key)
55
+ // Create a capability offer (admin - requires signing key)
42
56
  const offer = await client.agreement.offer({
43
57
  responder_sovereign_id: 'BETA-NA',
44
58
  capabilities: ['read:data', 'write:log'],
@@ -212,7 +226,7 @@ For NA routes not yet covered by a sub-client (e.g. `/admin/recognition-treaties
212
226
  use `buildAdminHeaders` directly:
213
227
 
214
228
  ```typescript
215
- import { buildAdminHeaders } from 'genesis-mesh-sdk';
229
+ import { buildAdminHeaders, canonicalJson } from 'genesis-mesh-sdk';
216
230
 
217
231
  const body = {
218
232
  subject_sovereign_id: 'BETA-NA',
@@ -225,7 +239,7 @@ const headers = buildAdminHeaders(body, keyId, signingKeyBase64);
225
239
  const res = await fetch(`${baseUrl}/admin/recognition-treaties`, {
226
240
  method: 'POST',
227
241
  headers: { 'Content-Type': 'application/json', ...headers },
228
- body: JSON.stringify(body),
242
+ body: canonicalJson(body),
229
243
  });
230
244
  ```
231
245
 
@@ -268,7 +282,9 @@ The SDK handles all of this automatically when `signingKeyBase64` is provided.
268
282
  ```bash
269
283
  npm run build # ESM → dist/esm/ and CJS → dist/cjs/
270
284
  npm run typecheck # type-check without emitting
271
- npm test # run 74 Jest tests
285
+ npm test # unit tests and Python-vector verification
286
+ npm run test:package # validate ESM and CommonJS entry points after building
287
+ npm run test:e2e # disposable local Python NA (see verification guide)
272
288
  ```
273
289
 
274
290
  ## 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
+ }