@ccmsg/cli 0.11.3 → 0.11.5
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 +8 -12
- package/package.json +1 -1
- package/src/auth/auth.ts +23 -23
- package/src/auth/http.ts +3 -3
- package/src/auth/records.ts +2 -2
- package/src/auth/webauthn.ts +1 -1
- package/src/cli.ts +10 -10
- package/src/daemon/control.ts +1 -1
- package/src/daemon/registry.ts +11 -11
- package/src/daemon/snapshot.ts +1 -1
- package/src/daemon/supervise.ts +4 -4
- package/src/dispatch/caller.ts +1 -1
- package/src/dispatch/dispatch.ts +4 -4
- package/src/dispatch/handler.ts +4 -4
- package/src/dispatch/identity.ts +1 -1
- package/src/dispatch/result.ts +2 -2
- package/src/files/containment.ts +2 -2
- package/src/harness/index.ts +1 -1
- package/src/instance/client.ts +1 -1
- package/src/instance/config.ts +13 -13
- package/src/instance/instance.ts +42 -42
- package/src/instance/lock.ts +2 -2
- package/src/instance/log.ts +1 -1
- package/src/instance/paths.ts +6 -6
- package/src/instance/socket.ts +1 -1
- package/src/kv/store.ts +1 -1
- package/src/mesh/instances.ts +2 -2
- package/src/mesh/keys.ts +7 -7
- package/src/mesh/mesh.ts +67 -67
- package/src/mesh/relay.ts +8 -8
- package/src/mesh/wire.ts +2 -2
- package/src/messaging/delivery.ts +24 -24
- package/src/messaging/direct.ts +23 -23
- package/src/messaging/inbox.ts +6 -6
- package/src/messaging/notify.ts +5 -5
- package/src/plugin/codex.ts +1 -1
- package/src/sessions/classify.ts +6 -6
- package/src/sessions/dump.ts +1 -1
- package/src/sessions/handlers.ts +2 -2
- package/src/sessions/harness.ts +12 -12
- package/src/sessions/last-live.ts +4 -4
- package/src/sessions/registry.ts +32 -32
- package/src/sessions/status.ts +7 -7
- package/src/sessions/terminals.ts +1 -1
- package/src/transcript/files.ts +4 -4
- package/src/transcript/fold.ts +9 -9
- package/src/transcript/items/classify.ts +56 -8
- package/src/transcript/read.ts +1 -1
- package/src/transcript/tail.ts +3 -3
- package/src/transcript/transcripts.ts +7 -7
- package/src/translate/helper.ts +2 -2
- package/src/transport/conn.ts +3 -3
- package/src/transport/dial.ts +1 -1
- package/src/transport/driver.ts +2 -2
- package/src/transport/entry.ts +1 -1
- package/src/transport/framing.ts +1 -1
- package/src/transport/listener.ts +3 -3
- package/src/transport/uds.ts +2 -2
- package/src/transport/ws.ts +2 -2
- package/src/upstream/events.ts +3 -3
- package/src/upstream/gateway.ts +6 -6
- package/src/upstream/json.ts +1 -1
- package/src/upstream/requests.ts +6 -6
- package/src/upstream/stats.ts +1 -1
- package/src/upstream/status.ts +4 -4
- package/src/upstream/usage.ts +1 -1
- package/src/upstream/webhook.ts +1 -1
package/src/mesh/mesh.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface MeshClaim {
|
|
|
58
58
|
* In the range WebSocket leaves to applications. It exists so the far end can
|
|
59
59
|
* tell this closure from a fault: losing a glare is its normal course, and
|
|
60
60
|
* reconnecting on it would reopen exactly the connection both sides just
|
|
61
|
-
* agreed to drop (§8.1). */
|
|
61
|
+
* agreed to drop (mesh-peer-auth §8.1). */
|
|
62
62
|
export const GLARE_CLOSE = 4000;
|
|
63
63
|
|
|
64
64
|
/** How often a link is asked whether it is still there, and how long silence
|
|
@@ -78,7 +78,7 @@ export const GLARE_CLOSE = 4000;
|
|
|
78
78
|
export const HEARTBEAT_MS = 20_000;
|
|
79
79
|
export const HEARTBEAT_TIMEOUT_MS = 3 * HEARTBEAT_MS;
|
|
80
80
|
|
|
81
|
-
/** The reconnection backoff (§8.2).
|
|
81
|
+
/** The reconnection backoff (mesh-peer-auth §8.2).
|
|
82
82
|
*
|
|
83
83
|
* Loose on purpose: a peer that comes back dials us, so the moment it recovers
|
|
84
84
|
* is signalled by its own start rather than found by our retries. What this
|
|
@@ -88,7 +88,7 @@ export const RECONNECT_MAX_MS = 60_000;
|
|
|
88
88
|
|
|
89
89
|
/** How many key requests are answered per second, over all callers.
|
|
90
90
|
*
|
|
91
|
-
* The key endpoint is reached before anything is proven (§6), so it is the one
|
|
91
|
+
* The key endpoint is reached before anything is proven (mesh-peer-auth §6), so it is the one
|
|
92
92
|
* surface an unauthenticated caller can make this instance do work on. The cap
|
|
93
93
|
* is well above what a mesh of any size needs — one request per connection
|
|
94
94
|
* established — and well below what would cost anything. */
|
|
@@ -96,7 +96,7 @@ const JWK_RATE_LIMIT = 20;
|
|
|
96
96
|
const JWK_RATE_WINDOW_MS = 1_000;
|
|
97
97
|
|
|
98
98
|
/** How long a forwarded op may take before its caller is told the instance
|
|
99
|
-
* could not be reached (§7.3).
|
|
99
|
+
* could not be reached (DESIGN §7.3).
|
|
100
100
|
*
|
|
101
101
|
* Chosen rather than derived: no primary source states a deadline. The
|
|
102
102
|
* reasoning is that the two outcomes this sits between are both worse than a
|
|
@@ -109,13 +109,13 @@ export const FORWARD_TIMEOUT_MS = 10_000;
|
|
|
109
109
|
/** What the instance gives the mesh once it exists.
|
|
110
110
|
*
|
|
111
111
|
* The mesh is built before the instance, because the listener has to be up for
|
|
112
|
-
* self-identification to reach it (§8.3), so the two things a link needs from
|
|
112
|
+
* self-identification to reach it (DESIGN §8.3), so the two things a link needs from
|
|
113
113
|
* the instance arrive afterwards rather than through the constructor. */
|
|
114
114
|
export interface MeshHost {
|
|
115
|
-
/** The one door a frame goes through (§
|
|
115
|
+
/** The one door a frame goes through (DESIGN §2.2). A request a peer carried here
|
|
116
116
|
* is answered by the same dispatch every other request is. */
|
|
117
117
|
handle(frame: unknown, conn: Requester): Promise<DispatchResult>;
|
|
118
|
-
/** Hand a relayed frame to this instance's own subscribers (§7.4). */
|
|
118
|
+
/** Hand a relayed frame to this instance's own subscribers (DESIGN §7.4). */
|
|
119
119
|
publish(topic: string, data: unknown, instance: InstanceId): void;
|
|
120
120
|
/** Take a frame on a topic the relay does not carry.
|
|
121
121
|
*
|
|
@@ -125,7 +125,7 @@ export interface MeshHost {
|
|
|
125
125
|
* receives it is the set itself, which merges by key (DR-0001 §2.6). */
|
|
126
126
|
element(topic: string, instance: InstanceId, data: unknown): void;
|
|
127
127
|
/** Which instances can be reached has changed, which is part of what this
|
|
128
|
-
* instance states on `peers` (§7.5). */
|
|
128
|
+
* instance states on `peers` (DESIGN §7.5). */
|
|
129
129
|
changed(): void;
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -134,7 +134,7 @@ export interface MeshDeps {
|
|
|
134
134
|
readonly id: InstanceId;
|
|
135
135
|
/** Every mesh endpoint, this instance's own among them. */
|
|
136
136
|
readonly peers: readonly Endpoint[];
|
|
137
|
-
/** Which of them is this instance, as the data says (§7.1). */
|
|
137
|
+
/** Which of them is this instance, as the data says (DESIGN §7.1). */
|
|
138
138
|
readonly self: Endpoint;
|
|
139
139
|
readonly conns: ConnRegistry;
|
|
140
140
|
readonly log?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
@@ -144,7 +144,7 @@ export interface MeshDeps {
|
|
|
144
144
|
readonly heartbeatTimeoutMs?: number;
|
|
145
145
|
readonly reconnectMinMs?: number;
|
|
146
146
|
readonly forwardTimeoutMs?: number;
|
|
147
|
-
/** The clock the retention window of §7.5 is read against. */
|
|
147
|
+
/** The clock the retention window of DESIGN §7.5 is read against. */
|
|
148
148
|
readonly now?: () => Timestamp;
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -153,16 +153,16 @@ interface Link {
|
|
|
153
153
|
readonly conn: Requester;
|
|
154
154
|
/** One actor per caller this link has spoken for, keyed by the identity
|
|
155
155
|
* itself. Cached rather than made per request because a subscription is held
|
|
156
|
-
* by a connection and released when it closes (§6.3): the topic mechanism
|
|
156
|
+
* by a connection and released when it closes (DESIGN §6.3): the topic mechanism
|
|
157
157
|
* has to see the same object each time one caller subscribes. */
|
|
158
158
|
readonly actors: Map<string, PeerActor>;
|
|
159
|
-
/** Which end opened the socket, which is what the glare rule compares (§8.1). */
|
|
159
|
+
/** Which end opened the socket, which is what the glare rule compares (mesh-peer-auth §8.1). */
|
|
160
160
|
readonly dialledByUs: boolean;
|
|
161
161
|
readonly heartbeat: ReturnType<typeof setInterval>;
|
|
162
162
|
lastHeard: number;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/** One request this instance forwarded and is waiting on (§7.3). */
|
|
165
|
+
/** One request this instance forwarded and is waiting on (DESIGN §7.3). */
|
|
166
166
|
interface Forwarded {
|
|
167
167
|
readonly peer: Endpoint;
|
|
168
168
|
/** The id the caller used, restored on the reply so the caller's connection
|
|
@@ -178,10 +178,10 @@ interface Forwarded {
|
|
|
178
178
|
* The envelope's `caller` is taken as said. It is the one thing the
|
|
179
179
|
* destination believes on the forwarder's word, and it can, because the link
|
|
180
180
|
* is authenticated: mesh-peer-auth proved the far end is an instance on the
|
|
181
|
-
* peer list, and a peer list is one deployment (§8.2). Everything else is
|
|
181
|
+
* peer list, and a peer list is one deployment (DESIGN §8.2). Everything else is
|
|
182
182
|
* decided here — the role check reads this identity against this instance's
|
|
183
183
|
* own attribute table, and so do the capability and locality checks, which is
|
|
184
|
-
* what §7.3 means by putting a forwarded op through the steps again rather
|
|
184
|
+
* what DESIGN §7.3 means by putting a forwarded op through the steps again rather
|
|
185
185
|
* than taking the forwarder's outcome for it.
|
|
186
186
|
*
|
|
187
187
|
* `from_instance` is not part of that judgement: the field says where the
|
|
@@ -213,11 +213,11 @@ class PeerActor implements Requester {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/** One handshake this instance is verifying, as the receiving end (§5).
|
|
216
|
+
/** One handshake this instance is verifying, as the receiving end (mesh-peer-auth §5).
|
|
217
217
|
*
|
|
218
218
|
* Everything the verification needs is here and nowhere else, so the whole of
|
|
219
219
|
* what a handshake leaves behind is one map entry that is deleted when it
|
|
220
|
-
* finishes — which is what §10.5 asks be true. */
|
|
220
|
+
* finishes — which is what mesh-peer-auth §10.5 asks be true. */
|
|
221
221
|
interface Pending {
|
|
222
222
|
readonly claim: MeshClaim;
|
|
223
223
|
readonly challenge: string;
|
|
@@ -225,14 +225,14 @@ interface Pending {
|
|
|
225
225
|
readonly fail: (cause: Error) => void;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/** One key this instance minted for a connection it dialled (§7). */
|
|
228
|
+
/** One key this instance minted for a connection it dialled (mesh-peer-auth §7). */
|
|
229
229
|
interface Minted {
|
|
230
230
|
readonly key: EphemeralKey;
|
|
231
231
|
readonly aud: Endpoint;
|
|
232
232
|
conn?: Requester;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
/** Which of two connections to one peer survives a glare (§8.1).
|
|
235
|
+
/** Which of two connections to one peer survives a glare (mesh-peer-auth §8.1).
|
|
236
236
|
*
|
|
237
237
|
* The connection opened by the smaller `iss` is the one that stays. Neither is
|
|
238
238
|
* better than the other — both were verified before either was dropped — so
|
|
@@ -249,7 +249,7 @@ export function glareKeepsNew(self: Endpoint, peer: Endpoint, dialledByUs: boole
|
|
|
249
249
|
* what each of them is.
|
|
250
250
|
*
|
|
251
251
|
* Every instance dials every peer, so there is no side that owns a link and no
|
|
252
|
-
* peer that cannot be recovered from the other end (§8, and §12's reason for
|
|
252
|
+
* peer that cannot be recovered from the other end (mesh-peer-auth §8, and mesh-peer-auth §12's reason for
|
|
253
253
|
* not assigning the duty to one side). */
|
|
254
254
|
export class Mesh {
|
|
255
255
|
readonly #links = new Map<Endpoint, Link>();
|
|
@@ -258,7 +258,7 @@ export class Mesh {
|
|
|
258
258
|
readonly #retries = new Map<Endpoint, ReturnType<typeof setTimeout>>();
|
|
259
259
|
readonly #backoff = new Map<Endpoint, number>();
|
|
260
260
|
/** Which of the configured endpoints is this instance, as the data said
|
|
261
|
-
* before anything is dialled and fixed from then on (§5.5). */
|
|
261
|
+
* before anything is dialled and fixed from then on (mesh-peer-auth §5.5). */
|
|
262
262
|
readonly #self: Endpoint;
|
|
263
263
|
/** The authenticated endpoint-to-id mapping (DR-0001 §2.1), in both
|
|
264
264
|
* directions: a handshake writes it, `to_instance` reads it to find the link
|
|
@@ -280,13 +280,13 @@ export class Mesh {
|
|
|
280
280
|
readonly #linkOf = new Map<Requester, Link>();
|
|
281
281
|
readonly #forwarded = new Map<string, Forwarded>();
|
|
282
282
|
/** The relayed topics local subscribers are asking for right now. `peers` is
|
|
283
|
-
* always among them: it is the routing table of §7.3, and a question about
|
|
283
|
+
* always among them: it is the routing table of DESIGN §7.3, and a question about
|
|
284
284
|
* where a session lives is answered whether or not anyone is subscribed
|
|
285
|
-
* (§6.3, "reading the current value is not what subscription drives"). */
|
|
285
|
+
* (DESIGN §6.3, "reading the current value is not what subscription drives"). */
|
|
286
286
|
readonly #demanded = new Set<string>(["peers", AUTH_TOPIC]);
|
|
287
287
|
#host: MeshHost | undefined;
|
|
288
288
|
|
|
289
|
-
/** What the peers said, kept across a disconnection (§7.5). */
|
|
289
|
+
/** What the peers said, kept across a disconnection (DESIGN §7.5). */
|
|
290
290
|
readonly relay: Relay;
|
|
291
291
|
|
|
292
292
|
constructor(private readonly deps: MeshDeps) {
|
|
@@ -304,13 +304,13 @@ export class Mesh {
|
|
|
304
304
|
});
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
/** Give the mesh the instance it belongs to (§8.3). */
|
|
307
|
+
/** Give the mesh the instance it belongs to (DESIGN §8.3). */
|
|
308
308
|
bind(host: MeshHost): void {
|
|
309
309
|
this.#host = host;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/** The registry the mesh's own connections are in. It is the instance's, and
|
|
313
|
-
* is shared because a mesh link is one of its connections (§
|
|
313
|
+
* is shared because a mesh link is one of its connections (DESIGN §2.1). */
|
|
314
314
|
get conns(): ConnRegistry {
|
|
315
315
|
return this.deps.conns;
|
|
316
316
|
}
|
|
@@ -332,7 +332,7 @@ export class Mesh {
|
|
|
332
332
|
/** The peers this instance dials: the configured list without itself.
|
|
333
333
|
*
|
|
334
334
|
* The list is the same on every instance, which is what lets one file be
|
|
335
|
-
* distributed to all of them (§8.2) — and it names this instance too, so
|
|
335
|
+
* distributed to all of them (DESIGN §8.2) — and it names this instance too, so
|
|
336
336
|
* removing ourselves is the reader's job rather than the writer's. `identify`
|
|
337
337
|
* is what found which entry that is. */
|
|
338
338
|
get peers(): Endpoint[] {
|
|
@@ -342,7 +342,7 @@ export class Mesh {
|
|
|
342
342
|
|
|
343
343
|
/** The peers taken off this host's list while this instance was running.
|
|
344
344
|
*
|
|
345
|
-
* Config is read once (
|
|
345
|
+
* Config is read once (DR-0004) and this does not change that: what a person
|
|
346
346
|
* writes goes on taking effect at the next start. What this holds is the one
|
|
347
347
|
* edit that cannot wait for one — an endpoint this host is no longer to be
|
|
348
348
|
* talking to, which would otherwise stay connected until somebody restarted
|
|
@@ -369,7 +369,7 @@ export class Mesh {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
/** Where peers reach this instance: the row of the mesh carrying its own id
|
|
372
|
-
* (§7.1). Everything that reads it — the handshake's `aud`, the mesh's own
|
|
372
|
+
* (DESIGN §7.1). Everything that reads it — the handshake's `aud`, the mesh's own
|
|
373
373
|
* routes, what `hello` reports — is the one address the data states. */
|
|
374
374
|
get self(): Endpoint {
|
|
375
375
|
return this.#self;
|
|
@@ -385,7 +385,7 @@ export class Mesh {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
/** What `hello` reports: this instance, then every configured peer, with
|
|
388
|
-
* whether it can be reached right now (§7.5).
|
|
388
|
+
* whether it can be reached right now (DESIGN §7.5).
|
|
389
389
|
*
|
|
390
390
|
* A peer no handshake has settled yet is listed without an id. The operator
|
|
391
391
|
* configured that endpoint, so it is an entry of the cluster whether or not
|
|
@@ -409,7 +409,7 @@ export class Mesh {
|
|
|
409
409
|
/** Whether any peer is currently out of reach.
|
|
410
410
|
*
|
|
411
411
|
* What separates "no instance in the cluster knows this session" from "an
|
|
412
|
-
* instance that might know it cannot be asked" — the one distinction §
|
|
412
|
+
* instance that might know it cannot be asked" — the one distinction DESIGN §6.6
|
|
413
413
|
* says rests on the mesh's connection state and on nothing else. */
|
|
414
414
|
anyUnreachable(): boolean {
|
|
415
415
|
return this.peers.some((peer) => !this.reachable(peer));
|
|
@@ -420,7 +420,7 @@ export class Mesh {
|
|
|
420
420
|
* A session the cluster has named belongs to the instance its `peers` row
|
|
421
421
|
* states. One nobody has named while a peer is out of reach is answered with
|
|
422
422
|
* that peer: forwarding there fails and the caller is told
|
|
423
|
-
* `instance_unreachable`, which is what §
|
|
423
|
+
* `instance_unreachable`, which is what DESIGN §6.6 asks for in place of deciding
|
|
424
424
|
* the session does not exist. */
|
|
425
425
|
ownerOf(sid: Sid): InstanceId | undefined {
|
|
426
426
|
const owner = this.relay.owner(sid);
|
|
@@ -436,7 +436,7 @@ export class Mesh {
|
|
|
436
436
|
return undefined;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
// --- op forwarding (§7.3) ---
|
|
439
|
+
// --- op forwarding (DESIGN §7.3) ---
|
|
440
440
|
|
|
441
441
|
/** Carry one op to the instance that owns its subject, and bring the answer
|
|
442
442
|
* back.
|
|
@@ -499,7 +499,7 @@ export class Mesh {
|
|
|
499
499
|
* What the person's authentication needs of a peer (`auth.resolve`,
|
|
500
500
|
* `auth.rotate`) is a fact only its issuer holds, asked for by the instance
|
|
501
501
|
* that needs it — so the `caller` is this instance's own role, and the
|
|
502
|
-
* request travels the ordinary forwarding path (§7.3, DR-0001 §2.6).
|
|
502
|
+
* request travels the ordinary forwarding path (DESIGN §7.3, DR-0001 §2.6).
|
|
503
503
|
*
|
|
504
504
|
* The body of the reply is answered, and a refusal is thrown as the error the
|
|
505
505
|
* far end named, so a caller reads one outcome rather than a result kind. */
|
|
@@ -528,7 +528,7 @@ export class Mesh {
|
|
|
528
528
|
return this.#linkOf.has(conn);
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
/** The connection a forwarded request is dispatched as (§7.3).
|
|
531
|
+
/** The connection a forwarded request is dispatched as (DESIGN §7.3).
|
|
532
532
|
*
|
|
533
533
|
* The caller the envelope names, on the link it arrived over. A request that
|
|
534
534
|
* names none is dispatched as the link itself, whose role is `instance` —
|
|
@@ -549,7 +549,7 @@ export class Mesh {
|
|
|
549
549
|
return actor;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
// --- event relay (§7.4) ---
|
|
552
|
+
// --- event relay (DESIGN §7.4) ---
|
|
553
553
|
|
|
554
554
|
/** The current value of a relayed topic, one entry per instance that has
|
|
555
555
|
* stated one. Handed to a fresh local subscriber beside this instance's own
|
|
@@ -561,12 +561,12 @@ export class Mesh {
|
|
|
561
561
|
/** A local subscriber appeared on a cluster topic, or the last one left.
|
|
562
562
|
*
|
|
563
563
|
* The subscription travels: what a subscriber asks of this instance, this
|
|
564
|
-
* instance asks of every peer, and the frames come back unchanged (§7.4).
|
|
564
|
+
* instance asks of every peer, and the frames come back unchanged (DESIGN §7.4).
|
|
565
565
|
* `peers` is never given up, because it is also the routing table. */
|
|
566
566
|
demand(topic: string, wanted: boolean): void {
|
|
567
567
|
// `auth.records` is never given up and never asked for on demand: every
|
|
568
568
|
// instance holds the whole set whether or not anything local is watching
|
|
569
|
-
// it, the way `peers` is also the routing table (§7.4, DR-0001 §2.6).
|
|
569
|
+
// it, the way `peers` is also the routing table (DESIGN §7.4, DR-0001 §2.6).
|
|
570
570
|
if (topic === AUTH_TOPIC || !isClusterTopic(topic)) return;
|
|
571
571
|
if (wanted) {
|
|
572
572
|
if (this.#demanded.has(topic)) return;
|
|
@@ -594,7 +594,7 @@ export class Mesh {
|
|
|
594
594
|
// The instance asks on behalf of whoever subscribed to it, and what they
|
|
595
595
|
// have in common is that they are this deployment's people rather than
|
|
596
596
|
// any one session: a cluster topic is the same value for all of them
|
|
597
|
-
// (§6.2), so there is nothing narrower to name.
|
|
597
|
+
// (DESIGN §6.2), so there is nothing narrower to name.
|
|
598
598
|
//
|
|
599
599
|
// `auth.records` is the exception, and the one topic no person may hear:
|
|
600
600
|
// it carries the tokens that authenticate them, so the instance asks for
|
|
@@ -621,10 +621,10 @@ export class Mesh {
|
|
|
621
621
|
* The whole judgement is here, inside the op that dispatch already validated
|
|
622
622
|
* and allowed: nothing settles an identity on another path, and a handshake
|
|
623
623
|
* that fails any step throws, which is what leaves the connection anonymous
|
|
624
|
-
* (§
|
|
624
|
+
* (DESIGN §2.2 step 7). */
|
|
625
625
|
async greet(conn: Requester, claim: MeshClaim): Promise<void> {
|
|
626
626
|
const self = this.self;
|
|
627
|
-
// 1-3 of §5.7, asked before the key is fetched: the cheap comparisons come
|
|
627
|
+
// 1-3 of mesh-peer-auth §5.7, asked before the key is fetched: the cheap comparisons come
|
|
628
628
|
// first because the fetch reaches out to another host.
|
|
629
629
|
if (claim.ver !== MESH_VER) {
|
|
630
630
|
throw new OpError("invalid_args", `this instance speaks mesh handshake ${MESH_VER}`);
|
|
@@ -657,7 +657,7 @@ export class Mesh {
|
|
|
657
657
|
try {
|
|
658
658
|
// The key comes over a connection of its own, opened to the endpoint the
|
|
659
659
|
// greeting names. Asking for it on this connection would let whoever
|
|
660
|
-
// opened it answer with their own key and pass their own signature (§6).
|
|
660
|
+
// opened it answer with their own key and pass their own signature (mesh-peer-auth §6).
|
|
661
661
|
const jwk = await this.#fetchKey(claim, challenge);
|
|
662
662
|
const jws = await withTimeout(
|
|
663
663
|
proof.promise,
|
|
@@ -674,7 +674,7 @@ export class Mesh {
|
|
|
674
674
|
);
|
|
675
675
|
} finally {
|
|
676
676
|
// The challenge is spent whatever happened, so there is no record of it
|
|
677
|
-
// anywhere once the handshake ends (§5.5, §10.5).
|
|
677
|
+
// anywhere once the handshake ends (mesh-peer-auth §5.5, §10.5).
|
|
678
678
|
this.#pending.delete(conn);
|
|
679
679
|
}
|
|
680
680
|
// The claim is checked again now that it is trusted: the fetch and the wait
|
|
@@ -709,7 +709,7 @@ export class Mesh {
|
|
|
709
709
|
/** A frame that is not an op. True when the mesh took it.
|
|
710
710
|
*
|
|
711
711
|
* The proof arrives here because it belongs on the connection being
|
|
712
|
-
* authenticated (§5), which is the one connection the op vocabulary has no
|
|
712
|
+
* authenticated (mesh-peer-auth §5), which is the one connection the op vocabulary has no
|
|
713
713
|
* name for: mesh carries no ops of its own (contract, `Plane`). */
|
|
714
714
|
frame(conn: Requester, frame: unknown): boolean {
|
|
715
715
|
const mesh = meshFrameOf(frame);
|
|
@@ -724,7 +724,7 @@ export class Mesh {
|
|
|
724
724
|
}
|
|
725
725
|
const pending = this.#pending.get(conn);
|
|
726
726
|
// A proof with no handshake waiting for it: either none was started, or the
|
|
727
|
-
// challenge it answers has already been spent. Neither is retried (§5.5).
|
|
727
|
+
// challenge it answers has already been spent. Neither is retried (mesh-peer-auth §5.5).
|
|
728
728
|
if (pending === undefined) {
|
|
729
729
|
conn.close();
|
|
730
730
|
return true;
|
|
@@ -734,7 +734,7 @@ export class Mesh {
|
|
|
734
734
|
}
|
|
735
735
|
|
|
736
736
|
/** What a proven link wrote that is not a request: a topic frame to relay
|
|
737
|
-
* (§7.4), or the reply to something this instance forwarded (§7.3).
|
|
737
|
+
* (DESIGN §7.4), or the reply to something this instance forwarded (DESIGN §7.3).
|
|
738
738
|
*
|
|
739
739
|
* Only a link is read this way. A client connection could otherwise guess a
|
|
740
740
|
* forwarded id and settle a request it has nothing to do with, and could
|
|
@@ -777,12 +777,12 @@ export class Mesh {
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
/** Whether this connection is mid-handshake, which is what makes an ordinary
|
|
780
|
-
* request on it a protocol violation rather than an early call (§5.8). */
|
|
780
|
+
* request on it a protocol violation rather than an early call (mesh-peer-auth §5.8). */
|
|
781
781
|
handshaking(conn: Requester): boolean {
|
|
782
782
|
return this.#pending.has(conn);
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
// --- the HTTP surface: the key of §6 ---
|
|
785
|
+
// --- the HTTP surface: the key of mesh-peer-auth §6 ---
|
|
786
786
|
|
|
787
787
|
/** Answer the one request that is served before anything is proven, or
|
|
788
788
|
* nothing when the request is not it. */
|
|
@@ -813,7 +813,7 @@ export class Mesh {
|
|
|
813
813
|
}
|
|
814
814
|
const minted = this.#minted.get(kid);
|
|
815
815
|
// Unknown to us, or known and no longer connected to the handshake it was
|
|
816
|
-
// made for. Either way there is no key to give (§6.1).
|
|
816
|
+
// made for. Either way there is no key to give (mesh-peer-auth §6.1).
|
|
817
817
|
if (minted === undefined || minted.conn === undefined) {
|
|
818
818
|
return new Response("no such key", { status: 404 });
|
|
819
819
|
}
|
|
@@ -846,7 +846,7 @@ export class Mesh {
|
|
|
846
846
|
return this.#jwkServed <= JWK_RATE_LIMIT;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
-
// --- the dialling end (§5, steps 1-3 and 13) ---
|
|
849
|
+
// --- the dialling end (mesh-peer-auth §5, steps 1-3 and 13) ---
|
|
850
850
|
|
|
851
851
|
async #dial(peer: Endpoint): Promise<void> {
|
|
852
852
|
if (this.#stopping || this.#links.has(peer)) return;
|
|
@@ -887,7 +887,7 @@ export class Mesh {
|
|
|
887
887
|
|
|
888
888
|
/** What the far end wrote on a connection we opened.
|
|
889
889
|
*
|
|
890
|
-
* The greeting's reply is the acknowledgement of §5.8: it is what says the
|
|
890
|
+
* The greeting's reply is the acknowledgement of mesh-peer-auth §5.8: it is what says the
|
|
891
891
|
* peer finished verifying, which is both the moment this instance may speak
|
|
892
892
|
* and the moment its key has no further use. */
|
|
893
893
|
#dialledFrame(peer: Endpoint, conn: Requester, frame: unknown, kid: string): void {
|
|
@@ -897,7 +897,7 @@ export class Mesh {
|
|
|
897
897
|
// A request the peer forwarded to us. A dialled connection is answered by
|
|
898
898
|
// whoever dialled it (transport, `DialOptions`), so the reply goes out
|
|
899
899
|
// here rather than through the driver — but what decides it is the same
|
|
900
|
-
// dispatch every other request goes through (§7.3).
|
|
900
|
+
// dispatch every other request goes through (DESIGN §7.3).
|
|
901
901
|
if (typeof fields["op"] === "string") this.#answer(conn, fields);
|
|
902
902
|
return;
|
|
903
903
|
}
|
|
@@ -962,7 +962,7 @@ export class Mesh {
|
|
|
962
962
|
* one is already held.
|
|
963
963
|
*
|
|
964
964
|
* Both connections are verified before either is dropped, so whichever
|
|
965
|
-
* survives is one that was proven (§8.1). */
|
|
965
|
+
* survives is one that was proven (mesh-peer-auth §8.1). */
|
|
966
966
|
#hold(peer: Endpoint, conn: Requester, dialledByUs: boolean): void {
|
|
967
967
|
const self = this.self;
|
|
968
968
|
const held = this.#links.get(peer);
|
|
@@ -996,7 +996,7 @@ export class Mesh {
|
|
|
996
996
|
this.#drop(peer, conn);
|
|
997
997
|
});
|
|
998
998
|
// What it said before is still held and stops being marked; what it says
|
|
999
|
-
// now replaces it, which is the whole of "restored by reconnection" (§7.5).
|
|
999
|
+
// now replaces it, which is the whole of "restored by reconnection" (DESIGN §7.5).
|
|
1000
1000
|
// Under the id, because that is what its frames name themselves with: the
|
|
1001
1001
|
// endpoint is where the link was dialled and says nothing about the value.
|
|
1002
1002
|
const id = this.#idOf.get(peer);
|
|
@@ -1008,7 +1008,7 @@ export class Mesh {
|
|
|
1008
1008
|
|
|
1009
1009
|
/** Say that the set of reachable instances moved. Two listeners: whatever
|
|
1010
1010
|
* the deps gave, and the instance, which restates `peers` — the topic the
|
|
1011
|
-
* view rides on (§7.5). */
|
|
1011
|
+
* view rides on (DESIGN §7.5). */
|
|
1012
1012
|
#changed(): void {
|
|
1013
1013
|
this.deps.onChanged?.();
|
|
1014
1014
|
this.#host?.changed();
|
|
@@ -1021,7 +1021,7 @@ export class Mesh {
|
|
|
1021
1021
|
if (silence > (this.deps.heartbeatTimeoutMs ?? HEARTBEAT_TIMEOUT_MS)) {
|
|
1022
1022
|
// Nothing has come back for long enough that the link is gone whatever
|
|
1023
1023
|
// the socket believes — which is the whole reason for the heartbeat
|
|
1024
|
-
// (§8.3): a middlebox drops a connection without telling either end.
|
|
1024
|
+
// (mesh-peer-auth §8.3): a middlebox drops a connection without telling either end.
|
|
1025
1025
|
this.deps.log?.("mesh peer went silent", { peer, silence_ms: silence });
|
|
1026
1026
|
link.conn.close();
|
|
1027
1027
|
this.#drop(peer, link.conn);
|
|
@@ -1043,7 +1043,7 @@ export class Mesh {
|
|
|
1043
1043
|
this.#links.delete(peer);
|
|
1044
1044
|
this.#linkOf.delete(conn);
|
|
1045
1045
|
// Its sessions become a kind of Disappeared and its values are marked
|
|
1046
|
-
// rather than dropped (§7.5), and anything on its way there is answered
|
|
1046
|
+
// rather than dropped (DESIGN §7.5), and anything on its way there is answered
|
|
1047
1047
|
// now instead of waiting out a deadline it can no longer beat.
|
|
1048
1048
|
const id = this.#idOf.get(peer);
|
|
1049
1049
|
if (id !== undefined) this.relay.lost(id);
|
|
@@ -1080,7 +1080,7 @@ export class Mesh {
|
|
|
1080
1080
|
this.#retries.set(peer, timer);
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
|
-
/** Let every link and every timer go. Called from the stop order (§8.5). */
|
|
1083
|
+
/** Let every link and every timer go. Called from the stop order (DESIGN §8.5). */
|
|
1084
1084
|
stop(): void {
|
|
1085
1085
|
this.#stopping = true;
|
|
1086
1086
|
for (const timer of this.#retries.values()) clearTimeout(timer);
|
|
@@ -1091,14 +1091,14 @@ export class Mesh {
|
|
|
1091
1091
|
// The far end has no other way to learn this instance is going: it would
|
|
1092
1092
|
// keep the link, keep answering `reachable`, and keep routing
|
|
1093
1093
|
// `instance-local` ops here until its own heartbeat gave up minutes later,
|
|
1094
|
-
// where the disconnection of §7.5 is supposed to be immediate. Which side
|
|
1094
|
+
// where the disconnection of DESIGN §7.5 is supposed to be immediate. Which side
|
|
1095
1095
|
// dialled a link is decided by the glare rule from a comparison of
|
|
1096
|
-
// endpoint strings (§8.1), so which of a peer's links this instance
|
|
1096
|
+
// endpoint strings (mesh-peer-auth §8.1), so which of a peer's links this instance
|
|
1097
1097
|
// accepted is not something either end chose — leaving those open makes a
|
|
1098
1098
|
// clean stop look like a silent one to whichever half of the cluster the
|
|
1099
1099
|
// comparison put on this side.
|
|
1100
1100
|
//
|
|
1101
|
-
// An accepted socket is transport's to release (§8.5 step 5), and left to
|
|
1101
|
+
// An accepted socket is transport's to release (DESIGN §8.5 step 5), and left to
|
|
1102
1102
|
// it the far end is told whenever the listener gets round to it: measured
|
|
1103
1103
|
// against Bun 1.3.13, `stop` on a server that has itself closed a
|
|
1104
1104
|
// WebSocket — which the mesh does, to drop the loser of a glare — never
|
|
@@ -1107,7 +1107,7 @@ export class Mesh {
|
|
|
1107
1107
|
// Closing here is what makes the notice the mesh's own rather than a side
|
|
1108
1108
|
// effect of a listener going down. The step-3 notice this precedes is
|
|
1109
1109
|
// `restarting`, which is addressed to clients — a peer learns from the
|
|
1110
|
-
// link, and that is the whole of what §7.5 asks for.
|
|
1110
|
+
// link, and that is the whole of what DESIGN §7.5 asks for.
|
|
1111
1111
|
for (const link of this.#links.values()) {
|
|
1112
1112
|
clearInterval(link.heartbeat);
|
|
1113
1113
|
link.conn.close();
|
|
@@ -1116,7 +1116,7 @@ export class Mesh {
|
|
|
1116
1116
|
this.#links.clear();
|
|
1117
1117
|
this.#linkOf.clear();
|
|
1118
1118
|
// Keys die with the connections they were made for, and none outlives this
|
|
1119
|
-
// (§7).
|
|
1119
|
+
// (mesh-peer-auth §7).
|
|
1120
1120
|
this.#minted.clear();
|
|
1121
1121
|
for (const pending of this.#pending.values()) {
|
|
1122
1122
|
pending.fail(new Error("this instance is stopping"));
|
|
@@ -1125,7 +1125,7 @@ export class Mesh {
|
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
1127
|
/** What is held per handshake right now, so a test can state that nothing is
|
|
1128
|
-
* kept once one has finished (§10.5). */
|
|
1128
|
+
* kept once one has finished (mesh-peer-auth §10.5). */
|
|
1129
1129
|
get held(): { keys: number; handshakes: number; links: number } {
|
|
1130
1130
|
return {
|
|
1131
1131
|
keys: this.#minted.size,
|
|
@@ -1141,7 +1141,7 @@ export class Mesh {
|
|
|
1141
1141
|
const response = await fetch(jwkEndpoint(claim.iss, claim.kid), {
|
|
1142
1142
|
method: "POST",
|
|
1143
1143
|
// One request and close, which is what the second connection is
|
|
1144
|
-
// (§6.2): it exists to carry the key and the challenge, and keeping it
|
|
1144
|
+
// (mesh-peer-auth §6.2): it exists to carry the key and the challenge, and keeping it
|
|
1145
1145
|
// pooled afterwards would leave a connection nothing speaks on.
|
|
1146
1146
|
headers: { "content-type": "application/json", connection: "close" },
|
|
1147
1147
|
body: JSON.stringify(body),
|
|
@@ -1149,14 +1149,14 @@ export class Mesh {
|
|
|
1149
1149
|
});
|
|
1150
1150
|
if (!response.ok) throw new Error(`${claim.iss} did not hand out the key ${claim.kid}`);
|
|
1151
1151
|
const jwk = (await response.json()) as MeshJwk;
|
|
1152
|
-
// §5.7-8, the third of the three ids that have to agree: a key served under
|
|
1152
|
+
// mesh-peer-auth §5.7-8, the third of the three ids that have to agree: a key served under
|
|
1153
1153
|
// one id and answering to another would break the correspondence the whole
|
|
1154
1154
|
// exchange is keyed on.
|
|
1155
1155
|
if (jwk.kid !== claim.kid) throw new Error("the key served is not the key asked for");
|
|
1156
1156
|
return jwk;
|
|
1157
1157
|
}
|
|
1158
1158
|
|
|
1159
|
-
/** §5.7, steps 4 to 8. */
|
|
1159
|
+
/** mesh-peer-auth §5.7, steps 4 to 8. */
|
|
1160
1160
|
#verify(jws: string, claim: MeshClaim, challenge: string, jwk: MeshJwk): void {
|
|
1161
1161
|
let parsed;
|
|
1162
1162
|
try {
|
package/src/mesh/relay.ts
CHANGED
|
@@ -21,7 +21,7 @@ const ROW_TOPICS: readonly string[] = ["peers", "agents"];
|
|
|
21
21
|
|
|
22
22
|
/** The topics a subscriber sees the whole cluster on.
|
|
23
23
|
*
|
|
24
|
-
* A per-instance whole is relayable by construction (§6.2): a frame replaces
|
|
24
|
+
* A per-instance whole is relayable by construction (DESIGN §6.2): a frame replaces
|
|
25
25
|
* its own instance's entries and leaves every other instance's alone, so
|
|
26
26
|
* several instances can state the same topic name without colliding. The rows
|
|
27
27
|
* above are relayable for the same reason read one element at a time. */
|
|
@@ -56,7 +56,7 @@ function rowsOf(topic: string, data: unknown): readonly SessionRow[] {
|
|
|
56
56
|
|
|
57
57
|
export interface RelayDeps {
|
|
58
58
|
/** Hand a relayed frame to this instance's own subscribers, under the
|
|
59
|
-
* instance that produced it (§7.4). */
|
|
59
|
+
* instance that produced it (DESIGN §7.4). */
|
|
60
60
|
readonly publish: (topic: string, data: unknown, instance: InstanceId) => void;
|
|
61
61
|
/** The clock, so a test can move the retention window without waiting it
|
|
62
62
|
* out. */
|
|
@@ -64,7 +64,7 @@ export interface RelayDeps {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** What the peers said, held on this instance so that losing a peer does not
|
|
67
|
-
* empty the cluster view (§7.5).
|
|
67
|
+
* empty the cluster view (DESIGN §7.5).
|
|
68
68
|
*
|
|
69
69
|
* Two things live here and nowhere else: the last whole value each instance
|
|
70
70
|
* stated per topic, and whether that instance can be reached right now. The
|
|
@@ -78,7 +78,7 @@ export interface RelayDeps {
|
|
|
78
78
|
export class Relay {
|
|
79
79
|
/** Per instance, the last whole value it stated per topic. */
|
|
80
80
|
readonly #held = new Map<InstanceId, Map<string, unknown>>();
|
|
81
|
-
/** The mark of §7.5: when this instance stopped being reachable. Absent
|
|
81
|
+
/** The mark of DESIGN §7.5: when this instance stopped being reachable. Absent
|
|
82
82
|
* while it is reachable. */
|
|
83
83
|
readonly #lostAt = new Map<InstanceId, Timestamp>();
|
|
84
84
|
|
|
@@ -97,7 +97,7 @@ export class Relay {
|
|
|
97
97
|
* peer it arrived from: a mesh of three relays transitively, and the frame
|
|
98
98
|
* names its origin the whole way. Held under that origin, and passed on
|
|
99
99
|
* unchanged — recomputing it would put the same judgement in two places
|
|
100
|
-
* (§7.4). */
|
|
100
|
+
* (DESIGN §7.4). */
|
|
101
101
|
accept(instance: InstanceId, topic: string, data: unknown, snapshot = false): void {
|
|
102
102
|
if (!isClusterTopic(topic)) return;
|
|
103
103
|
this.#sweep();
|
|
@@ -134,7 +134,7 @@ export class Relay {
|
|
|
134
134
|
|
|
135
135
|
/** The link to this instance is gone. What it said is kept and marked,
|
|
136
136
|
* because dropping it would empty the view until the instance comes back
|
|
137
|
-
* and restates everything (§7.5). */
|
|
137
|
+
* and restates everything (DESIGN §7.5). */
|
|
138
138
|
lost(instance: InstanceId): void {
|
|
139
139
|
if (!this.#lostAt.has(instance)) this.#lostAt.set(instance, this.#now());
|
|
140
140
|
this.#sweep();
|
|
@@ -175,7 +175,7 @@ export class Relay {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/** Which instance a session belongs to, read from the cluster values the
|
|
178
|
-
* peers stated (§7.3).
|
|
178
|
+
* peers stated (DESIGN §7.3).
|
|
179
179
|
*
|
|
180
180
|
* `peers` names every session an instance holds, connected and lost alike,
|
|
181
181
|
* and is checked first. A session whose greeting has not reached its
|
|
@@ -199,7 +199,7 @@ export class Relay {
|
|
|
199
199
|
/** Drop what an instance said once it has been gone for the retention
|
|
200
200
|
* window. The window is the contract's, shared with `last_live` and the
|
|
201
201
|
* inbox: past it, everything the value would point at is gone too
|
|
202
|
-
* (§7.5,
|
|
202
|
+
* (DESIGN §7.5, DR-0014). */
|
|
203
203
|
#sweep(): void {
|
|
204
204
|
const now = this.#now();
|
|
205
205
|
for (const [instance, since] of this.#lostAt) {
|
package/src/mesh/wire.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function wsEndpoint(endpoint: Endpoint): string {
|
|
|
24
24
|
/** Where one connection's key is fetched, and the challenge for it left.
|
|
25
25
|
*
|
|
26
26
|
* A plain request rather than a frame on the link, because this is the second
|
|
27
|
-
* connection of §6: the protocol asks that the key be fetched outside the
|
|
27
|
+
* connection of mesh-peer-auth §6: the protocol asks that the key be fetched outside the
|
|
28
28
|
* connection being authenticated. */
|
|
29
29
|
export function jwkEndpoint(endpoint: Endpoint, kid: string): string {
|
|
30
30
|
return `${endpoint}${JWK_ROUTE}${encodeURIComponent(kid)}`;
|
|
@@ -66,7 +66,7 @@ export function meshFrameOf(frame: unknown): MeshFrame | undefined {
|
|
|
66
66
|
return typeof jws === "string" ? { mesh: "proof", jws } : undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** What a key request carries: the challenge the proof must sign (§6). */
|
|
69
|
+
/** What a key request carries: the challenge the proof must sign (mesh-peer-auth §6). */
|
|
70
70
|
export interface JwkRequest {
|
|
71
71
|
readonly ver: number;
|
|
72
72
|
readonly challenge: string;
|