genesis-mesh-sdk 0.59.0 → 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 +33 -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
@@ -1,76 +1,142 @@
1
1
  "use strict";
2
2
  /**
3
3
  * HTTP transport layer for the Genesis Mesh SDK.
4
- * Handles request dispatch, response parsing, and error mapping.
4
+ * Handles request dispatch, response parsing, error mapping and opt-in retries.
5
5
  * Admin-route authentication is delegated to auth.ts.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.HttpTransport = void 0;
9
+ exports.buildPath = buildPath;
9
10
  const auth_js_1 = require("./auth.js");
10
11
  const errors_js_1 = require("./errors.js");
12
+ const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
13
+ function buildPath(path, query) {
14
+ if (!query)
15
+ return path;
16
+ const params = new URLSearchParams();
17
+ for (const [key, value] of Object.entries(query)) {
18
+ if (value !== undefined && value !== null)
19
+ params.append(key, String(value));
20
+ }
21
+ const qs = params.toString();
22
+ return qs ? `${path}?${qs}` : path;
23
+ }
11
24
  class HttpTransport {
12
25
  baseUrl;
13
- signingKeyBase64;
26
+ /** Key ID sent in X-Admin-Key-Id. */
14
27
  keyId;
28
+ signer;
15
29
  timeout;
30
+ retry;
31
+ headers;
16
32
  _fetch;
17
33
  constructor(options) {
18
34
  this.baseUrl = options.baseUrl.replace(/\/$/, '');
19
- this.signingKeyBase64 = options.signingKeyBase64;
20
- this.keyId = options.keyId ?? 'operator-local';
35
+ this.signer = options.signer
36
+ ?? (options.signingKeyBase64
37
+ ? (0, auth_js_1.seedSigner)(options.signingKeyBase64, options.keyId ?? 'operator-local')
38
+ : undefined);
39
+ this.keyId = this.signer?.keyId ?? options.keyId ?? 'operator-local';
21
40
  this.timeout = options.timeout ?? 10_000;
41
+ this.retry = { attempts: options.retry?.attempts ?? 0, baseDelayMs: options.retry?.baseDelayMs ?? 200 };
42
+ if (!Number.isSafeInteger(this.retry.attempts) || this.retry.attempts < 0 || this.retry.attempts > 10
43
+ || !Number.isFinite(this.retry.baseDelayMs) || this.retry.baseDelayMs < 0 || this.retry.baseDelayMs > 60_000) {
44
+ throw new Error('retry requires 0..10 attempts and a finite baseDelayMs between 0 and 60000');
45
+ }
46
+ this.headers = options.headers ?? {};
22
47
  this._fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
23
48
  }
24
- async adminPost(path, body) {
25
- if (!this.signingKeyBase64) {
26
- throw new Error('signingKeyBase64 is required for admin routes');
27
- }
28
- const adminHeaders = (0, auth_js_1.buildAdminHeaders)(body, this.keyId, this.signingKeyBase64);
29
- return this._post(path, body, adminHeaders);
49
+ /** Signed admin POST. Not retried unless `idempotent` is set. */
50
+ async adminPost(path, body, idempotent = false) {
51
+ const response = await this._send({ method: 'POST', path, body, admin: true, idempotent });
52
+ return this._parse(response);
30
53
  }
31
- async publicPost(path, body) {
32
- return this._post(path, body, {});
54
+ /** Signed admin GET (the NA verifies the signature over an empty body). */
55
+ async adminGet(path, query) {
56
+ const response = await this._send({ method: 'GET', path, query, admin: true, idempotent: true });
57
+ return this._parse(response);
33
58
  }
34
- async publicGet(path) {
35
- const url = this.baseUrl + path;
36
- let response;
37
- try {
38
- response = await this._fetch(url, {
39
- method: 'GET',
40
- signal: AbortSignal.timeout(this.timeout),
41
- });
42
- }
43
- catch (err) {
44
- throw new errors_js_1.NetworkError(`GET ${path} failed: ${err.message}`);
45
- }
59
+ /** Signed admin GET returning the raw response text (e.g. NDJSON export). */
60
+ async adminGetText(path, query) {
61
+ const response = await this._send({ method: 'GET', path, query, admin: true, idempotent: true });
62
+ if (!response.ok)
63
+ return this._parse(response);
64
+ return response.text();
65
+ }
66
+ /** Unauthenticated POST. Verification routes are idempotent; pass `true` to allow retries. */
67
+ async publicPost(path, body, idempotent = false) {
68
+ const response = await this._send({ method: 'POST', path, body, admin: false, idempotent });
69
+ return this._parse(response);
70
+ }
71
+ async publicGet(path, query) {
72
+ const response = await this._send({ method: 'GET', path, query, admin: false, idempotent: true });
46
73
  return this._parse(response);
47
74
  }
48
- async _post(path, body, extraHeaders) {
49
- const url = this.baseUrl + path;
50
- let response;
75
+ async _send(spec) {
76
+ const attempts = spec.idempotent ? this.retry.attempts : 0;
77
+ for (let attempt = 0;; attempt++) {
78
+ try {
79
+ const response = await this._once(spec);
80
+ if (attempt < attempts && RETRYABLE_STATUS.has(response.status)) {
81
+ await this._backoff(attempt);
82
+ continue;
83
+ }
84
+ let body;
85
+ try {
86
+ body = await response.text();
87
+ }
88
+ catch {
89
+ throw new errors_js_1.NetworkError(`Failed to read response body (${spec.method} ${spec.path})`);
90
+ }
91
+ return { ok: response.ok, status: response.status, text: async () => body };
92
+ }
93
+ catch (err) {
94
+ if (attempt < attempts && err instanceof errors_js_1.NetworkError) {
95
+ await this._backoff(attempt);
96
+ continue;
97
+ }
98
+ throw err;
99
+ }
100
+ }
101
+ }
102
+ async _once(spec) {
103
+ const headers = { ...this.headers };
104
+ if (spec.admin) {
105
+ if (!this.signer) {
106
+ throw new Error('signingKeyBase64 is required for admin routes (or pass a signer)');
107
+ }
108
+ Object.assign(headers, await (0, auth_js_1.buildAdminHeadersWithSigner)(spec.method === 'GET' ? {} : spec.body, this.signer));
109
+ }
110
+ const init = { method: spec.method, headers, signal: AbortSignal.timeout(this.timeout) };
111
+ if (spec.method === 'POST') {
112
+ headers['Content-Type'] = 'application/json';
113
+ init.body = (0, auth_js_1.canonicalJson)(spec.body);
114
+ }
115
+ const path = buildPath(spec.path, spec.query);
51
116
  try {
52
- response = await this._fetch(url, {
53
- method: 'POST',
54
- headers: { 'Content-Type': 'application/json', ...extraHeaders },
55
- body: JSON.stringify(body),
56
- signal: AbortSignal.timeout(this.timeout),
57
- });
117
+ return await this._fetch(this.baseUrl + path, init);
58
118
  }
59
119
  catch (err) {
60
- throw new errors_js_1.NetworkError(`POST ${path} failed: ${err.message}`);
120
+ throw new errors_js_1.NetworkError(`${spec.method} ${spec.path} failed: ${err.message}`);
61
121
  }
62
- return this._parse(response);
122
+ }
123
+ _backoff(attempt) {
124
+ const delay = Math.min(60_000, this.retry.baseDelayMs * 2 ** attempt);
125
+ return new Promise(resolve => setTimeout(resolve, delay));
63
126
  }
64
127
  async _parse(response) {
65
128
  let data;
66
129
  try {
67
- data = await response.json();
130
+ data = (0, auth_js_1.parseJson)(await response.text());
68
131
  }
69
132
  catch {
133
+ if (!response.ok) {
134
+ throw new errors_js_1.GenesisMeshError(`HTTP ${response.status} with a non-JSON body`, 'unknown', response.status);
135
+ }
70
136
  throw new errors_js_1.NetworkError(`Failed to parse response body (HTTP ${response.status})`);
71
137
  }
72
138
  if (!response.ok) {
73
- throw (0, errors_js_1.fromHttpError)(response.status, data);
139
+ throw (0, errors_js_1.fromHttpError)(response.status, (data ?? {}));
74
140
  }
75
141
  return data;
76
142
  }
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  /** Typed error classes mirroring the NA HTTP error surface. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.BadRequestError = exports.NetworkError = exports.RateLimitError = exports.NotFoundError = exports.ValidationError = exports.UnauthorizedError = exports.GenesisMeshError = void 0;
4
+ exports.BadRequestError = exports.NetworkError = exports.RateLimitError = exports.NotFoundError = exports.ServiceUnavailableError = exports.ConflictError = exports.ForbiddenError = exports.ValidationError = exports.UnauthorizedError = exports.GenesisMeshError = void 0;
5
5
  exports.fromHttpError = fromHttpError;
6
6
  class GenesisMeshError extends Error {
7
7
  code;
8
8
  status;
9
+ /** Structured `error.details` from the NA envelope (empty when absent). */
10
+ details = {};
11
+ /** `error.request_id` from the NA envelope, for correlating with NA logs. */
12
+ requestId = null;
9
13
  constructor(message, code, status) {
10
14
  super(message);
11
15
  this.name = 'GenesisMeshError';
@@ -29,6 +33,28 @@ class ValidationError extends GenesisMeshError {
29
33
  }
30
34
  }
31
35
  exports.ValidationError = ValidationError;
36
+ class ForbiddenError extends GenesisMeshError {
37
+ constructor(message = 'Forbidden', code = 'forbidden') {
38
+ super(message, code, 403);
39
+ this.name = 'ForbiddenError';
40
+ }
41
+ }
42
+ exports.ForbiddenError = ForbiddenError;
43
+ /** 409: the request conflicts with stored state (e.g. `evidence_conflict`, `executor_key_exists`). */
44
+ class ConflictError extends GenesisMeshError {
45
+ constructor(message, code = 'conflict') {
46
+ super(message, code, 409);
47
+ this.name = 'ConflictError';
48
+ }
49
+ }
50
+ exports.ConflictError = ConflictError;
51
+ class ServiceUnavailableError extends GenesisMeshError {
52
+ constructor(message = 'Service unavailable', code = 'service_unavailable') {
53
+ super(message, code, 503);
54
+ this.name = 'ServiceUnavailableError';
55
+ }
56
+ }
57
+ exports.ServiceUnavailableError = ServiceUnavailableError;
32
58
  class NotFoundError extends GenesisMeshError {
33
59
  constructor(message, code = 'not_found') {
34
60
  super(message, code, 404);
@@ -67,21 +93,37 @@ function fromHttpError(status, body) {
67
93
  const errorField = body['error'];
68
94
  let message;
69
95
  let code;
96
+ let details;
97
+ let requestId;
70
98
  if (typeof errorField === 'object' && errorField !== null) {
71
99
  const nested = errorField;
72
100
  message = String(nested['message'] ?? 'Unknown error');
73
101
  code = String(nested['code'] ?? 'unknown');
102
+ details = nested['details'];
103
+ requestId = nested['request_id'];
74
104
  }
75
105
  else {
76
106
  message = String(errorField ?? body['message'] ?? 'Unknown error');
77
107
  code = String(body['code'] ?? 'unknown');
78
108
  }
109
+ const error = errorForStatus(status, message, code);
110
+ if (typeof details === 'object' && details !== null && !Array.isArray(details)) {
111
+ error.details = details;
112
+ }
113
+ if (typeof requestId === 'string')
114
+ error.requestId = requestId;
115
+ return error;
116
+ }
117
+ function errorForStatus(status, message, code) {
79
118
  switch (status) {
80
119
  case 400: return new BadRequestError(message, code);
81
120
  case 401: return new UnauthorizedError(message, code);
121
+ case 403: return new ForbiddenError(message, code);
82
122
  case 404: return new NotFoundError(message, code);
123
+ case 409: return new ConflictError(message, code);
83
124
  case 422: return new ValidationError(message, code);
84
125
  case 429: return new RateLimitError(message, code);
126
+ case 503: return new ServiceUnavailableError(message, code);
85
127
  default: return new GenesisMeshError(message, code, status);
86
128
  }
87
129
  }
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EvidenceStoreClient = void 0;
4
+ const execution_js_1 = require("./execution.js");
5
+ const canonical_js_1 = require("./canonical.js");
6
+ const errors_js_1 = require("./errors.js");
7
+ const verify_js_1 = require("./verify.js");
8
+ const MAX_PAGE = 1000;
9
+ function resourcePath(resourceId) {
10
+ return resourceId.split('/').map(encodeURIComponent).join('/');
11
+ }
12
+ /** The NA evidence store (v0.59): controller submission, operator search, history, export. */
13
+ class EvidenceStoreClient {
14
+ http;
15
+ constructor(http) {
16
+ this.http = http;
17
+ }
18
+ /**
19
+ * Submit one signed ExecutionEvidence record. Authenticated by the executor
20
+ * signature, not by operator headers. An identical resubmission returns
21
+ * `status: "duplicate"`, so it is safe to retry.
22
+ */
23
+ async submit(evidence) {
24
+ const reason = (0, execution_js_1.checkMetadataOnly)(evidence.execution_parameters, evidence.outcome_detail);
25
+ if (reason)
26
+ throw new execution_js_1.SecretMaterialError(reason);
27
+ return this.http.publicPost('/evidence/execution', { evidence }, true);
28
+ }
29
+ /** Search stored entries (admin). Use `next_after_sequence` as the next `after_sequence`. */
30
+ search(params = {}) {
31
+ return this.http.adminGet('/admin/evidence', { ...params });
32
+ }
33
+ /** Every matching entry, following pages (admin). */
34
+ async *iterate(params = {}) {
35
+ let after = 0;
36
+ while (after !== null) {
37
+ const page = await this.search({ ...params, after_sequence: after });
38
+ if (page.next_after_sequence !== null && (!Number.isSafeInteger(page.next_after_sequence) || page.next_after_sequence <= after)) {
39
+ throw new Error('evidence search cursor did not advance');
40
+ }
41
+ yield* page.entries;
42
+ after = page.next_after_sequence;
43
+ }
44
+ }
45
+ /** Store mode, size, last sequence and retention checkpoint (admin). */
46
+ status() {
47
+ return this.http.adminGet('/admin/evidence/status');
48
+ }
49
+ /** Verify every stored entry, chain and signature on the NA (admin). */
50
+ verify() {
51
+ return this.http.adminGet('/admin/evidence/verify');
52
+ }
53
+ /** One resource's full history, decision to execution, verified by the NA (admin). */
54
+ resourceHistory(resourceId) {
55
+ return this.http.adminGet(`/admin/evidence/resources/${resourcePath(resourceId)}`);
56
+ }
57
+ /** A vendor's decisions and the evidence under them, verified by the NA (admin). */
58
+ vendorHistory(vendorId) {
59
+ return this.http.adminGet(`/admin/evidence/vendors/${encodeURIComponent(vendorId)}`);
60
+ }
61
+ /** One page of `gm.evidence.event` JSON Lines, unparsed (admin). */
62
+ exportText(params = {}) {
63
+ return this.http.adminGetText('/admin/evidence/export', { ...params });
64
+ }
65
+ /** One page of export events, parsed (admin). */
66
+ async export(params = {}) {
67
+ return (0, verify_js_1.parseExportLines)(await this.exportText(params));
68
+ }
69
+ /** Every event from `since_sequence` onward, following pages - an incremental SIEM pull (admin). */
70
+ async *exportAll(sinceSequence = 0, pageSize = MAX_PAGE) {
71
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > MAX_PAGE
72
+ || !Number.isSafeInteger(sinceSequence) || sinceSequence < 0) {
73
+ throw new Error('export requires a nonnegative cursor and page size between 1 and 1000');
74
+ }
75
+ let since = sinceSequence;
76
+ for (;;) {
77
+ const events = await this.export({ since_sequence: since, limit: pageSize });
78
+ if (events.length && events[events.length - 1].entry.store_sequence <= since) {
79
+ throw new Error('evidence export cursor did not advance');
80
+ }
81
+ yield* events;
82
+ if (events.length < pageSize)
83
+ return;
84
+ since = events[events.length - 1].entry.store_sequence;
85
+ }
86
+ }
87
+ /** Registered executor keys, retired keys included (admin). */
88
+ async listExecutorKeys() {
89
+ const body = await this.http.adminGet('/admin/evidence/executor-keys');
90
+ return body.executor_keys;
91
+ }
92
+ /** Register a controller's executor signing key (admin, privileged). */
93
+ registerExecutorKey(params) {
94
+ return this.http.adminPost('/admin/evidence/executor-keys', params);
95
+ }
96
+ /** Retire an executor key: it still verifies old records and can sign no new ones (admin, privileged). */
97
+ retireExecutorKey(keyId) {
98
+ return this.http.adminPost(`/admin/evidence/executor-keys/${encodeURIComponent(keyId)}/retire`, {});
99
+ }
100
+ /** Remove entries older than `olderThanDays` behind a signed checkpoint (admin, privileged). */
101
+ applyRetention(olderThanDays) {
102
+ return this.http.adminPost('/admin/evidence/retention/apply', { older_than_days: olderThanDays });
103
+ }
104
+ /** The most recent retention checkpoint in the store, or null (admin). */
105
+ async latestCheckpoint() {
106
+ let latest = null;
107
+ for await (const event of this.iterate({ entry_kind: 'retention_checkpoint' })) {
108
+ latest = event.payload;
109
+ }
110
+ return latest;
111
+ }
112
+ /**
113
+ * The head of a resource chain - what the next record must link to - or null
114
+ * for a resource with no history. Falls back to the latest retention
115
+ * checkpoint when retention removed every stored record (admin).
116
+ */
117
+ async resourceHead(resourceId) {
118
+ let history;
119
+ try {
120
+ history = await this.resourceHistory(resourceId);
121
+ }
122
+ catch (err) {
123
+ if (err instanceof errors_js_1.NotFoundError && err.code === 'resource_not_found') {
124
+ return (await this.latestCheckpoint())?.resource_heads[resourceId] ?? null;
125
+ }
126
+ throw err;
127
+ }
128
+ if (!history.verification?.verified)
129
+ throw new Error('resource history did not verify');
130
+ let head = null;
131
+ for (const event of history.entries) {
132
+ if (event.entry.entry_kind !== 'execution')
133
+ continue;
134
+ const record = event.payload;
135
+ if (record.resource_id !== resourceId || record.resource_sequence === undefined)
136
+ continue;
137
+ if (!head || record.resource_sequence > head.resource_sequence) {
138
+ head = { resource_sequence: record.resource_sequence, record_digest: (0, canonical_js_1.executionDigest)(record) };
139
+ }
140
+ }
141
+ return head;
142
+ }
143
+ /**
144
+ * Latest state of every resource with stored execution evidence, for
145
+ * reconciliation against a cloud inventory (admin).
146
+ */
147
+ async resourceStates() {
148
+ const states = new Map();
149
+ for await (const event of this.iterate({ entry_kind: 'execution' })) {
150
+ const record = event.payload;
151
+ if (record.resource_id === undefined || record.resource_sequence === undefined)
152
+ continue;
153
+ const previous = states.get(record.resource_id);
154
+ if (previous && previous.resource_sequence >= record.resource_sequence)
155
+ continue;
156
+ const success = record.outcome === 'success';
157
+ states.set(record.resource_id, {
158
+ resource_id: record.resource_id,
159
+ resource_sequence: record.resource_sequence,
160
+ record_digest: (0, canonical_js_1.executionDigest)(record),
161
+ last_action: record.resource_action ?? null,
162
+ last_outcome: record.outcome,
163
+ last_success_action: success ? record.resource_action ?? null : previous?.last_success_action ?? null,
164
+ last_success_parameters: success ? record.execution_parameters : previous?.last_success_parameters ?? null,
165
+ last_executed_at: record.executed_at,
166
+ last_decision_id: record.decision_id,
167
+ });
168
+ }
169
+ return states;
170
+ }
171
+ }
172
+ exports.EvidenceStoreClient = EvidenceStoreClient;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Execution evidence: build and sign records exactly as the Python reference
4
+ * `record_execution` does, and refuse secret material before anything is signed.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ExecutionRecorder = exports.SecretMaterialError = exports.MAX_METADATA_BYTES = void 0;
8
+ exports.checkMetadataOnly = checkMetadataOnly;
9
+ const node_crypto_1 = require("node:crypto");
10
+ const auth_js_1 = require("./auth.js");
11
+ const canonical_js_1 = require("./canonical.js");
12
+ const errors_js_1 = require("./errors.js");
13
+ /** Limit on execution_parameters + outcome_detail, as enforced by the NA. */
14
+ exports.MAX_METADATA_BYTES = 16 * 1024;
15
+ const SECRET_KEYS = new Set([
16
+ 'value', 'secret', 'secretvalue', 'password', 'passwd', 'passphrase', 'token',
17
+ 'accesstoken', 'refreshtoken', 'bearer', 'privatekey', 'keymaterial',
18
+ 'credential', 'credentials', 'clientsecret', 'apikey', 'pem', 'connectionstring',
19
+ ]);
20
+ const LONG_OPAQUE = /^[A-Za-z0-9+/=_-]{120,}$/;
21
+ const JWT = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
22
+ /** Thrown before signing when a record would carry secret material; the NA would refuse it. */
23
+ class SecretMaterialError extends errors_js_1.GenesisMeshError {
24
+ constructor(message) {
25
+ super(message, 'evidence_secret_material', 0);
26
+ this.name = 'SecretMaterialError';
27
+ }
28
+ }
29
+ exports.SecretMaterialError = SecretMaterialError;
30
+ function normaliseKey(key) {
31
+ return key.toLowerCase().replace(/[-_.]/g, '');
32
+ }
33
+ function secretMaterial(value, path = '') {
34
+ if (Array.isArray(value)) {
35
+ for (let i = 0; i < value.length; i++) {
36
+ const found = secretMaterial(value[i], `${path}${i}.`);
37
+ if (found)
38
+ return found;
39
+ }
40
+ }
41
+ else if (value !== null && typeof value === 'object') {
42
+ for (const [key, inner] of Object.entries(value)) {
43
+ if (SECRET_KEYS.has(normaliseKey(key)))
44
+ return `field '${path}${key}' is not allowed in evidence metadata`;
45
+ const found = secretMaterial(inner, `${path}${key}.`);
46
+ if (found)
47
+ return found;
48
+ }
49
+ }
50
+ else if (typeof value === 'string') {
51
+ const field = path.replace(/\.$/, '');
52
+ if (value.includes('-----BEGIN'))
53
+ return `field '${field}' contains a PEM block`;
54
+ if (LONG_OPAQUE.test(value) || JWT.test(value))
55
+ return `field '${field}' looks like key or token material`;
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Why the metadata would be refused as secret material, or null. A guard, not a
61
+ * guarantee: send identifiers, versions and timestamps, never secret values.
62
+ */
63
+ function checkMetadataOnly(executionParameters, outcomeDetail = null) {
64
+ const size = Buffer.byteLength(JSON.stringify({ execution_parameters: executionParameters, outcome_detail: outcomeDetail }), 'utf-8');
65
+ if (size > exports.MAX_METADATA_BYTES)
66
+ return `metadata is ${size} bytes, over the ${exports.MAX_METADATA_BYTES}-byte limit`;
67
+ return secretMaterial(executionParameters) ?? (outcomeDetail ? secretMaterial({ outcome_detail: outcomeDetail }) : null);
68
+ }
69
+ function resourceHeadOf(prior) {
70
+ if ('record_digest' in prior)
71
+ return prior;
72
+ return { resource_sequence: prior.resource_sequence ?? 0, record_digest: (0, canonical_js_1.executionDigest)(prior) };
73
+ }
74
+ /** Builds and signs ExecutionEvidence for one executor. */
75
+ class ExecutionRecorder {
76
+ executorSovereignId;
77
+ signer;
78
+ constructor(options) {
79
+ this.executorSovereignId = options.executorSovereignId;
80
+ this.signer = options.signer;
81
+ }
82
+ get keyId() {
83
+ return this.signer.keyId;
84
+ }
85
+ async record(params) {
86
+ if ((params.resource_id === undefined) !== (params.resource_action === undefined)) {
87
+ throw new Error('resource_id and resource_action go together');
88
+ }
89
+ const execution_parameters = params.execution_parameters ?? {};
90
+ const outcome_detail = params.outcome_detail ?? null;
91
+ const secret = checkMetadataOnly(execution_parameters, outcome_detail);
92
+ if (secret)
93
+ throw new SecretMaterialError(secret);
94
+ const prior = params.prior_record ?? null;
95
+ const record = {
96
+ evidence_id: params.evidence_id ?? (0, node_crypto_1.randomUUID)(),
97
+ sequence_no: prior ? prior.sequence_no + 1 : 1,
98
+ decision_id: params.decision.decision_id,
99
+ context_id: params.decision.context_id,
100
+ agreement_id: params.decision.agreement_id,
101
+ executor_sovereign_id: this.executorSovereignId,
102
+ executed_capability: params.executed_capability,
103
+ execution_parameters,
104
+ executed_at: (0, auth_js_1.pythonTimestamp)(params.executed_at ?? new Date()),
105
+ outcome: params.outcome,
106
+ outcome_detail,
107
+ prev_evidence_digest: prior ? (0, canonical_js_1.executionDigest)(prior) : null,
108
+ signature: null,
109
+ };
110
+ if (params.resource_id !== undefined && params.resource_action !== undefined) {
111
+ const head = params.prior_resource ? resourceHeadOf(params.prior_resource) : null;
112
+ record.resource_id = params.resource_id;
113
+ record.resource_action = params.resource_action;
114
+ record.resource_sequence = head ? head.resource_sequence + 1 : 1;
115
+ record.prev_resource_digest = head ? head.record_digest : null;
116
+ }
117
+ record.signature = await (0, auth_js_1.signCanonical)((0, canonical_js_1.executionCanonical)(record), this.signer);
118
+ return record;
119
+ }
120
+ }
121
+ exports.ExecutionRecorder = ExecutionRecorder;