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.
- package/CHANGELOG.md +33 -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/dist/cjs/client.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
49
|
-
const
|
|
50
|
-
let
|
|
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
|
-
|
|
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(
|
|
120
|
+
throw new errors_js_1.NetworkError(`${spec.method} ${spec.path} failed: ${err.message}`);
|
|
61
121
|
}
|
|
62
|
-
|
|
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.
|
|
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
|
}
|
package/dist/cjs/errors.js
CHANGED
|
@@ -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;
|