@agentplat/mesh 0.3.0-beta.2 → 0.3.0-beta.4
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/README.md +133 -4
- package/dist/adaptive-overlay-contracts.d.ts +149 -0
- package/dist/adaptive-overlay-contracts.d.ts.map +1 -0
- package/dist/adaptive-overlay-contracts.js +3 -0
- package/dist/adaptive-overlay-contracts.js.map +1 -0
- package/dist/adaptive-overlay-runtime.d.ts +67 -0
- package/dist/adaptive-overlay-runtime.d.ts.map +1 -0
- package/dist/adaptive-overlay-runtime.js +427 -0
- package/dist/adaptive-overlay-runtime.js.map +1 -0
- package/dist/adaptive-overlay-validation.d.ts +19 -0
- package/dist/adaptive-overlay-validation.d.ts.map +1 -0
- package/dist/adaptive-overlay-validation.js +326 -0
- package/dist/adaptive-overlay-validation.js.map +1 -0
- package/dist/adaptive-overlay.d.ts +4 -0
- package/dist/adaptive-overlay.d.ts.map +1 -0
- package/dist/adaptive-overlay.js +4 -0
- package/dist/adaptive-overlay.js.map +1 -0
- package/dist/authenticated-overlay-transport.d.ts +227 -0
- package/dist/authenticated-overlay-transport.d.ts.map +1 -0
- package/dist/authenticated-overlay-transport.js +702 -0
- package/dist/authenticated-overlay-transport.js.map +1 -0
- package/dist/authority-continuity-contracts.d.ts +249 -0
- package/dist/authority-continuity-contracts.d.ts.map +1 -0
- package/dist/authority-continuity-contracts.js +11 -0
- package/dist/authority-continuity-contracts.js.map +1 -0
- package/dist/authority-continuity.d.ts +43 -0
- package/dist/authority-continuity.d.ts.map +1 -0
- package/dist/authority-continuity.js +1338 -0
- package/dist/authority-continuity.js.map +1 -0
- package/dist/continuity.d.ts +3 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +3 -0
- package/dist/continuity.js.map +1 -0
- package/dist/coordination-allocation-contracts.d.ts +6 -0
- package/dist/coordination-allocation-contracts.d.ts.map +1 -1
- package/dist/coordination-allocation-state.d.ts.map +1 -1
- package/dist/coordination-allocation-state.js +37 -7
- package/dist/coordination-allocation-state.js.map +1 -1
- package/dist/coordination-allocation.d.ts +1 -1
- package/dist/coordination-allocation.d.ts.map +1 -1
- package/dist/coordination-allocation.js +92 -11
- package/dist/coordination-allocation.js.map +1 -1
- package/dist/coordination-discovery-state.d.ts.map +1 -1
- package/dist/coordination-discovery-state.js +5 -3
- package/dist/coordination-discovery-state.js.map +1 -1
- package/dist/coordination-recovery.js +5 -1
- package/dist/coordination-recovery.js.map +1 -1
- package/dist/durability.d.ts +22 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +22 -0
- package/dist/durability.js.map +1 -1
- package/dist/overlay-transport.d.ts +7 -0
- package/dist/overlay-transport.d.ts.map +1 -0
- package/dist/overlay-transport.js +7 -0
- package/dist/overlay-transport.js.map +1 -0
- package/dist/overlay.d.ts +9 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +9 -0
- package/dist/overlay.js.map +1 -0
- package/dist/sparse-overlay-contracts.d.ts +91 -0
- package/dist/sparse-overlay-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-contracts.js +2 -0
- package/dist/sparse-overlay-contracts.js.map +1 -0
- package/dist/sparse-overlay-durability.d.ts +23 -0
- package/dist/sparse-overlay-durability.d.ts.map +1 -0
- package/dist/sparse-overlay-durability.js +97 -0
- package/dist/sparse-overlay-durability.js.map +1 -0
- package/dist/sparse-overlay-runtime-contracts.d.ts +158 -0
- package/dist/sparse-overlay-runtime-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-runtime-contracts.js +2 -0
- package/dist/sparse-overlay-runtime-contracts.js.map +1 -0
- package/dist/sparse-overlay-runtime.d.ts +48 -0
- package/dist/sparse-overlay-runtime.d.ts.map +1 -0
- package/dist/sparse-overlay-runtime.js +763 -0
- package/dist/sparse-overlay-runtime.js.map +1 -0
- package/dist/sparse-overlay-transport-contracts.d.ts +183 -0
- package/dist/sparse-overlay-transport-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-transport-contracts.js +2 -0
- package/dist/sparse-overlay-transport-contracts.js.map +1 -0
- package/dist/sparse-overlay-transport-durability.d.ts +19 -0
- package/dist/sparse-overlay-transport-durability.d.ts.map +1 -0
- package/dist/sparse-overlay-transport-durability.js +91 -0
- package/dist/sparse-overlay-transport-durability.js.map +1 -0
- package/dist/sparse-overlay-transport.d.ts +97 -0
- package/dist/sparse-overlay-transport.d.ts.map +1 -0
- package/dist/sparse-overlay-transport.js +888 -0
- package/dist/sparse-overlay-transport.js.map +1 -0
- package/dist/sparse-overlay.d.ts +74 -0
- package/dist/sparse-overlay.d.ts.map +1 -0
- package/dist/sparse-overlay.js +817 -0
- package/dist/sparse-overlay.js.map +1 -0
- package/fixtures/beta1/manifest.json +4 -4
- package/fixtures/beta1/states/discovery-v1.json +3 -3
- package/fixtures/beta1/states/discovery-v2.json +3 -3
- package/package.json +20 -4
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { MESH_SIGNATURE_ALGORITHM, canonicalizeMeshJsonBytes, } from "@agentplat/mesh-protocol";
|
|
2
|
+
import { sha256Base64Url } from "./sha256.js";
|
|
3
|
+
/**
|
|
4
|
+
* Authenticated, epoch-bound envelopes for the sparse peer transport.
|
|
5
|
+
*
|
|
6
|
+
* The transport deliberately does not define a directory, PKI or membership
|
|
7
|
+
* protocol. Those decisions are supplied by the authority ports below. The
|
|
8
|
+
* only persisted evidence is public routing metadata, digests and signatures.
|
|
9
|
+
*/
|
|
10
|
+
export const MESH_AUTHENTICATED_OVERLAY_TRANSPORT_SCHEMA_VERSION_V1 = 1;
|
|
11
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
12
|
+
maximumFutureSequence: 4_096,
|
|
13
|
+
maximumFutureLogicalTime: 30_000,
|
|
14
|
+
maximumInboundPeers: 4_096,
|
|
15
|
+
maximumCommitAttempts: 8,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Authenticated next-hop adapter for MeshSparsePeerTransportRuntimeV1.
|
|
19
|
+
* Pass it as that runtime's `nextHop`; the underlying durable overlay remains
|
|
20
|
+
* responsible for queueing, causal recovery and delivery idempotency.
|
|
21
|
+
*/
|
|
22
|
+
export class MeshAuthenticatedSparseOverlayNextHopTransportV1 {
|
|
23
|
+
#runtime;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.#runtime = new AuthenticatedOverlayRuntimeV1(options);
|
|
26
|
+
}
|
|
27
|
+
async deliver(input) {
|
|
28
|
+
const response = await this.#request("deliver", input.recipient, {
|
|
29
|
+
overlayId: input.overlayId,
|
|
30
|
+
membershipDigest: input.membershipDigest,
|
|
31
|
+
sender: input.sender,
|
|
32
|
+
delivery: input.delivery,
|
|
33
|
+
logicalTime: input.logicalTime,
|
|
34
|
+
}, input.logicalTime);
|
|
35
|
+
return admissionFromResponse(response);
|
|
36
|
+
}
|
|
37
|
+
async catchUp(input) {
|
|
38
|
+
const response = await this.#request("catch_up", input.recipient, {
|
|
39
|
+
overlayId: input.overlayId,
|
|
40
|
+
membershipDigest: input.membershipDigest,
|
|
41
|
+
requester: input.requester,
|
|
42
|
+
afterSequence: input.afterSequence,
|
|
43
|
+
maximumItems: input.maximumItems,
|
|
44
|
+
logicalTime: input.logicalTime,
|
|
45
|
+
}, input.logicalTime);
|
|
46
|
+
if (!response.accepted || !isObject(response.body))
|
|
47
|
+
throw new Error("authenticated_overlay_catch_up_rejected");
|
|
48
|
+
return response.body;
|
|
49
|
+
}
|
|
50
|
+
async fetchCausal(input) {
|
|
51
|
+
const response = await this.#request("fetch_causal", input.recipient, {
|
|
52
|
+
overlayId: input.overlayId,
|
|
53
|
+
membershipDigest: input.membershipDigest,
|
|
54
|
+
requester: input.requester,
|
|
55
|
+
deliveryDigest: input.deliveryDigest,
|
|
56
|
+
logicalTime: input.logicalTime,
|
|
57
|
+
}, input.logicalTime);
|
|
58
|
+
if (!response.accepted)
|
|
59
|
+
return undefined;
|
|
60
|
+
return response.body === null
|
|
61
|
+
? undefined
|
|
62
|
+
: response.body;
|
|
63
|
+
}
|
|
64
|
+
async #request(operation, recipient, body, logicalTime) {
|
|
65
|
+
const target = peerIdentity(recipient, "recipient");
|
|
66
|
+
const envelope = await this.#runtime.createRequest(operation, target, body, logicalTime);
|
|
67
|
+
const response = await this.#runtime.network.exchange({
|
|
68
|
+
recipient: target,
|
|
69
|
+
envelope,
|
|
70
|
+
logicalTime,
|
|
71
|
+
});
|
|
72
|
+
await this.#runtime.verifyAndRememberResponse(envelope, response, logicalTime);
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Endpoint adapter. It authenticates before invoking the durable overlay endpoint. */
|
|
77
|
+
export class MeshAuthenticatedSparseOverlayEndpointV1 {
|
|
78
|
+
#runtime;
|
|
79
|
+
#endpoint;
|
|
80
|
+
constructor(options) {
|
|
81
|
+
this.#runtime = new AuthenticatedOverlayRuntimeV1({
|
|
82
|
+
...options,
|
|
83
|
+
network: rejectingNetwork(),
|
|
84
|
+
});
|
|
85
|
+
if (!options.endpoint)
|
|
86
|
+
throw new TypeError("sparse overlay endpoint is required");
|
|
87
|
+
this.#endpoint = options.endpoint;
|
|
88
|
+
}
|
|
89
|
+
async receive(input) {
|
|
90
|
+
let verified;
|
|
91
|
+
try {
|
|
92
|
+
verified = await this.#runtime.verifyAndRememberRequest(input.envelope, input.logicalTime);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return this.#runtime.rejection(input.envelope, rejectionCode(error), input.logicalTime);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const body = await this.#dispatch(verified);
|
|
99
|
+
return this.#runtime.response(verified, true, null, body, input.logicalTime);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return this.#runtime.response(verified, false, "unsupported_operation", null, input.logicalTime);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async #dispatch(envelope) {
|
|
106
|
+
const body = isObject(envelope.body)
|
|
107
|
+
? envelope.body
|
|
108
|
+
: fail("request_binding_invalid");
|
|
109
|
+
assertDispatchBinding(envelope, body);
|
|
110
|
+
if (envelope.operation === "deliver") {
|
|
111
|
+
const input = body;
|
|
112
|
+
return this.#endpoint.receive(input);
|
|
113
|
+
}
|
|
114
|
+
if (envelope.operation === "catch_up") {
|
|
115
|
+
const input = body;
|
|
116
|
+
return this.#endpoint.catchUp(input);
|
|
117
|
+
}
|
|
118
|
+
if (envelope.operation === "fetch_causal") {
|
|
119
|
+
const input = body;
|
|
120
|
+
return (await this.#endpoint.fetchCausal(input)) ?? null;
|
|
121
|
+
}
|
|
122
|
+
return fail("unsupported_operation");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Bounded in-memory reference persistence, suitable only for local development.
|
|
127
|
+
* Its anchor shares process memory with the state and is therefore not an
|
|
128
|
+
* independently protected production anti-rollback witness.
|
|
129
|
+
*/
|
|
130
|
+
export class InMemoryMeshAuthenticatedOverlayStateStoreV1 {
|
|
131
|
+
#snapshots = new Map();
|
|
132
|
+
async loadCurrent(scope) {
|
|
133
|
+
const snapshot = this.#snapshots.get(scope);
|
|
134
|
+
return snapshot ? structuredClone(snapshot) : undefined;
|
|
135
|
+
}
|
|
136
|
+
async compareAndSet(input) {
|
|
137
|
+
const current = this.#snapshots.get(input.scope);
|
|
138
|
+
if ((current?.anchor.revision ?? null) !== (input.expected?.revision ?? null) ||
|
|
139
|
+
(current?.anchor.stateDigest ?? null) !==
|
|
140
|
+
(input.expected?.stateDigest ?? null))
|
|
141
|
+
return false;
|
|
142
|
+
if (input.next.revision !== (current?.state.revision ?? -1) + 1 ||
|
|
143
|
+
input.next.previousStateDigest !== (current?.state.stateDigest ?? null))
|
|
144
|
+
throw new Error("authenticated_overlay_invalid_state_transition");
|
|
145
|
+
this.#snapshots.set(input.scope, structuredClone({
|
|
146
|
+
state: input.next,
|
|
147
|
+
anchor: {
|
|
148
|
+
revision: input.next.revision,
|
|
149
|
+
stateDigest: input.next.stateDigest,
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
snapshot() {
|
|
155
|
+
return Object.freeze([...this.#snapshots.values()].map((snapshot) => snapshot.state));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Explicit local wiring for development; it is a route table, never a directory. */
|
|
159
|
+
export class InMemoryMeshAuthenticatedOverlayNetworkV1 {
|
|
160
|
+
#endpoints = new Map();
|
|
161
|
+
connect(peer, endpoint) {
|
|
162
|
+
const identity = peerIdentity(peer, "peer");
|
|
163
|
+
if (!endpoint)
|
|
164
|
+
throw new TypeError("authenticated overlay endpoint is required");
|
|
165
|
+
this.#endpoints.set(networkKey(identity), endpoint);
|
|
166
|
+
}
|
|
167
|
+
disconnect(peer) {
|
|
168
|
+
this.#endpoints.delete(networkKey(peerIdentity(peer, "peer")));
|
|
169
|
+
}
|
|
170
|
+
async exchange(input) {
|
|
171
|
+
const endpoint = this.#endpoints.get(networkKey(peerIdentity(input.recipient, "recipient")));
|
|
172
|
+
if (!endpoint)
|
|
173
|
+
throw new Error("authenticated_overlay_peer_unavailable");
|
|
174
|
+
return endpoint.receive({
|
|
175
|
+
envelope: input.envelope,
|
|
176
|
+
logicalTime: input.logicalTime,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Opaque Web Crypto signer adapter; callers retain private-key lifecycle control. */
|
|
181
|
+
export class WebCryptoMeshAuthenticatedOverlaySignerV1 {
|
|
182
|
+
keyId;
|
|
183
|
+
privateKey;
|
|
184
|
+
crypto;
|
|
185
|
+
constructor(keyId, privateKey, crypto = globalThis.crypto) {
|
|
186
|
+
this.keyId = keyId;
|
|
187
|
+
this.privateKey = privateKey;
|
|
188
|
+
this.crypto = crypto;
|
|
189
|
+
identifier(keyId, "keyId");
|
|
190
|
+
if (!crypto?.subtle ||
|
|
191
|
+
privateKey.type !== "private" ||
|
|
192
|
+
privateKey.algorithm.name !== MESH_SIGNATURE_ALGORITHM ||
|
|
193
|
+
!privateKey.usages.includes("sign"))
|
|
194
|
+
throw new TypeError("Web Crypto signing key is invalid");
|
|
195
|
+
}
|
|
196
|
+
async sign(input) {
|
|
197
|
+
return new Uint8Array(await this.crypto.subtle.sign(MESH_SIGNATURE_ALGORITHM, this.privateKey, copy(input.bytes)));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
class AuthenticatedOverlayRuntimeV1 {
|
|
201
|
+
network;
|
|
202
|
+
#options;
|
|
203
|
+
#limits;
|
|
204
|
+
#scope;
|
|
205
|
+
constructor(options) {
|
|
206
|
+
if (!options || typeof options !== "object")
|
|
207
|
+
throw new TypeError("authenticated overlay options are required");
|
|
208
|
+
identifier(options.overlayId, "overlayId");
|
|
209
|
+
integer(options.membershipEpoch, "membershipEpoch", 0);
|
|
210
|
+
peerIdentity(options.localPeer, "localPeer");
|
|
211
|
+
identifier(options.signer?.keyId, "signer.keyId");
|
|
212
|
+
if (!options.membership ||
|
|
213
|
+
!options.keys ||
|
|
214
|
+
!options.store ||
|
|
215
|
+
!options.network)
|
|
216
|
+
throw new TypeError("authenticated overlay authority, state and network ports are required");
|
|
217
|
+
if (typeof options.membership.resolve !== "function" ||
|
|
218
|
+
typeof options.keys.resolve !== "function" ||
|
|
219
|
+
typeof options.store.loadCurrent !== "function" ||
|
|
220
|
+
typeof options.store.compareAndSet !== "function" ||
|
|
221
|
+
typeof options.network.exchange !== "function")
|
|
222
|
+
throw new TypeError("authenticated overlay port is invalid");
|
|
223
|
+
this.#options = options;
|
|
224
|
+
this.network = options.network;
|
|
225
|
+
this.#limits = Object.freeze({
|
|
226
|
+
maximumFutureSequence: positive(options.maximumFutureSequence, DEFAULT_LIMITS.maximumFutureSequence, "maximumFutureSequence"),
|
|
227
|
+
maximumFutureLogicalTime: positive(options.maximumFutureLogicalTime, DEFAULT_LIMITS.maximumFutureLogicalTime, "maximumFutureLogicalTime"),
|
|
228
|
+
maximumInboundPeers: positive(options.maximumInboundPeers, DEFAULT_LIMITS.maximumInboundPeers, "maximumInboundPeers"),
|
|
229
|
+
maximumCommitAttempts: positive(options.maximumCommitAttempts, DEFAULT_LIMITS.maximumCommitAttempts, "maximumCommitAttempts"),
|
|
230
|
+
});
|
|
231
|
+
this.#scope = `authenticated-overlay:${options.overlayId}:${options.localPeer.peerId}:${options.membershipEpoch}`;
|
|
232
|
+
}
|
|
233
|
+
async createRequest(operation, recipient, body, logicalTime) {
|
|
234
|
+
await this.#assertLocalSigningKey(logicalTime);
|
|
235
|
+
const sequence = await this.#reserveOutbound(logicalTime);
|
|
236
|
+
const payloadDigest = digest(body);
|
|
237
|
+
const unsigned = {
|
|
238
|
+
schemaVersion: 1,
|
|
239
|
+
kind: "request",
|
|
240
|
+
requestId: `authenticated-overlay:${this.#options.localPeer.peerId}:${sequence}`,
|
|
241
|
+
overlayId: this.#options.overlayId,
|
|
242
|
+
membershipEpoch: this.#options.membershipEpoch,
|
|
243
|
+
sender: this.#options.localPeer,
|
|
244
|
+
recipient,
|
|
245
|
+
sequence,
|
|
246
|
+
logicalTime,
|
|
247
|
+
payloadDigest,
|
|
248
|
+
operation,
|
|
249
|
+
body,
|
|
250
|
+
proof: {
|
|
251
|
+
algorithm: MESH_SIGNATURE_ALGORITHM,
|
|
252
|
+
keyId: this.#options.signer.keyId,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
...unsigned,
|
|
257
|
+
proof: {
|
|
258
|
+
...unsigned.proof,
|
|
259
|
+
value: encode(await this.#options.signer.sign({ bytes: canonical(unsigned) })),
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async verifyAndRememberRequest(envelope, logicalTime) {
|
|
264
|
+
await this.#verifyAndRemember(envelope, logicalTime, "request");
|
|
265
|
+
return envelope;
|
|
266
|
+
}
|
|
267
|
+
async verifyAndRememberResponse(request, response, logicalTime) {
|
|
268
|
+
await this.#verifyEnvelope(response, logicalTime, "response");
|
|
269
|
+
if (response.requestId !== request.requestId ||
|
|
270
|
+
response.operation !== request.operation ||
|
|
271
|
+
response.requestDigest !== digest(request) ||
|
|
272
|
+
!samePeer(response.recipient, request.sender) ||
|
|
273
|
+
!samePeer(response.sender, request.recipient))
|
|
274
|
+
fail("response_binding_invalid");
|
|
275
|
+
// Do not consume a peer's sequence until this response is correlated to
|
|
276
|
+
// the exact authenticated request that triggered it.
|
|
277
|
+
await this.#rememberInbound(response, logicalTime);
|
|
278
|
+
}
|
|
279
|
+
async response(request, accepted, reasonCode, body, logicalTime) {
|
|
280
|
+
await this.#assertLocalSigningKey(logicalTime);
|
|
281
|
+
const sequence = await this.#reserveOutbound(logicalTime);
|
|
282
|
+
const unsigned = {
|
|
283
|
+
schemaVersion: 1,
|
|
284
|
+
kind: "response",
|
|
285
|
+
requestId: request.requestId,
|
|
286
|
+
requestDigest: digest(request),
|
|
287
|
+
operation: request.operation,
|
|
288
|
+
accepted,
|
|
289
|
+
reasonCode,
|
|
290
|
+
body,
|
|
291
|
+
overlayId: this.#options.overlayId,
|
|
292
|
+
membershipEpoch: this.#options.membershipEpoch,
|
|
293
|
+
sender: this.#options.localPeer,
|
|
294
|
+
recipient: request.sender,
|
|
295
|
+
sequence,
|
|
296
|
+
logicalTime,
|
|
297
|
+
payloadDigest: digest(body),
|
|
298
|
+
proof: {
|
|
299
|
+
algorithm: MESH_SIGNATURE_ALGORITHM,
|
|
300
|
+
keyId: this.#options.signer.keyId,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
...unsigned,
|
|
305
|
+
proof: {
|
|
306
|
+
...unsigned.proof,
|
|
307
|
+
value: encode(await this.#options.signer.sign({ bytes: canonical(unsigned) })),
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async rejection(request, reasonCode, logicalTime) {
|
|
312
|
+
return this.response(request, false, reasonCode, null, logicalTime);
|
|
313
|
+
}
|
|
314
|
+
async #verifyAndRemember(envelope, logicalTime, expectedKind) {
|
|
315
|
+
await this.#verifyEnvelope(envelope, logicalTime, expectedKind);
|
|
316
|
+
await this.#rememberInbound(envelope, logicalTime);
|
|
317
|
+
}
|
|
318
|
+
async #verifyEnvelope(envelope, logicalTime, expectedKind) {
|
|
319
|
+
integer(logicalTime, "logicalTime", 0);
|
|
320
|
+
if (!envelope ||
|
|
321
|
+
envelope.kind !== expectedKind ||
|
|
322
|
+
envelope.schemaVersion !== 1)
|
|
323
|
+
fail("request_binding_invalid");
|
|
324
|
+
if (envelope.overlayId !== this.#options.overlayId)
|
|
325
|
+
fail("overlay_mismatch");
|
|
326
|
+
if (envelope.membershipEpoch !== this.#options.membershipEpoch)
|
|
327
|
+
fail("membership_epoch_mismatch");
|
|
328
|
+
if (!samePeer(envelope.recipient, this.#options.localPeer))
|
|
329
|
+
fail("recipient_mismatch");
|
|
330
|
+
peerIdentity(envelope.sender, "sender");
|
|
331
|
+
peerIdentity(envelope.recipient, "recipient");
|
|
332
|
+
integer(envelope.sequence, "sequence", 1);
|
|
333
|
+
integer(envelope.logicalTime, "envelope logicalTime", 0);
|
|
334
|
+
if (envelope.logicalTime >
|
|
335
|
+
logicalTime + this.#limits.maximumFutureLogicalTime)
|
|
336
|
+
fail("logical_time_future");
|
|
337
|
+
const recipientMembership = await this.#options.membership.resolve({
|
|
338
|
+
overlayId: this.#options.overlayId,
|
|
339
|
+
membershipEpoch: this.#options.membershipEpoch,
|
|
340
|
+
...this.#options.localPeer,
|
|
341
|
+
logicalTime,
|
|
342
|
+
});
|
|
343
|
+
if (!recipientMembership?.active)
|
|
344
|
+
fail("recipient_not_active");
|
|
345
|
+
if (!validDigest(envelope.payloadDigest) ||
|
|
346
|
+
digest(envelope.body) !== envelope.payloadDigest)
|
|
347
|
+
fail("payload_digest_invalid");
|
|
348
|
+
if (envelope.proof?.algorithm !== MESH_SIGNATURE_ALGORITHM ||
|
|
349
|
+
!identifier(envelope.proof?.keyId, "proof.keyId") ||
|
|
350
|
+
!base64(envelope.proof?.value))
|
|
351
|
+
fail("signature_invalid");
|
|
352
|
+
await this.#assertRemoteKey(envelope, logicalTime);
|
|
353
|
+
const bytes = canonical({
|
|
354
|
+
...envelope,
|
|
355
|
+
proof: {
|
|
356
|
+
algorithm: envelope.proof.algorithm,
|
|
357
|
+
keyId: envelope.proof.keyId,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
const key = await this.#options.keys.resolve({
|
|
361
|
+
overlayId: envelope.overlayId,
|
|
362
|
+
membershipEpoch: envelope.membershipEpoch,
|
|
363
|
+
peerId: envelope.sender.peerId,
|
|
364
|
+
keyId: envelope.proof.keyId,
|
|
365
|
+
logicalTime,
|
|
366
|
+
});
|
|
367
|
+
if (!key || key.status !== "active")
|
|
368
|
+
fail(key?.status === "revoked" ? "key_revoked" : "key_not_accepted");
|
|
369
|
+
const verified = await crypto(this.#options.crypto).subtle.verify(MESH_SIGNATURE_ALGORITHM, key.publicKey, copy(decode(envelope.proof.value)), copy(bytes));
|
|
370
|
+
if (!verified)
|
|
371
|
+
fail("signature_invalid");
|
|
372
|
+
}
|
|
373
|
+
async #assertLocalSigningKey(logicalTime) {
|
|
374
|
+
const membership = await this.#options.membership.resolve({
|
|
375
|
+
overlayId: this.#options.overlayId,
|
|
376
|
+
membershipEpoch: this.#options.membershipEpoch,
|
|
377
|
+
...this.#options.localPeer,
|
|
378
|
+
logicalTime,
|
|
379
|
+
});
|
|
380
|
+
if (!membership?.active)
|
|
381
|
+
fail("sender_not_active");
|
|
382
|
+
if (membership.currentKeyId !== this.#options.signer.keyId)
|
|
383
|
+
fail("key_not_accepted");
|
|
384
|
+
const key = await this.#options.keys.resolve({
|
|
385
|
+
overlayId: this.#options.overlayId,
|
|
386
|
+
membershipEpoch: this.#options.membershipEpoch,
|
|
387
|
+
peerId: this.#options.localPeer.peerId,
|
|
388
|
+
keyId: this.#options.signer.keyId,
|
|
389
|
+
logicalTime,
|
|
390
|
+
});
|
|
391
|
+
if (!key || key.status === "revoked")
|
|
392
|
+
fail(key?.status === "revoked" ? "key_revoked" : "key_not_accepted");
|
|
393
|
+
if (logicalTime < key.validFromLogicalTime)
|
|
394
|
+
fail("key_not_yet_valid");
|
|
395
|
+
if (logicalTime >= key.validUntilLogicalTime)
|
|
396
|
+
fail("key_expired");
|
|
397
|
+
}
|
|
398
|
+
async #assertRemoteKey(envelope, logicalTime) {
|
|
399
|
+
const membership = await this.#options.membership.resolve({
|
|
400
|
+
overlayId: envelope.overlayId,
|
|
401
|
+
membershipEpoch: envelope.membershipEpoch,
|
|
402
|
+
...envelope.sender,
|
|
403
|
+
logicalTime,
|
|
404
|
+
});
|
|
405
|
+
if (!membership?.active)
|
|
406
|
+
fail("sender_not_active");
|
|
407
|
+
const isCurrent = membership.currentKeyId === envelope.proof.keyId;
|
|
408
|
+
const isPrevious = membership.previousKeyId === envelope.proof.keyId;
|
|
409
|
+
if (!isCurrent && !isPrevious)
|
|
410
|
+
fail("key_not_accepted");
|
|
411
|
+
if (isPrevious &&
|
|
412
|
+
(membership.previousKeyAcceptUntilLogicalTime === undefined ||
|
|
413
|
+
logicalTime > membership.previousKeyAcceptUntilLogicalTime))
|
|
414
|
+
fail("rotation_window_expired");
|
|
415
|
+
const key = await this.#options.keys.resolve({
|
|
416
|
+
overlayId: envelope.overlayId,
|
|
417
|
+
membershipEpoch: envelope.membershipEpoch,
|
|
418
|
+
peerId: envelope.sender.peerId,
|
|
419
|
+
keyId: envelope.proof.keyId,
|
|
420
|
+
logicalTime,
|
|
421
|
+
});
|
|
422
|
+
if (!key || key.status === "revoked")
|
|
423
|
+
fail(key?.status === "revoked" ? "key_revoked" : "key_not_accepted");
|
|
424
|
+
if (logicalTime < key.validFromLogicalTime)
|
|
425
|
+
fail("key_not_yet_valid");
|
|
426
|
+
if (logicalTime >= key.validUntilLogicalTime)
|
|
427
|
+
fail("key_expired");
|
|
428
|
+
}
|
|
429
|
+
async #reserveOutbound(logicalTime) {
|
|
430
|
+
let sequence = 0;
|
|
431
|
+
await this.#mutate(logicalTime, (state) => {
|
|
432
|
+
sequence = state.nextOutboundSequence;
|
|
433
|
+
return stateWith(state, {
|
|
434
|
+
nextOutboundSequence: sequence + 1,
|
|
435
|
+
lastLogicalTime: Math.max(state.lastLogicalTime, logicalTime),
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
return sequence;
|
|
439
|
+
}
|
|
440
|
+
async #rememberInbound(envelope, logicalTime) {
|
|
441
|
+
await this.#mutate(logicalTime, (state) => {
|
|
442
|
+
const current = state.inboundCursors.find((cursor) => cursor.peerId === envelope.sender.peerId &&
|
|
443
|
+
cursor.membershipEpoch === envelope.membershipEpoch);
|
|
444
|
+
const highest = current?.highestSequence ?? 0;
|
|
445
|
+
if (envelope.sequence <= highest)
|
|
446
|
+
fail("sequence_replayed");
|
|
447
|
+
if (envelope.sequence > highest + this.#limits.maximumFutureSequence)
|
|
448
|
+
fail("sequence_future");
|
|
449
|
+
if (!current &&
|
|
450
|
+
state.inboundCursors.length >= this.#limits.maximumInboundPeers)
|
|
451
|
+
fail("state_conflict");
|
|
452
|
+
const replacement = Object.freeze({
|
|
453
|
+
peerId: envelope.sender.peerId,
|
|
454
|
+
membershipEpoch: envelope.membershipEpoch,
|
|
455
|
+
highestSequence: envelope.sequence,
|
|
456
|
+
});
|
|
457
|
+
return stateWith(state, {
|
|
458
|
+
inboundCursors: Object.freeze([
|
|
459
|
+
...state.inboundCursors.filter((cursor) => cursor !== current),
|
|
460
|
+
replacement,
|
|
461
|
+
].sort((left, right) => left.peerId.localeCompare(right.peerId) ||
|
|
462
|
+
left.membershipEpoch - right.membershipEpoch)),
|
|
463
|
+
lastLogicalTime: Math.max(state.lastLogicalTime, logicalTime),
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async #mutate(logicalTime, operation) {
|
|
468
|
+
for (let attempt = 0; attempt < this.#limits.maximumCommitAttempts; attempt += 1) {
|
|
469
|
+
const current = await this.#restore(logicalTime);
|
|
470
|
+
const next = operation(current);
|
|
471
|
+
if (!(await this.#options.store.compareAndSet({
|
|
472
|
+
scope: this.#scope,
|
|
473
|
+
expected: {
|
|
474
|
+
revision: current.revision,
|
|
475
|
+
stateDigest: current.stateDigest,
|
|
476
|
+
},
|
|
477
|
+
next,
|
|
478
|
+
})))
|
|
479
|
+
continue;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
fail("state_conflict");
|
|
483
|
+
}
|
|
484
|
+
async #restore(logicalTime) {
|
|
485
|
+
const snapshot = await this.#options.store.loadCurrent(this.#scope);
|
|
486
|
+
if (snapshot) {
|
|
487
|
+
const validated = validateState(snapshot.state, this.#scope, this.#options);
|
|
488
|
+
if (snapshot.anchor.revision !== validated.revision ||
|
|
489
|
+
snapshot.anchor.stateDigest !== validated.stateDigest)
|
|
490
|
+
fail("state_rollback_detected");
|
|
491
|
+
return validated;
|
|
492
|
+
}
|
|
493
|
+
const initial = stateWith({
|
|
494
|
+
schemaVersion: 1,
|
|
495
|
+
scope: this.#scope,
|
|
496
|
+
overlayId: this.#options.overlayId,
|
|
497
|
+
localPeer: this.#options.localPeer,
|
|
498
|
+
membershipEpoch: this.#options.membershipEpoch,
|
|
499
|
+
revision: -1,
|
|
500
|
+
nextOutboundSequence: 1,
|
|
501
|
+
inboundCursors: [],
|
|
502
|
+
lastLogicalTime: logicalTime,
|
|
503
|
+
previousStateDigest: null,
|
|
504
|
+
stateDigest: "sha256:bootstrap",
|
|
505
|
+
}, {});
|
|
506
|
+
if (await this.#options.store.compareAndSet({
|
|
507
|
+
scope: this.#scope,
|
|
508
|
+
expected: null,
|
|
509
|
+
next: initial,
|
|
510
|
+
}))
|
|
511
|
+
return initial;
|
|
512
|
+
const raced = await this.#options.store.loadCurrent(this.#scope);
|
|
513
|
+
if (!raced)
|
|
514
|
+
fail("state_conflict");
|
|
515
|
+
const validated = validateState(raced.state, this.#scope, this.#options);
|
|
516
|
+
if (raced.anchor.revision !== validated.revision ||
|
|
517
|
+
raced.anchor.stateDigest !== validated.stateDigest)
|
|
518
|
+
fail("state_rollback_detected");
|
|
519
|
+
return validated;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function stateWith(state, change) {
|
|
523
|
+
const base = {
|
|
524
|
+
...state,
|
|
525
|
+
...change,
|
|
526
|
+
revision: state.revision + 1,
|
|
527
|
+
previousStateDigest: state.revision < 0 ? null : state.stateDigest,
|
|
528
|
+
};
|
|
529
|
+
const stateDigest = digest(withoutStateDigest(base));
|
|
530
|
+
return Object.freeze({ ...base, stateDigest });
|
|
531
|
+
}
|
|
532
|
+
function validateState(state, scope, options) {
|
|
533
|
+
if (!state ||
|
|
534
|
+
state.schemaVersion !== 1 ||
|
|
535
|
+
state.scope !== scope ||
|
|
536
|
+
state.overlayId !== options.overlayId ||
|
|
537
|
+
state.membershipEpoch !== options.membershipEpoch ||
|
|
538
|
+
!samePeer(state.localPeer, options.localPeer) ||
|
|
539
|
+
!validDigest(state.stateDigest) ||
|
|
540
|
+
state.revision < 0 ||
|
|
541
|
+
state.nextOutboundSequence < 1)
|
|
542
|
+
fail("state_rollback_detected");
|
|
543
|
+
const calculated = digest(withoutStateDigest(state));
|
|
544
|
+
if (calculated !== state.stateDigest)
|
|
545
|
+
fail("state_rollback_detected");
|
|
546
|
+
return state;
|
|
547
|
+
}
|
|
548
|
+
function admissionFromResponse(response) {
|
|
549
|
+
if (!response.accepted || !isObject(response.body))
|
|
550
|
+
return Object.freeze({
|
|
551
|
+
status: "rejected",
|
|
552
|
+
reasonCode: response.reasonCode ?? "authenticated_overlay_rejected",
|
|
553
|
+
});
|
|
554
|
+
const result = response.body;
|
|
555
|
+
if (result.status === "admitted" ||
|
|
556
|
+
result.status === "duplicate" ||
|
|
557
|
+
result.status === "backpressured" ||
|
|
558
|
+
result.status === "rejected")
|
|
559
|
+
return result;
|
|
560
|
+
return Object.freeze({
|
|
561
|
+
status: "rejected",
|
|
562
|
+
reasonCode: "authenticated_overlay_response_invalid",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function rejectingNetwork() {
|
|
566
|
+
return {
|
|
567
|
+
async exchange() {
|
|
568
|
+
throw new Error("endpoint_has_no_outbound_network");
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function peerIdentity(value, label) {
|
|
573
|
+
if (!isObject(value))
|
|
574
|
+
throw new TypeError(`${label} is invalid`);
|
|
575
|
+
return Object.freeze({
|
|
576
|
+
peerId: identifier(value.peerId, `${label}.peerId`),
|
|
577
|
+
peerIndex: integer(value.peerIndex, `${label}.peerIndex`, 0),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function samePeer(left, right) {
|
|
581
|
+
return left.peerId === right.peerId && left.peerIndex === right.peerIndex;
|
|
582
|
+
}
|
|
583
|
+
function networkKey(peer) {
|
|
584
|
+
return `${peer.peerIndex}:${peer.peerId}`;
|
|
585
|
+
}
|
|
586
|
+
function assertDispatchBinding(envelope, body) {
|
|
587
|
+
if (body.overlayId !== envelope.overlayId ||
|
|
588
|
+
body.logicalTime !== envelope.logicalTime)
|
|
589
|
+
fail("request_binding_invalid");
|
|
590
|
+
const actor = envelope.operation === "deliver" ? body.sender : body.requester;
|
|
591
|
+
if (!isObject(actor) ||
|
|
592
|
+
actor.peerId !== envelope.sender.peerId ||
|
|
593
|
+
actor.peerIndex !== envelope.sender.peerIndex)
|
|
594
|
+
fail("request_binding_invalid");
|
|
595
|
+
if (envelope.operation === "deliver") {
|
|
596
|
+
const delivery = body.delivery;
|
|
597
|
+
if (!delivery || delivery.senderPeerIndex !== envelope.sender.peerIndex)
|
|
598
|
+
fail("request_binding_invalid");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function withoutStateDigest(state) {
|
|
602
|
+
const { stateDigest: _stateDigest, ...unsigned } = state;
|
|
603
|
+
return unsigned;
|
|
604
|
+
}
|
|
605
|
+
function identifier(value, label) {
|
|
606
|
+
if (typeof value !== "string" ||
|
|
607
|
+
value.length < 1 ||
|
|
608
|
+
value.length > 256 ||
|
|
609
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/u.test(value))
|
|
610
|
+
throw new TypeError(`${label} is invalid`);
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
function integer(value, label, minimum) {
|
|
614
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
615
|
+
throw new TypeError(`${label} is invalid`);
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
function positive(value, fallback, label) {
|
|
619
|
+
return integer(value ?? fallback, label, 1);
|
|
620
|
+
}
|
|
621
|
+
function isObject(value) {
|
|
622
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
623
|
+
}
|
|
624
|
+
function canonical(value) {
|
|
625
|
+
const result = canonicalizeMeshJsonBytes(value);
|
|
626
|
+
if (!result.ok)
|
|
627
|
+
throw new TypeError("authenticated overlay canonicalization failed");
|
|
628
|
+
return result.value;
|
|
629
|
+
}
|
|
630
|
+
function digest(value) {
|
|
631
|
+
const bytes = canonical(value);
|
|
632
|
+
return `sha256:${sha256(bytes)}`;
|
|
633
|
+
}
|
|
634
|
+
function validDigest(value) {
|
|
635
|
+
return typeof value === "string" && /^sha256:[A-Za-z0-9_-]{43}$/u.test(value);
|
|
636
|
+
}
|
|
637
|
+
function sha256(bytes) {
|
|
638
|
+
return sha256Base64Url(bytes);
|
|
639
|
+
}
|
|
640
|
+
function encode(bytes) {
|
|
641
|
+
let binary = "";
|
|
642
|
+
for (const byte of bytes)
|
|
643
|
+
binary += String.fromCharCode(byte);
|
|
644
|
+
return btoa(binary)
|
|
645
|
+
.replace(/=/gu, "")
|
|
646
|
+
.replace(/\+/gu, "-")
|
|
647
|
+
.replace(/\//gu, "_");
|
|
648
|
+
}
|
|
649
|
+
function decode(value) {
|
|
650
|
+
if (!base64(value))
|
|
651
|
+
fail("signature_invalid");
|
|
652
|
+
const normalized = value.replace(/-/gu, "+").replace(/_/gu, "/");
|
|
653
|
+
const binary = atob(normalized + "=".repeat((4 - (normalized.length % 4)) % 4));
|
|
654
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
655
|
+
}
|
|
656
|
+
function base64(value) {
|
|
657
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]+$/u.test(value);
|
|
658
|
+
}
|
|
659
|
+
function copy(value) {
|
|
660
|
+
return value.slice().buffer;
|
|
661
|
+
}
|
|
662
|
+
function crypto(injected) {
|
|
663
|
+
const value = injected ?? globalThis.crypto;
|
|
664
|
+
if (!value?.subtle)
|
|
665
|
+
throw new TypeError("Web Crypto is unavailable");
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
function fail(code) {
|
|
669
|
+
const error = new Error(code);
|
|
670
|
+
error.name = "MeshAuthenticatedOverlayTransportErrorV1";
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
function rejectionCode(error) {
|
|
674
|
+
const message = error instanceof Error ? error.message : "unsupported_operation";
|
|
675
|
+
return isRejection(message) ? message : "unsupported_operation";
|
|
676
|
+
}
|
|
677
|
+
function isRejection(value) {
|
|
678
|
+
return [
|
|
679
|
+
"overlay_mismatch",
|
|
680
|
+
"recipient_mismatch",
|
|
681
|
+
"membership_epoch_mismatch",
|
|
682
|
+
"sender_not_active",
|
|
683
|
+
"recipient_not_active",
|
|
684
|
+
"peer_binding_invalid",
|
|
685
|
+
"key_not_accepted",
|
|
686
|
+
"key_revoked",
|
|
687
|
+
"key_not_yet_valid",
|
|
688
|
+
"key_expired",
|
|
689
|
+
"rotation_window_expired",
|
|
690
|
+
"signature_invalid",
|
|
691
|
+
"payload_digest_invalid",
|
|
692
|
+
"sequence_replayed",
|
|
693
|
+
"sequence_future",
|
|
694
|
+
"logical_time_future",
|
|
695
|
+
"request_binding_invalid",
|
|
696
|
+
"response_binding_invalid",
|
|
697
|
+
"state_rollback_detected",
|
|
698
|
+
"state_conflict",
|
|
699
|
+
"unsupported_operation",
|
|
700
|
+
].includes(value);
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=authenticated-overlay-transport.js.map
|