genesis-mesh-sdk 0.59.1 → 0.61.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.
- package/CHANGELOG.md +42 -0
- package/README.md +8 -0
- package/dist/cjs/auth.js +10 -3
- package/dist/cjs/canonical.js +22 -0
- package/dist/cjs/client.js +103 -23
- package/dist/cjs/data_intent.js +35 -0
- package/dist/cjs/errors.js +20 -0
- package/dist/cjs/health.js +59 -0
- package/dist/cjs/index.js +18 -2
- package/dist/cjs/verify.js +71 -0
- package/dist/esm/auth.js +10 -3
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/canonical.js +19 -0
- package/dist/esm/canonical.js.map +1 -1
- package/dist/esm/client.js +103 -23
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/data_intent.js +33 -0
- package/dist/esm/data_intent.js.map +1 -0
- package/dist/esm/errors.js +19 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/health.js +56 -0
- package/dist/esm/health.js.map +1 -0
- package/dist/esm/index.js +10 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/verify.js +69 -1
- package/dist/esm/verify.js.map +1 -1
- package/dist/types/auth.d.ts +5 -3
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/canonical.d.ts +5 -1
- package/dist/types/canonical.d.ts.map +1 -1
- package/dist/types/client.d.ts +24 -3
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/data_intent.d.ts +27 -0
- package/dist/types/data_intent.d.ts.map +1 -0
- package/dist/types/errors.d.ts +11 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/health.d.ts +23 -0
- package/dist/types/health.d.ts.map +1 -0
- package/dist/types/index.d.ts +11 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.d.ts +65 -14
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/verify.d.ts +33 -1
- package/dist/types/verify.d.ts.map +1 -1
- package/docs/high-availability.md +86 -0
- package/docs/offline-verification.md +38 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,48 @@ Versions align with the [Genesis Mesh release sequence](https://github.com/Genes
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.61.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
Coordinated Genesis Mesh v0.61.0 release: the cross-language interoperability
|
|
13
|
+
proof. The core's interop scenario uses this SDK to sign data access intents,
|
|
14
|
+
submit them to the NA, and verify Python records.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `verifyAgreement`, `verifyDataLicensePolicySignature` and
|
|
19
|
+
`verifyDataAccessIntent`: offline verification with the reason codes of the
|
|
20
|
+
Python reference.
|
|
21
|
+
- `createDataAccessIntent`: build and sign a data access intent as the agent.
|
|
22
|
+
- `agreementCanonical`, `dataLicensePolicyCanonical`,
|
|
23
|
+
`dataAccessIntentCanonical`.
|
|
24
|
+
- The shared `interop` conformance vectors (25) in
|
|
25
|
+
`tests/fixtures/conformance/interop.json`, all passing.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `AgreementRecord`, `AgreementTerms`, `CapabilityOffer`, `CapabilityCounter`,
|
|
30
|
+
`DataAccessIntent` and `DataSourceDescriptor` now match the wire format.
|
|
31
|
+
- `parseJson` keeps integer literals beyond `Number.MAX_SAFE_INTEGER` exactly
|
|
32
|
+
(as `bigint`), and `canonicalJson` encodes them.
|
|
33
|
+
|
|
34
|
+
## [0.60.0] - 2026-10-01
|
|
35
|
+
|
|
36
|
+
Coordinated Genesis Mesh v0.60.0 release: client support for the Network
|
|
37
|
+
Authority's optional high availability.
|
|
38
|
+
|
|
39
|
+
- Add `baseUrls`: several NA instances without a load balancer. Idempotent
|
|
40
|
+
requests fail over on transport errors and 502/503/504. Non-idempotent
|
|
41
|
+
requests move only when the connection was never made, and are never
|
|
42
|
+
replayed after it may have been.
|
|
43
|
+
- Add `NetworkError.connectFailed`.
|
|
44
|
+
- Add `client.health`: `liveness()`, `readiness()` (typed `/readyz`, not-ready
|
|
45
|
+
returned as `ready: false` with the failing checks), `health()`, and
|
|
46
|
+
`endpoints()` to probe every configured instance.
|
|
47
|
+
- Add `isRetryableConflict()` and the `HaConflictCode` type for the 409 codes
|
|
48
|
+
an NA instance returns when it loses a race the database decided.
|
|
49
|
+
- Add a live HA test (`tests/e2e-ha.test.ts`): two instances on PostgreSQL
|
|
50
|
+
behind nginx, one killed mid-run. CI runs it against core `main`.
|
|
51
|
+
|
|
10
52
|
## [0.59.1] - 2026-10-01
|
|
11
53
|
|
|
12
54
|
Coordinated Genesis Mesh v0.59.1 release: TypeScript support for governed
|
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ execution recording, evidence-store queries and offline verification.
|
|
|
37
37
|
- [Governed actions and reconciliation](docs/governance.md)
|
|
38
38
|
- [Evidence store, policies, signers and retries](docs/evidence-store.md)
|
|
39
39
|
- [Offline verification and integration tests](docs/offline-verification.md)
|
|
40
|
+
- [High availability: failover, readiness and conflicts](docs/high-availability.md)
|
|
40
41
|
|
|
41
42
|
`governedAction` requires verification keys and explicit expected policies. It
|
|
42
43
|
verifies the signed response and its request context before calling the supplied
|
|
@@ -219,6 +220,13 @@ const dv = await client.dataUsage.verify({ intent, policy: pol });
|
|
|
219
220
|
console.log(dv.valid, dv.violations);
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
## Offline verification
|
|
224
|
+
|
|
225
|
+
The SDK verifies boundary decisions, evidence exports, and from 0.61.0
|
|
226
|
+
agreements, data license policies and data access intents offline, and signs
|
|
227
|
+
data access intents with `createDataAccessIntent`. See
|
|
228
|
+
[docs/offline-verification.md](docs/offline-verification.md).
|
|
229
|
+
|
|
222
230
|
## Raw admin calls
|
|
223
231
|
|
|
224
232
|
For NA routes not yet covered by a sub-client (e.g. `/admin/recognition-treaties`),
|
package/dist/cjs/auth.js
CHANGED
|
@@ -74,12 +74,17 @@ function pythonNumber(value, pythonFloat = false) {
|
|
|
74
74
|
*/
|
|
75
75
|
const PYTHON_FLOATS = new WeakMap();
|
|
76
76
|
/**
|
|
77
|
-
* JSON.parse that
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* JSON.parse that keeps what Python would sign: integral-valued floats are
|
|
78
|
+
* remembered so canonicalJson reproduces them (`90.0`), and integer literals
|
|
79
|
+
* beyond Number.MAX_SAFE_INTEGER become `bigint` so no digit is lost. Use it
|
|
80
|
+
* for any NA JSON that will be verified or digested. Requires JSON.parse source
|
|
81
|
+
* text access (Node.js 22+).
|
|
80
82
|
*/
|
|
81
83
|
function parseJson(text) {
|
|
82
84
|
return JSON.parse(text, function (key, value, context) {
|
|
85
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value) && context?.source && /^-?\d+$/.test(context.source)) {
|
|
86
|
+
return BigInt(context.source);
|
|
87
|
+
}
|
|
83
88
|
if (typeof value === 'number' && Number.isInteger(value) && context?.source && /[.eE]/.test(context.source)) {
|
|
84
89
|
let keys = PYTHON_FLOATS.get(this);
|
|
85
90
|
if (!keys)
|
|
@@ -112,6 +117,8 @@ function canonicalJson(value) {
|
|
|
112
117
|
return asciiString(value);
|
|
113
118
|
if (typeof value === 'number')
|
|
114
119
|
return pythonNumber(value);
|
|
120
|
+
if (typeof value === 'bigint')
|
|
121
|
+
return value.toString();
|
|
115
122
|
if (typeof value === 'boolean')
|
|
116
123
|
return value ? 'true' : 'false';
|
|
117
124
|
if (Array.isArray(value))
|
package/dist/cjs/canonical.js
CHANGED
|
@@ -20,6 +20,9 @@ exports.freshnessProofCanonical = freshnessProofCanonical;
|
|
|
20
20
|
exports.checkpointCanonical = checkpointCanonical;
|
|
21
21
|
exports.entryDigest = entryDigest;
|
|
22
22
|
exports.payloadDigest = payloadDigest;
|
|
23
|
+
exports.agreementCanonical = agreementCanonical;
|
|
24
|
+
exports.dataLicensePolicyCanonical = dataLicensePolicyCanonical;
|
|
25
|
+
exports.dataAccessIntentCanonical = dataAccessIntentCanonical;
|
|
23
26
|
const auth_js_1 = require("./auth.js");
|
|
24
27
|
/** v0.59 resource-chain fields, omitted from the execution canonical form when absent. */
|
|
25
28
|
exports.RESOURCE_CHAIN_FIELDS = [
|
|
@@ -87,3 +90,22 @@ function entryDigest(entry) {
|
|
|
87
90
|
function payloadDigest(payload) {
|
|
88
91
|
return (0, auth_js_1.canonicalDigest)(payload);
|
|
89
92
|
}
|
|
93
|
+
/** Fields both parties sign; identical for CapabilityCounter and AgreementRecord (excludes ids and timestamps). */
|
|
94
|
+
const AGREEMENT_CANONICAL_FIELDS = [
|
|
95
|
+
'agreed_terms', 'graph_digest', 'offer_id', 'offerer_evidence',
|
|
96
|
+
'offerer_sovereign_id', 'responder_evidence', 'responder_sovereign_id',
|
|
97
|
+
];
|
|
98
|
+
/** The body both parties of an agreement sign. */
|
|
99
|
+
function agreementCanonical(record) {
|
|
100
|
+
const source = record;
|
|
101
|
+
const body = {};
|
|
102
|
+
for (const key of AGREEMENT_CANONICAL_FIELDS)
|
|
103
|
+
body[key] = source[key] ?? null;
|
|
104
|
+
return (0, auth_js_1.canonicalJson)(body);
|
|
105
|
+
}
|
|
106
|
+
function dataLicensePolicyCanonical(policy) {
|
|
107
|
+
return (0, auth_js_1.canonicalJson)(without(policy, ['signature']));
|
|
108
|
+
}
|
|
109
|
+
function dataAccessIntentCanonical(intent) {
|
|
110
|
+
return (0, auth_js_1.canonicalJson)(without(intent, ['signature']));
|
|
111
|
+
}
|
package/dist/cjs/client.js
CHANGED
|
@@ -21,8 +21,24 @@ function buildPath(path, query) {
|
|
|
21
21
|
const qs = params.toString();
|
|
22
22
|
return qs ? `${path}?${qs}` : path;
|
|
23
23
|
}
|
|
24
|
+
const FAILOVER_STATUS = new Set([502, 503, 504]);
|
|
25
|
+
/** Errors meaning no connection was made, so the request was never sent. */
|
|
26
|
+
const CONNECT_FAILURE_CODES = new Set([
|
|
27
|
+
'ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', 'ENETUNREACH', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT',
|
|
28
|
+
]);
|
|
29
|
+
function isConnectFailure(err) {
|
|
30
|
+
const cause = err?.cause;
|
|
31
|
+
if (!cause)
|
|
32
|
+
return false;
|
|
33
|
+
if (typeof cause.code === 'string' && CONNECT_FAILURE_CODES.has(cause.code))
|
|
34
|
+
return true;
|
|
35
|
+
return Array.isArray(cause.errors) && cause.errors.length > 0
|
|
36
|
+
&& cause.errors.every(e => typeof e?.code === 'string' && CONNECT_FAILURE_CODES.has(e.code));
|
|
37
|
+
}
|
|
24
38
|
class HttpTransport {
|
|
25
|
-
|
|
39
|
+
/** Every configured NA endpoint, in preference order. */
|
|
40
|
+
baseUrls;
|
|
41
|
+
active = 0;
|
|
26
42
|
/** Key ID sent in X-Admin-Key-Id. */
|
|
27
43
|
keyId;
|
|
28
44
|
signer;
|
|
@@ -31,7 +47,14 @@ class HttpTransport {
|
|
|
31
47
|
headers;
|
|
32
48
|
_fetch;
|
|
33
49
|
constructor(options) {
|
|
34
|
-
|
|
50
|
+
const urls = options.baseUrls ?? (options.baseUrl !== undefined ? [options.baseUrl] : []);
|
|
51
|
+
if (urls.length === 0 || urls.some(u => typeof u !== 'string' || !/^https?:\/\//.test(u))) {
|
|
52
|
+
throw new Error('baseUrl or baseUrls must give at least one http(s) URL');
|
|
53
|
+
}
|
|
54
|
+
if (options.baseUrl !== undefined && options.baseUrls !== undefined) {
|
|
55
|
+
throw new Error('pass either baseUrl or baseUrls, not both');
|
|
56
|
+
}
|
|
57
|
+
this.baseUrls = Object.freeze(urls.map(u => u.replace(/\/$/, '')));
|
|
35
58
|
this.signer = options.signer
|
|
36
59
|
?? (options.signingKeyBase64
|
|
37
60
|
? (0, auth_js_1.seedSigner)(options.signingKeyBase64, options.keyId ?? 'operator-local')
|
|
@@ -46,6 +69,15 @@ class HttpTransport {
|
|
|
46
69
|
this.headers = options.headers ?? {};
|
|
47
70
|
this._fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
48
71
|
}
|
|
72
|
+
/** The endpoint requests currently go to. */
|
|
73
|
+
get baseUrl() {
|
|
74
|
+
return this.baseUrls[this.active];
|
|
75
|
+
}
|
|
76
|
+
/** Move to the next endpoint (no-op with one endpoint). */
|
|
77
|
+
failover() {
|
|
78
|
+
if (this.baseUrls.length > 1)
|
|
79
|
+
this.active = (this.active + 1) % this.baseUrls.length;
|
|
80
|
+
}
|
|
49
81
|
/** Signed admin POST. Not retried unless `idempotent` is set. */
|
|
50
82
|
async adminPost(path, body, idempotent = false) {
|
|
51
83
|
const response = await this._send({ method: 'POST', path, body, admin: true, idempotent });
|
|
@@ -73,30 +105,76 @@ class HttpTransport {
|
|
|
73
105
|
return this._parse(response);
|
|
74
106
|
}
|
|
75
107
|
async _send(spec) {
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
// Idempotent requests may try every other endpoint once, plus the
|
|
109
|
+
// configured retries (with backoff). Non-idempotent requests move to
|
|
110
|
+
// another endpoint only when the connection was never established; after
|
|
111
|
+
// any other failure they are not replayed, and the next request starts at
|
|
112
|
+
// another endpoint.
|
|
113
|
+
const failovers = this.baseUrls.length - 1;
|
|
114
|
+
const retries = spec.idempotent ? this.retry.attempts : 0;
|
|
115
|
+
let failoversLeft = failovers;
|
|
116
|
+
let retriesUsed = 0;
|
|
117
|
+
for (;;) {
|
|
118
|
+
let response;
|
|
119
|
+
let error;
|
|
78
120
|
try {
|
|
79
|
-
|
|
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 };
|
|
121
|
+
response = await this._attempt(spec);
|
|
92
122
|
}
|
|
93
123
|
catch (err) {
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
if (!(err instanceof errors_js_1.NetworkError))
|
|
125
|
+
throw err;
|
|
126
|
+
error = err;
|
|
127
|
+
}
|
|
128
|
+
const failoverWorthy = error !== undefined || (response !== undefined && FAILOVER_STATUS.has(response.status));
|
|
129
|
+
if (failoverWorthy)
|
|
130
|
+
this.failover();
|
|
131
|
+
// A request that may have reached an instance is replayed elsewhere only
|
|
132
|
+
// if it is idempotent; one that never connected can always move on.
|
|
133
|
+
const mayMove = spec.idempotent || (error instanceof errors_js_1.NetworkError && error.connectFailed);
|
|
134
|
+
if (failoverWorthy && mayMove && failoversLeft > 0) {
|
|
135
|
+
failoversLeft -= 1;
|
|
136
|
+
continue;
|
|
99
137
|
}
|
|
138
|
+
const retryable = error !== undefined || (response !== undefined && RETRYABLE_STATUS.has(response.status));
|
|
139
|
+
if (retryable && retriesUsed < retries) {
|
|
140
|
+
await this._backoff(retriesUsed);
|
|
141
|
+
retriesUsed += 1;
|
|
142
|
+
failoversLeft = failovers;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (error !== undefined)
|
|
146
|
+
throw error;
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** One request to the active endpoint, with the body fully read (a dropped body is a NetworkError). */
|
|
151
|
+
async _attempt(spec) {
|
|
152
|
+
const res = await this._once(spec);
|
|
153
|
+
let body;
|
|
154
|
+
try {
|
|
155
|
+
body = await res.text();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw new errors_js_1.NetworkError(`Failed to read response body (${spec.method} ${spec.path})`);
|
|
159
|
+
}
|
|
160
|
+
return { ok: res.ok, status: res.status, text: async () => body };
|
|
161
|
+
}
|
|
162
|
+
/** Unauthenticated GET against one specific endpoint (no failover), e.g. per-instance readiness. */
|
|
163
|
+
async publicGetAt(baseUrl, path) {
|
|
164
|
+
const url = baseUrl.replace(/\/$/, '') + path;
|
|
165
|
+
let response;
|
|
166
|
+
try {
|
|
167
|
+
response = await this._fetch(url, { method: 'GET', headers: { ...this.headers }, signal: AbortSignal.timeout(this.timeout) });
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
throw new errors_js_1.NetworkError(`GET ${url} failed: ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
const text = await response.text();
|
|
173
|
+
try {
|
|
174
|
+
return { status: response.status, body: (0, auth_js_1.parseJson)(text) };
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new errors_js_1.NetworkError(`Failed to parse response body from ${url} (HTTP ${response.status})`);
|
|
100
178
|
}
|
|
101
179
|
}
|
|
102
180
|
async _once(spec) {
|
|
@@ -117,7 +195,9 @@ class HttpTransport {
|
|
|
117
195
|
return await this._fetch(this.baseUrl + path, init);
|
|
118
196
|
}
|
|
119
197
|
catch (err) {
|
|
120
|
-
|
|
198
|
+
const error = new errors_js_1.NetworkError(`${spec.method} ${spec.path} failed: ${err.message}`);
|
|
199
|
+
error.connectFailed = isConnectFailure(err);
|
|
200
|
+
throw error;
|
|
121
201
|
}
|
|
122
202
|
}
|
|
123
203
|
_backoff(attempt) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent-side data access intents (v0.61): build and sign a DataAccessIntent
|
|
4
|
+
* exactly as the Python reference `create_data_access_intent` does, so the NA
|
|
5
|
+
* and every SDK verify it.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createDataAccessIntent = createDataAccessIntent;
|
|
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
|
+
/**
|
|
13
|
+
* Build and sign a DataAccessIntent with the agent's signer. The reference
|
|
14
|
+
* implementation uses the agent sovereign id as the signature key id, so pass
|
|
15
|
+
* a signer whose `keyId` is the agent sovereign id for byte-identical output.
|
|
16
|
+
*/
|
|
17
|
+
async function createDataAccessIntent(params, signer) {
|
|
18
|
+
const validFor = params.valid_for_seconds ?? 300;
|
|
19
|
+
if (!Number.isSafeInteger(validFor) || validFor <= 0)
|
|
20
|
+
throw new Error('valid_for_seconds must be a positive integer');
|
|
21
|
+
const now = params.now ?? new Date();
|
|
22
|
+
const intent = {
|
|
23
|
+
intent_id: params.intent_id ?? (0, node_crypto_1.randomUUID)(),
|
|
24
|
+
agent_sovereign_id: params.agent_sovereign_id,
|
|
25
|
+
decision_id: params.decision_id,
|
|
26
|
+
declared_sources: params.sources.map(s => ({ ...s, classification_tags: [...(s.classification_tags ?? [])] })),
|
|
27
|
+
declared_access_types: [...params.access_types],
|
|
28
|
+
estimated_volume_bytes: params.estimated_volume_bytes ?? null,
|
|
29
|
+
declared_at: (0, auth_js_1.pythonTimestamp)(now),
|
|
30
|
+
expires_at: (0, auth_js_1.pythonTimestamp)(new Date(now.getTime() + validFor * 1000)),
|
|
31
|
+
signature: null,
|
|
32
|
+
};
|
|
33
|
+
intent.signature = await (0, auth_js_1.signCanonical)((0, canonical_js_1.dataAccessIntentCanonical)(intent), signer);
|
|
34
|
+
return intent;
|
|
35
|
+
}
|
package/dist/cjs/errors.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
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
|
+
exports.isRetryableConflict = isRetryableConflict;
|
|
6
7
|
class GenesisMeshError extends Error {
|
|
7
8
|
code;
|
|
8
9
|
status;
|
|
@@ -70,6 +71,12 @@ class RateLimitError extends GenesisMeshError {
|
|
|
70
71
|
}
|
|
71
72
|
exports.RateLimitError = RateLimitError;
|
|
72
73
|
class NetworkError extends GenesisMeshError {
|
|
74
|
+
/**
|
|
75
|
+
* True when the connection could not be established (refused, unresolvable,
|
|
76
|
+
* unreachable): the request never reached the NA, so another instance may
|
|
77
|
+
* take it even when it is not idempotent (v0.60).
|
|
78
|
+
*/
|
|
79
|
+
connectFailed = false;
|
|
73
80
|
constructor(message, code = 'network_error') {
|
|
74
81
|
super(message, code, 0);
|
|
75
82
|
this.name = 'NetworkError';
|
|
@@ -127,3 +134,16 @@ function errorForStatus(status, message, code) {
|
|
|
127
134
|
default: return new GenesisMeshError(message, code, status);
|
|
128
135
|
}
|
|
129
136
|
}
|
|
137
|
+
const HA_CONFLICT_CODES = new Set([
|
|
138
|
+
'boundary_policy_activation_conflict',
|
|
139
|
+
'boundary_policy_version_conflict',
|
|
140
|
+
'crl_publish_contention',
|
|
141
|
+
'retention_in_progress',
|
|
142
|
+
]);
|
|
143
|
+
/**
|
|
144
|
+
* True for a 409 that only means another NA instance won a race the database
|
|
145
|
+
* decided (v0.60). The request changed nothing and can be retried.
|
|
146
|
+
*/
|
|
147
|
+
function isRetryableConflict(err) {
|
|
148
|
+
return err instanceof ConflictError && HA_CONFLICT_CODES.has(err.code);
|
|
149
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealthClient = void 0;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
function normalise(body, ready) {
|
|
6
|
+
return { ...body, status: ready ? 'ready' : 'not_ready', ready };
|
|
7
|
+
}
|
|
8
|
+
/** Liveness, readiness and health of the Network Authority (v0.60). */
|
|
9
|
+
class HealthClient {
|
|
10
|
+
http;
|
|
11
|
+
constructor(http) {
|
|
12
|
+
this.http = http;
|
|
13
|
+
}
|
|
14
|
+
/** Process liveness only (`GET /healthz`). */
|
|
15
|
+
liveness() {
|
|
16
|
+
return this.http.publicGet('/healthz');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Readiness (`GET /readyz`): database writable at the expected schema, key
|
|
20
|
+
* loaded, shared state in HA mode. A not-ready NA (503 `service_not_ready`)
|
|
21
|
+
* is returned as `ready: false` with the failing checks, not thrown. With
|
|
22
|
+
* several `baseUrls`, a not-ready instance is skipped for the next one.
|
|
23
|
+
*/
|
|
24
|
+
async readiness() {
|
|
25
|
+
try {
|
|
26
|
+
return normalise(await this.http.publicGet('/readyz'), true);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err instanceof errors_js_1.ServiceUnavailableError && err.code === 'service_not_ready') {
|
|
30
|
+
return normalise(err.details, false);
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Network, version, boundary policy health and evidence store mode (`GET /health`). */
|
|
36
|
+
health() {
|
|
37
|
+
return this.http.publicGet('/health');
|
|
38
|
+
}
|
|
39
|
+
/** Probe every configured endpoint's `/readyz` directly, for monitoring an HA deployment. */
|
|
40
|
+
async endpoints() {
|
|
41
|
+
return Promise.all(this.http.baseUrls.map(async (baseUrl) => {
|
|
42
|
+
try {
|
|
43
|
+
const { status, body } = await this.http.publicGetAt(baseUrl, '/readyz');
|
|
44
|
+
if (status === 200)
|
|
45
|
+
return { base_url: baseUrl, reachable: true, ready: true, readiness: normalise(body, true) };
|
|
46
|
+
const details = body?.['error']?.details;
|
|
47
|
+
return {
|
|
48
|
+
base_url: baseUrl, reachable: true, ready: false,
|
|
49
|
+
...(details ? { readiness: normalise(details, false) } : {}),
|
|
50
|
+
error: `HTTP ${status}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return { base_url: baseUrl, reachable: false, ready: false, error: err.message };
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.HealthClient = HealthClient;
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.BadRequestError = exports.NetworkError = exports.ServiceUnavailableError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.ValidationError = exports.ForbiddenError = exports.UnauthorizedError = exports.GenesisMeshError = exports.DecisionVerificationError = exports.GovernedActionError = exports.reconcileResources = exports.summarizeDecision = exports.governedAction = exports.parseTimestampMicros = exports.parseExportLines = exports.verifyEvidenceEvents = exports.verifyExecutionSignature = void 0;
|
|
3
|
+
exports.verifyAttestationSignature = exports.verifyDecisionSignature = exports.verifyBoundaryDecision = exports.MAX_METADATA_BYTES = exports.SecretMaterialError = exports.checkMetadataOnly = exports.ExecutionRecorder = exports.payloadDigest = exports.entryDigest = exports.checkpointCanonical = exports.justificationCanonical = exports.dataAccessIntentCanonical = exports.dataLicensePolicyCanonical = exports.agreementCanonical = exports.policySetDigest = exports.policyDigest = exports.policyCanonical = exports.revocationFeedCanonical = exports.attestationDigest = exports.attestationCanonical = exports.executionDigest = exports.executionCanonical = exports.decisionCanonical = exports.RESOURCE_CHAIN_FIELDS = exports.buildAdminHeadersWithSigner = exports.buildAdminHeaders = exports.pythonTimestamp = exports.verifyCanonical = exports.signCanonical = exports.seedSigner = exports.publicKeyFromSeed = exports.verifyBytes = exports.signBytes = exports.sha256Hex = exports.parseJson = exports.canonicalDigest = exports.canonicalJson = exports.buildPath = exports.HttpTransport = exports.DataUsageClient = exports.ConsensusClient = exports.DisclosureClient = exports.AttestationClient = exports.HealthClient = exports.EvidenceStoreClient = exports.EvidenceClient = exports.PolicyClient = exports.BoundaryClient = exports.AgreementClient = exports.GenesisMeshClient = void 0;
|
|
4
|
+
exports.BadRequestError = exports.NetworkError = exports.isRetryableConflict = exports.ServiceUnavailableError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.ValidationError = exports.ForbiddenError = exports.UnauthorizedError = exports.GenesisMeshError = exports.DecisionVerificationError = exports.GovernedActionError = exports.reconcileResources = exports.summarizeDecision = exports.governedAction = exports.createDataAccessIntent = exports.verifyDataAccessIntent = exports.verifyDataLicensePolicySignature = exports.verifyAgreement = exports.parseTimestampMicros = exports.parseExportLines = exports.verifyEvidenceEvents = exports.verifyExecutionSignature = exports.verifyRetentionCheckpoint = exports.verifyRevocationFeedSignature = exports.verifyJustificationSignature = exports.verifyPolicySignature = void 0;
|
|
5
5
|
const client_js_1 = require("./client.js");
|
|
6
6
|
const agreement_js_1 = require("./agreement.js");
|
|
7
7
|
const boundary_js_1 = require("./boundary.js");
|
|
@@ -12,6 +12,7 @@ const consensus_js_1 = require("./consensus.js");
|
|
|
12
12
|
const data_usage_js_1 = require("./data_usage.js");
|
|
13
13
|
const policy_js_1 = require("./policy.js");
|
|
14
14
|
const evidence_store_js_1 = require("./evidence_store.js");
|
|
15
|
+
const health_js_1 = require("./health.js");
|
|
15
16
|
class GenesisMeshClient {
|
|
16
17
|
agreement;
|
|
17
18
|
boundary;
|
|
@@ -23,6 +24,8 @@ class GenesisMeshClient {
|
|
|
23
24
|
disclosure;
|
|
24
25
|
consensus;
|
|
25
26
|
dataUsage;
|
|
27
|
+
/** Liveness, readiness and health (v0.60). */
|
|
28
|
+
health;
|
|
26
29
|
constructor(options) {
|
|
27
30
|
const http = new client_js_1.HttpTransport(options);
|
|
28
31
|
this.agreement = new agreement_js_1.AgreementClient(http);
|
|
@@ -30,6 +33,7 @@ class GenesisMeshClient {
|
|
|
30
33
|
this.policy = new policy_js_1.PolicyClient(http);
|
|
31
34
|
this.evidence = new evidence_js_1.EvidenceClient(http);
|
|
32
35
|
this.evidenceStore = new evidence_store_js_1.EvidenceStoreClient(http);
|
|
36
|
+
this.health = new health_js_1.HealthClient(http);
|
|
33
37
|
this.attestation = new attestation_js_1.AttestationClient(http);
|
|
34
38
|
this.disclosure = new disclosure_js_1.DisclosureClient(http);
|
|
35
39
|
this.consensus = new consensus_js_1.ConsensusClient(http);
|
|
@@ -48,6 +52,8 @@ var evidence_js_2 = require("./evidence.js");
|
|
|
48
52
|
Object.defineProperty(exports, "EvidenceClient", { enumerable: true, get: function () { return evidence_js_2.EvidenceClient; } });
|
|
49
53
|
var evidence_store_js_2 = require("./evidence_store.js");
|
|
50
54
|
Object.defineProperty(exports, "EvidenceStoreClient", { enumerable: true, get: function () { return evidence_store_js_2.EvidenceStoreClient; } });
|
|
55
|
+
var health_js_2 = require("./health.js");
|
|
56
|
+
Object.defineProperty(exports, "HealthClient", { enumerable: true, get: function () { return health_js_2.HealthClient; } });
|
|
51
57
|
var attestation_js_2 = require("./attestation.js");
|
|
52
58
|
Object.defineProperty(exports, "AttestationClient", { enumerable: true, get: function () { return attestation_js_2.AttestationClient; } });
|
|
53
59
|
var disclosure_js_2 = require("./disclosure.js");
|
|
@@ -87,6 +93,9 @@ Object.defineProperty(exports, "revocationFeedCanonical", { enumerable: true, ge
|
|
|
87
93
|
Object.defineProperty(exports, "policyCanonical", { enumerable: true, get: function () { return canonical_js_1.policyCanonical; } });
|
|
88
94
|
Object.defineProperty(exports, "policyDigest", { enumerable: true, get: function () { return canonical_js_1.policyDigest; } });
|
|
89
95
|
Object.defineProperty(exports, "policySetDigest", { enumerable: true, get: function () { return canonical_js_1.policySetDigest; } });
|
|
96
|
+
Object.defineProperty(exports, "agreementCanonical", { enumerable: true, get: function () { return canonical_js_1.agreementCanonical; } });
|
|
97
|
+
Object.defineProperty(exports, "dataLicensePolicyCanonical", { enumerable: true, get: function () { return canonical_js_1.dataLicensePolicyCanonical; } });
|
|
98
|
+
Object.defineProperty(exports, "dataAccessIntentCanonical", { enumerable: true, get: function () { return canonical_js_1.dataAccessIntentCanonical; } });
|
|
90
99
|
Object.defineProperty(exports, "justificationCanonical", { enumerable: true, get: function () { return canonical_js_1.justificationCanonical; } });
|
|
91
100
|
Object.defineProperty(exports, "checkpointCanonical", { enumerable: true, get: function () { return canonical_js_1.checkpointCanonical; } });
|
|
92
101
|
Object.defineProperty(exports, "entryDigest", { enumerable: true, get: function () { return canonical_js_1.entryDigest; } });
|
|
@@ -110,6 +119,12 @@ Object.defineProperty(exports, "verifyExecutionSignature", { enumerable: true, g
|
|
|
110
119
|
Object.defineProperty(exports, "verifyEvidenceEvents", { enumerable: true, get: function () { return verify_js_1.verifyEvidenceEvents; } });
|
|
111
120
|
Object.defineProperty(exports, "parseExportLines", { enumerable: true, get: function () { return verify_js_1.parseExportLines; } });
|
|
112
121
|
Object.defineProperty(exports, "parseTimestampMicros", { enumerable: true, get: function () { return verify_js_1.parseTimestampMicros; } });
|
|
122
|
+
Object.defineProperty(exports, "verifyAgreement", { enumerable: true, get: function () { return verify_js_1.verifyAgreement; } });
|
|
123
|
+
Object.defineProperty(exports, "verifyDataLicensePolicySignature", { enumerable: true, get: function () { return verify_js_1.verifyDataLicensePolicySignature; } });
|
|
124
|
+
Object.defineProperty(exports, "verifyDataAccessIntent", { enumerable: true, get: function () { return verify_js_1.verifyDataAccessIntent; } });
|
|
125
|
+
// Data access intents (v0.61)
|
|
126
|
+
var data_intent_js_1 = require("./data_intent.js");
|
|
127
|
+
Object.defineProperty(exports, "createDataAccessIntent", { enumerable: true, get: function () { return data_intent_js_1.createDataAccessIntent; } });
|
|
113
128
|
// Governed lifecycles
|
|
114
129
|
var governance_js_1 = require("./governance.js");
|
|
115
130
|
Object.defineProperty(exports, "governedAction", { enumerable: true, get: function () { return governance_js_1.governedAction; } });
|
|
@@ -127,5 +142,6 @@ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: functio
|
|
|
127
142
|
Object.defineProperty(exports, "ConflictError", { enumerable: true, get: function () { return errors_js_1.ConflictError; } });
|
|
128
143
|
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_js_1.RateLimitError; } });
|
|
129
144
|
Object.defineProperty(exports, "ServiceUnavailableError", { enumerable: true, get: function () { return errors_js_1.ServiceUnavailableError; } });
|
|
145
|
+
Object.defineProperty(exports, "isRetryableConflict", { enumerable: true, get: function () { return errors_js_1.isRetryableConflict; } });
|
|
130
146
|
Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return errors_js_1.NetworkError; } });
|
|
131
147
|
Object.defineProperty(exports, "BadRequestError", { enumerable: true, get: function () { return errors_js_1.BadRequestError; } });
|
package/dist/cjs/verify.js
CHANGED
|
@@ -11,6 +11,9 @@ exports.verifyExecutionSignature = verifyExecutionSignature;
|
|
|
11
11
|
exports.verifyBoundaryDecision = verifyBoundaryDecision;
|
|
12
12
|
exports.parseExportLines = parseExportLines;
|
|
13
13
|
exports.verifyEvidenceEvents = verifyEvidenceEvents;
|
|
14
|
+
exports.verifyAgreement = verifyAgreement;
|
|
15
|
+
exports.verifyDataLicensePolicySignature = verifyDataLicensePolicySignature;
|
|
16
|
+
exports.verifyDataAccessIntent = verifyDataAccessIntent;
|
|
14
17
|
const validation_js_1 = require("./validation.js");
|
|
15
18
|
/**
|
|
16
19
|
* Offline verification of NA-signed artifacts and evidence exports.
|
|
@@ -297,3 +300,71 @@ function verifyEvidenceEvents(events, options) {
|
|
|
297
300
|
}
|
|
298
301
|
return result;
|
|
299
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Verify an AgreementRecord offline: at least one signature by an offerer key
|
|
305
|
+
* and one by a responder key over the shared canonical body, and the graph
|
|
306
|
+
* digest when `expectedGraphDigest` is given. Port of `verify_agreement`.
|
|
307
|
+
*/
|
|
308
|
+
function verifyAgreement(record, offererPublicKeys, responderPublicKeys, expectedGraphDigest) {
|
|
309
|
+
const result = (accepted, reason) => ({ accepted, reason, agreement_id: record.agreement_id });
|
|
310
|
+
const signatures = record.signatures ?? [];
|
|
311
|
+
if (signatures.length === 0)
|
|
312
|
+
return result(false, 'missing_offerer_signature');
|
|
313
|
+
const canonical = (0, canonical_js_1.agreementCanonical)(record);
|
|
314
|
+
if (!anySigned(canonical, signatures, offererPublicKeys))
|
|
315
|
+
return result(false, 'invalid_offerer_signature');
|
|
316
|
+
if (!anySigned(canonical, signatures, responderPublicKeys)) {
|
|
317
|
+
return result(false, signatures.length < 2 ? 'missing_responder_signature' : 'invalid_responder_signature');
|
|
318
|
+
}
|
|
319
|
+
if (expectedGraphDigest !== undefined && record.graph_digest !== expectedGraphDigest) {
|
|
320
|
+
return result(false, 'graph_digest_mismatch');
|
|
321
|
+
}
|
|
322
|
+
return result(true, 'accepted');
|
|
323
|
+
}
|
|
324
|
+
// ── Data usage (v0.61) ────────────────────────────────────────────────────────
|
|
325
|
+
/** True when the licensor signed the DataLicensePolicy. */
|
|
326
|
+
function verifyDataLicensePolicySignature(policy, licensorPublicKeys) {
|
|
327
|
+
return signedBy((0, canonical_js_1.dataLicensePolicyCanonical)(policy), policy.signature, licensorPublicKeys);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check an agent-signed DataAccessIntent against a DataLicensePolicy offline:
|
|
331
|
+
* the agent signature, then expiry, licensed sources, prohibited
|
|
332
|
+
* classifications, permitted access types and the volume cap, in the order of
|
|
333
|
+
* the reference `verify_data_access_intent`.
|
|
334
|
+
*/
|
|
335
|
+
function verifyDataAccessIntent(intent, policy, agentPublicKeys, at = new Date()) {
|
|
336
|
+
const fail = (violations) => ({ valid: false, violation_reason: violations[0].violation_type, violations });
|
|
337
|
+
if (!intent.signature)
|
|
338
|
+
return fail([{ violation_type: 'intent_exceeds_license', detail: 'Missing intent signature' }]);
|
|
339
|
+
if (!signedBy((0, canonical_js_1.dataAccessIntentCanonical)(intent), intent.signature, agentPublicKeys)) {
|
|
340
|
+
return fail([{ violation_type: 'intent_exceeds_license', detail: 'Invalid intent signature' }]);
|
|
341
|
+
}
|
|
342
|
+
const now = micros(at);
|
|
343
|
+
const violations = [];
|
|
344
|
+
const add = (violation_type, detail) => violations.push({ violation_type, detail });
|
|
345
|
+
if (now > micros(intent.expires_at))
|
|
346
|
+
add('intent_expired', `Intent expired at ${intent.expires_at}`);
|
|
347
|
+
if (now > micros(policy.valid_until) || now < micros(policy.valid_from))
|
|
348
|
+
add('policy_expired', 'Policy not valid at verification time');
|
|
349
|
+
const allowed = new Set(policy.allowed_source_ids);
|
|
350
|
+
const prohibited = new Set(policy.prohibited_classification_tags);
|
|
351
|
+
for (const source of intent.declared_sources) {
|
|
352
|
+
if (!allowed.has(source.source_id))
|
|
353
|
+
add('source_not_licensed', `Source '${source.source_id}' not in allowed_source_ids`);
|
|
354
|
+
const overlap = source.classification_tags.filter(t => prohibited.has(t)).sort(auth_js_1.compareCodePoints);
|
|
355
|
+
if (overlap.length > 0) {
|
|
356
|
+
add('prohibited_classification', `Source '${source.source_id}' has prohibited tags: ${overlap.join(', ')}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const allowedTypes = new Set(policy.allowed_access_types.length > 0 ? policy.allowed_access_types : ['read']);
|
|
360
|
+
for (const accessType of intent.declared_access_types) {
|
|
361
|
+
if (!allowedTypes.has(accessType))
|
|
362
|
+
add('access_type_not_permitted', `Access type '${accessType}' not in allowed_access_types`);
|
|
363
|
+
}
|
|
364
|
+
const cap = policy.max_volume_bytes_per_session;
|
|
365
|
+
const volume = intent.estimated_volume_bytes;
|
|
366
|
+
if (cap !== null && cap !== undefined && volume !== null && volume !== undefined && volume > cap) {
|
|
367
|
+
add('volume_cap_exceeded', `Volume ${volume} > max ${cap}`);
|
|
368
|
+
}
|
|
369
|
+
return violations.length > 0 ? fail(violations) : { valid: true, violation_reason: null, violations: [] };
|
|
370
|
+
}
|
package/dist/esm/auth.js
CHANGED
|
@@ -58,12 +58,17 @@ function pythonNumber(value, pythonFloat = false) {
|
|
|
58
58
|
*/
|
|
59
59
|
const PYTHON_FLOATS = new WeakMap();
|
|
60
60
|
/**
|
|
61
|
-
* JSON.parse that
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* JSON.parse that keeps what Python would sign: integral-valued floats are
|
|
62
|
+
* remembered so canonicalJson reproduces them (`90.0`), and integer literals
|
|
63
|
+
* beyond Number.MAX_SAFE_INTEGER become `bigint` so no digit is lost. Use it
|
|
64
|
+
* for any NA JSON that will be verified or digested. Requires JSON.parse source
|
|
65
|
+
* text access (Node.js 22+).
|
|
64
66
|
*/
|
|
65
67
|
export function parseJson(text) {
|
|
66
68
|
return JSON.parse(text, function (key, value, context) {
|
|
69
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value) && context?.source && /^-?\d+$/.test(context.source)) {
|
|
70
|
+
return BigInt(context.source);
|
|
71
|
+
}
|
|
67
72
|
if (typeof value === 'number' && Number.isInteger(value) && context?.source && /[.eE]/.test(context.source)) {
|
|
68
73
|
let keys = PYTHON_FLOATS.get(this);
|
|
69
74
|
if (!keys)
|
|
@@ -96,6 +101,8 @@ export function canonicalJson(value) {
|
|
|
96
101
|
return asciiString(value);
|
|
97
102
|
if (typeof value === 'number')
|
|
98
103
|
return pythonNumber(value);
|
|
104
|
+
if (typeof value === 'bigint')
|
|
105
|
+
return value.toString();
|
|
99
106
|
if (typeof value === 'boolean')
|
|
100
107
|
return value ? 'true' : 'false';
|
|
101
108
|
if (Array.isArray(value))
|