@ccmsg/cli 0.1.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/LICENSE +21 -0
- package/README.md +23 -0
- package/package.json +32 -0
- package/src/cli.ts +1074 -0
- package/src/daemon/control.ts +88 -0
- package/src/daemon/index.ts +6 -0
- package/src/daemon/link.ts +93 -0
- package/src/daemon/log.ts +116 -0
- package/src/daemon/registry.ts +285 -0
- package/src/daemon/snapshot.ts +115 -0
- package/src/daemon/supervise.ts +446 -0
- package/src/dispatch/caller.ts +47 -0
- package/src/dispatch/dispatch.ts +128 -0
- package/src/dispatch/handler.ts +55 -0
- package/src/dispatch/identity.ts +22 -0
- package/src/dispatch/index.ts +5 -0
- package/src/dispatch/result.ts +58 -0
- package/src/files/containment.ts +263 -0
- package/src/files/files.ts +421 -0
- package/src/files/index.ts +14 -0
- package/src/files/sandbox.ts +0 -0
- package/src/greeting/hook.ts +48 -0
- package/src/greeting/index.ts +2 -0
- package/src/greeting/meta.ts +66 -0
- package/src/instance/config.ts +424 -0
- package/src/instance/handlers.ts +28 -0
- package/src/instance/identity.ts +44 -0
- package/src/instance/index.ts +8 -0
- package/src/instance/instance.ts +911 -0
- package/src/instance/lock.ts +108 -0
- package/src/instance/log.ts +30 -0
- package/src/instance/paths.ts +200 -0
- package/src/instance/socket.ts +62 -0
- package/src/kv/index.ts +2 -0
- package/src/kv/merge.ts +66 -0
- package/src/kv/store.ts +195 -0
- package/src/launcher/index.ts +4 -0
- package/src/launcher/launcher.ts +190 -0
- package/src/launcher/roots.ts +32 -0
- package/src/launcher/spawn.ts +81 -0
- package/src/launcher/tree.ts +80 -0
- package/src/mesh/index.ts +5 -0
- package/src/mesh/keys.ts +158 -0
- package/src/mesh/mesh.ts +1169 -0
- package/src/mesh/probe.ts +100 -0
- package/src/mesh/relay.ts +147 -0
- package/src/mesh/wire.ts +96 -0
- package/src/messaging/delivery.ts +375 -0
- package/src/messaging/direct.ts +433 -0
- package/src/messaging/handlers.ts +14 -0
- package/src/messaging/inbox.ts +191 -0
- package/src/messaging/index.ts +5 -0
- package/src/messaging/notify.ts +117 -0
- package/src/plugin/claude.ts +148 -0
- package/src/plugin/index.ts +13 -0
- package/src/plugin/install.ts +416 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.ts +359 -0
- package/src/sessions/classify.ts +66 -0
- package/src/sessions/dump.ts +105 -0
- package/src/sessions/fork.ts +127 -0
- package/src/sessions/handlers.ts +158 -0
- package/src/sessions/harness.ts +167 -0
- package/src/sessions/index.ts +26 -0
- package/src/sessions/last-live.ts +111 -0
- package/src/sessions/processes.ts +413 -0
- package/src/sessions/registry.ts +785 -0
- package/src/sessions/search.ts +278 -0
- package/src/sessions/status.ts +209 -0
- package/src/sessions/terminals.ts +72 -0
- package/src/sessions/workspace.ts +140 -0
- package/src/topics/handlers.ts +42 -0
- package/src/topics/index.ts +2 -0
- package/src/topics/topics.ts +290 -0
- package/src/transcript/files.ts +201 -0
- package/src/transcript/fold.ts +833 -0
- package/src/transcript/index.ts +16 -0
- package/src/transcript/read.ts +82 -0
- package/src/transcript/tail.ts +195 -0
- package/src/transcript/transcripts.ts +162 -0
- package/src/translate/helper.ts +87 -0
- package/src/translate/index.ts +2 -0
- package/src/translate/translate.ts +127 -0
- package/src/transport/conn.ts +129 -0
- package/src/transport/dial.ts +65 -0
- package/src/transport/driver.ts +102 -0
- package/src/transport/entry.ts +39 -0
- package/src/transport/framing.ts +131 -0
- package/src/transport/index.ts +8 -0
- package/src/transport/listener.ts +39 -0
- package/src/transport/uds.ts +88 -0
- package/src/transport/ws.ts +170 -0
- package/src/upstream/events.ts +125 -0
- package/src/upstream/gateway.ts +275 -0
- package/src/upstream/index.ts +8 -0
- package/src/upstream/json.ts +81 -0
- package/src/upstream/requests.ts +234 -0
- package/src/upstream/stats.ts +99 -0
- package/src/upstream/status.ts +281 -0
- package/src/upstream/usage.ts +208 -0
- package/src/upstream/webhook.ts +141 -0
- package/src/version.ts +8 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Endpoint } from "@ccmsg/protocol";
|
|
2
|
+
import { MESH_VER, randomId } from "./keys.ts";
|
|
3
|
+
import { probeEndpoint, type ProbeBody } from "./wire.ts";
|
|
4
|
+
|
|
5
|
+
/** How long a probe may take to come back.
|
|
6
|
+
*
|
|
7
|
+
* An endpoint that does not answer is either asleep or misconfigured, and
|
|
8
|
+
* waiting longer tells the two apart no better. It bounds startup rather than
|
|
9
|
+
* deciding correctness: only the probe this instance sends itself decides
|
|
10
|
+
* anything. */
|
|
11
|
+
export const PROBE_TIMEOUT_MS = 3_000;
|
|
12
|
+
|
|
13
|
+
/** The configured endpoints do not describe this instance.
|
|
14
|
+
*
|
|
15
|
+
* Its own class so startup can refuse the same way a broken config does (§8.3,
|
|
16
|
+
* DV-Q9): `self` is a setting, and one that does not reach this process is a
|
|
17
|
+
* setting that is wrong. */
|
|
18
|
+
export class SelfEndpointError extends Error {
|
|
19
|
+
constructor(msg: string) {
|
|
20
|
+
super(msg);
|
|
21
|
+
this.name = "SelfEndpointError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What one round of probes found. */
|
|
26
|
+
export interface PeerReport {
|
|
27
|
+
/** The configured endpoints that turned out to be this instance — `self`,
|
|
28
|
+
* and any alias of it a peer list happens to name. They are not dialled: a
|
|
29
|
+
* link to ourselves is not a link. */
|
|
30
|
+
readonly ours: readonly Endpoint[];
|
|
31
|
+
/** The endpoints that did not answer. Recorded and not refused: a peer that
|
|
32
|
+
* is asleep is the normal state of this mesh (§7.1, DV-Q11). */
|
|
33
|
+
readonly unreachable: readonly Endpoint[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The probes in flight, and the endpoint each was sent to.
|
|
37
|
+
*
|
|
38
|
+
* Since `self` is configured (DR-0001 §2.7), this no longer settles an identity:
|
|
39
|
+
* it checks the setting. The probe to `self` has to come back here, which is
|
|
40
|
+
* what catches a `self` that names somebody else, and the rest of the list is
|
|
41
|
+
* probed to record what can be reached before anything is dialled.
|
|
42
|
+
*
|
|
43
|
+
* The table is destroyed when the run finishes: what the exercise leaves behind
|
|
44
|
+
* is the report and nothing else (§7.3). */
|
|
45
|
+
export class PeerProbe {
|
|
46
|
+
#sent = new Map<string, Endpoint>();
|
|
47
|
+
readonly #matched = new Set<Endpoint>();
|
|
48
|
+
|
|
49
|
+
/** A probe arrived here. Answering is unconditional and holds no state: the
|
|
50
|
+
* comparison is the sender's, and this instance is the sender for exactly one
|
|
51
|
+
* of the probes it is currently answering (§5.1). */
|
|
52
|
+
accept(token: string): void {
|
|
53
|
+
const sentTo = this.#sent.get(token);
|
|
54
|
+
if (sentTo !== undefined) this.#matched.add(sentTo);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Ask `self` and every peer who answers there, and refuse to start if the
|
|
58
|
+
* endpoint this instance calls its own is somebody else's. */
|
|
59
|
+
async verify(self: Endpoint, peers: readonly Endpoint[]): Promise<PeerReport> {
|
|
60
|
+
const targets = [...new Set([self, ...peers])];
|
|
61
|
+
this.#sent = new Map(targets.map((target) => [randomId(), target]));
|
|
62
|
+
const unreachable: Endpoint[] = [];
|
|
63
|
+
await Promise.all(
|
|
64
|
+
[...this.#sent].map(async ([token, target]) => {
|
|
65
|
+
if (!(await this.#probe(target, token))) unreachable.push(target);
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
const ours = [...this.#matched];
|
|
69
|
+
this.#sent = new Map();
|
|
70
|
+
this.#matched.clear();
|
|
71
|
+
if (!ours.includes(self)) {
|
|
72
|
+
throw new SelfEndpointError(
|
|
73
|
+
unreachable.includes(self)
|
|
74
|
+
? `self is ${self}, which did not answer this instance's probe`
|
|
75
|
+
: `self is ${self}, which answers as another instance`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return { ours, unreachable };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether the endpoint answered. What it answered does not matter: the
|
|
82
|
+
* comparison happens where the probe lands, not in its reply. */
|
|
83
|
+
async #probe(target: Endpoint, token: string): Promise<boolean> {
|
|
84
|
+
const body: ProbeBody = { ver: MESH_VER, token };
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(probeEndpoint(target), {
|
|
87
|
+
method: "POST",
|
|
88
|
+
// Closed after the one round trip it is: a probe is sent once at
|
|
89
|
+
// startup, and a pooled connection kept open for it would outlive the
|
|
90
|
+
// exercise and hold the listener at the far end.
|
|
91
|
+
headers: { "content-type": "application/json", connection: "close" },
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
94
|
+
});
|
|
95
|
+
return response.ok;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type InstanceId,
|
|
3
|
+
LAST_LIVE_RETENTION_MS,
|
|
4
|
+
type PeerInfo,
|
|
5
|
+
PLAIN_TOPICS,
|
|
6
|
+
type Sid,
|
|
7
|
+
TOPIC_ATTRIBUTES,
|
|
8
|
+
type Timestamp,
|
|
9
|
+
} from "@ccmsg/protocol";
|
|
10
|
+
import type { TopicValue } from "../topics/index.ts";
|
|
11
|
+
|
|
12
|
+
/** The topics a subscriber sees the whole cluster on.
|
|
13
|
+
*
|
|
14
|
+
* The per-instance whole is what makes a cluster view possible at all (§6.2):
|
|
15
|
+
* a frame replaces its own instance's entries and leaves every other
|
|
16
|
+
* instance's alone, so several instances can state the same topic name without
|
|
17
|
+
* colliding. A topic of any other granularity has no such rule and is not
|
|
18
|
+
* relayed — an `element` topic like `inbox` names one instance's topic while
|
|
19
|
+
* its value belongs to a session, and a frame of it carries no way to say
|
|
20
|
+
* whose it is. */
|
|
21
|
+
export const CLUSTER_TOPICS: readonly string[] = PLAIN_TOPICS.filter(
|
|
22
|
+
(topic) => TOPIC_ATTRIBUTES[topic].granularity === "per_instance_whole",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export function isClusterTopic(topic: string): boolean {
|
|
26
|
+
return CLUSTER_TOPICS.includes(topic);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RelayDeps {
|
|
30
|
+
/** Hand a relayed frame to this instance's own subscribers, under the
|
|
31
|
+
* instance that produced it (§7.4). */
|
|
32
|
+
readonly publish: (topic: string, data: unknown, instance: InstanceId) => void;
|
|
33
|
+
/** The clock, so a test can move the retention window without waiting it
|
|
34
|
+
* out. */
|
|
35
|
+
readonly now?: () => Timestamp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What the peers said, held on this instance so that losing a peer does not
|
|
39
|
+
* empty the cluster view (§7.5).
|
|
40
|
+
*
|
|
41
|
+
* Two things live here and nowhere else: the last whole value each instance
|
|
42
|
+
* stated per topic, and whether that instance can be reached right now. The
|
|
43
|
+
* suppression table of the topic mechanism is not the place for either — it
|
|
44
|
+
* is forgotten the moment a topic has no subscriber, while what a peer last
|
|
45
|
+
* said has to outlive both the subscription and the link.
|
|
46
|
+
*
|
|
47
|
+
* The retention window is read rather than swept: nothing here runs on a
|
|
48
|
+
* timer (M3), so a value past the window is dropped by the next read that
|
|
49
|
+
* would have returned it. */
|
|
50
|
+
export class Relay {
|
|
51
|
+
/** Per instance, the last whole value it stated per topic. */
|
|
52
|
+
readonly #held = new Map<InstanceId, Map<string, unknown>>();
|
|
53
|
+
/** The mark of §7.5: when this instance stopped being reachable. Absent
|
|
54
|
+
* while it is reachable. */
|
|
55
|
+
readonly #lostAt = new Map<InstanceId, Timestamp>();
|
|
56
|
+
|
|
57
|
+
constructor(private readonly deps: RelayDeps) {}
|
|
58
|
+
|
|
59
|
+
#now(): Timestamp {
|
|
60
|
+
return (this.deps.now ?? Date.now)();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A frame a peer pushed on a topic this instance relays.
|
|
64
|
+
*
|
|
65
|
+
* `instance` is the one that produced the value, which is not always the
|
|
66
|
+
* peer it arrived from: a mesh of three relays transitively, and the frame
|
|
67
|
+
* names its origin the whole way. Held under that origin, and passed on
|
|
68
|
+
* unchanged — recomputing it would put the same judgement in two places
|
|
69
|
+
* (§7.4). */
|
|
70
|
+
accept(instance: InstanceId, topic: string, data: unknown): void {
|
|
71
|
+
if (!isClusterTopic(topic)) return;
|
|
72
|
+
this.#sweep();
|
|
73
|
+
const held = this.#held.get(instance) ?? new Map<string, unknown>();
|
|
74
|
+
this.#held.set(instance, held);
|
|
75
|
+
held.set(topic, data);
|
|
76
|
+
this.deps.publish(topic, data, instance);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The link to this instance is gone. What it said is kept and marked,
|
|
80
|
+
* because dropping it would empty the view until the instance comes back
|
|
81
|
+
* and restates everything (§7.5). */
|
|
82
|
+
lost(instance: InstanceId): void {
|
|
83
|
+
if (!this.#lostAt.has(instance)) this.#lostAt.set(instance, this.#now());
|
|
84
|
+
this.#sweep();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The link is back. The mark goes, and what it said stands until the
|
|
88
|
+
* instance replaces it with the snapshot its subscriptions bring. */
|
|
89
|
+
restored(instance: InstanceId): void {
|
|
90
|
+
this.#lostAt.delete(instance);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether this instance is currently held as unreachable. */
|
|
94
|
+
unreachable(instance: InstanceId): boolean {
|
|
95
|
+
this.#sweep();
|
|
96
|
+
return this.#lostAt.has(instance);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The current value of a relayed topic, one entry per instance that has
|
|
100
|
+
* stated one. What a fresh local subscriber is handed beside this
|
|
101
|
+
* instance's own snapshot. */
|
|
102
|
+
snapshot(topic: string): readonly TopicValue[] {
|
|
103
|
+
this.#sweep();
|
|
104
|
+
const values: TopicValue[] = [];
|
|
105
|
+
for (const [instance, held] of this.#held) {
|
|
106
|
+
const data = held.get(topic);
|
|
107
|
+
if (data !== undefined) values.push({ instance, data });
|
|
108
|
+
}
|
|
109
|
+
return values;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Which instance a session belongs to, read from the `peers` values the
|
|
113
|
+
* cluster stated (§7.3).
|
|
114
|
+
*
|
|
115
|
+
* The row names its own instance rather than the one that relayed it, so a
|
|
116
|
+
* value that travelled through a third instance still points at the session's
|
|
117
|
+
* own. */
|
|
118
|
+
owner(sid: Sid): InstanceId | undefined {
|
|
119
|
+
this.#sweep();
|
|
120
|
+
for (const held of this.#held.values()) {
|
|
121
|
+
const value = held.get("peers") as { peers?: PeerInfo[] } | undefined;
|
|
122
|
+
const row = value?.peers?.find((peer) => peer.sid === sid);
|
|
123
|
+
if (row !== undefined) return row.instance;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Drop what an instance said once it has been gone for the retention
|
|
129
|
+
* window. The window is the contract's, shared with `last_live` and the
|
|
130
|
+
* inbox: past it, everything the value would point at is gone too
|
|
131
|
+
* (§7.5, DV-Q12). */
|
|
132
|
+
#sweep(): void {
|
|
133
|
+
const now = this.#now();
|
|
134
|
+
for (const [instance, since] of this.#lostAt) {
|
|
135
|
+
if (now - since <= LAST_LIVE_RETENTION_MS) continue;
|
|
136
|
+
this.#lostAt.delete(instance);
|
|
137
|
+
this.#held.delete(instance);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** What is held right now, so a test can state that a disconnected
|
|
142
|
+
* instance's value is still there and that a swept one is not. */
|
|
143
|
+
get retained(): { instances: number; marked: number } {
|
|
144
|
+
this.#sweep();
|
|
145
|
+
return { instances: this.#held.size, marked: this.#lostAt.size };
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/mesh/wire.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Endpoint } from "@ccmsg/protocol";
|
|
2
|
+
import type { MeshJwk } from "./keys.ts";
|
|
3
|
+
|
|
4
|
+
/** The paths an instance's endpoint URL stands in front of.
|
|
5
|
+
*
|
|
6
|
+
* An `Endpoint` is compared whole, path included (contract, `Endpoint`), so
|
|
7
|
+
* everything an instance serves hangs below it. That is also what keeps two
|
|
8
|
+
* instances sharing one origin apart: the key of `wss://h/a` is only ever
|
|
9
|
+
* fetched from below `/a`, so `wss://h/b` cannot answer for it and a proof made
|
|
10
|
+
* with b's key cannot pass as a's (mesh-peer-auth §6.3). The separation is the
|
|
11
|
+
* shape of the URLs rather than a rule written somewhere. */
|
|
12
|
+
const WS_PATH = "/ws";
|
|
13
|
+
const JWK_PATH = "/mesh/jwk/";
|
|
14
|
+
const PROBE_PATH = "/mesh/probe";
|
|
15
|
+
|
|
16
|
+
/** Where a peer's mesh link is dialled. */
|
|
17
|
+
export function wsEndpoint(endpoint: Endpoint): string {
|
|
18
|
+
return `${endpoint}${WS_PATH}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Where one connection's key is fetched, and the challenge for it left.
|
|
22
|
+
*
|
|
23
|
+
* `http` rather than `ws` because this is the second connection of §6, which
|
|
24
|
+
* carries one request and closes: the protocol asks that it be a connection of
|
|
25
|
+
* its own outside the one being authenticated, not that it be a WebSocket. */
|
|
26
|
+
export function jwkEndpoint(endpoint: Endpoint, kid: string): string {
|
|
27
|
+
return `${httpBase(endpoint)}${JWK_PATH}${encodeURIComponent(kid)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function probeEndpoint(endpoint: Endpoint): string {
|
|
31
|
+
return `${httpBase(endpoint)}${PROBE_PATH}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The `kid` a request names, or nothing when the path is not a key request. */
|
|
35
|
+
export function kidOfPath(pathname: string, self: Endpoint): string | undefined {
|
|
36
|
+
const prefix = `${new URL(self).pathname.replace(/\/$/, "")}${JWK_PATH}`;
|
|
37
|
+
if (!pathname.startsWith(prefix)) return undefined;
|
|
38
|
+
const kid = decodeURIComponent(pathname.slice(prefix.length));
|
|
39
|
+
return kid === "" ? undefined : kid;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isProbePath(pathname: string, self: Endpoint): boolean {
|
|
43
|
+
return pathname === `${new URL(self).pathname.replace(/\/$/, "")}${PROBE_PATH}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The same authority and path, reached over HTTP. A `ws` URL and the `http`
|
|
47
|
+
* one beside it are one server; the scheme differs and nothing else does. */
|
|
48
|
+
function httpBase(endpoint: Endpoint): string {
|
|
49
|
+
const url = new URL(endpoint);
|
|
50
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
51
|
+
return url.href.replace(/\/$/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The subprotocol a dialling instance offers.
|
|
55
|
+
*
|
|
56
|
+
* A peer is let through the handshake on this marker alone and proves who it is
|
|
57
|
+
* afterwards, where a claim can actually be checked: an unproven mesh
|
|
58
|
+
* connection is anonymous and can reach only the ops that need no identity at
|
|
59
|
+
* all. What the person\'s entry offers instead is a token bound to a passkey
|
|
60
|
+
* (DR-0001), which is theirs and not an instance\'s to hold. */
|
|
61
|
+
export const MESH_PROTOCOL = "ccmsg.mesh";
|
|
62
|
+
|
|
63
|
+
/** A frame on the mesh link that is not an op.
|
|
64
|
+
*
|
|
65
|
+
* The contract's op vocabulary holds no mesh ops — an op crosses instances by
|
|
66
|
+
* carrying the envelope's mesh fields (contract, `Plane`) — so the handshake's
|
|
67
|
+
* own traffic is spelled apart from it, under a key no request has. */
|
|
68
|
+
export type MeshFrame =
|
|
69
|
+
| { readonly mesh: "proof"; readonly jws: string }
|
|
70
|
+
| { readonly mesh: "ping" }
|
|
71
|
+
| { readonly mesh: "pong" };
|
|
72
|
+
|
|
73
|
+
export function meshFrameOf(frame: unknown): MeshFrame | undefined {
|
|
74
|
+
if (typeof frame !== "object" || frame === null) return undefined;
|
|
75
|
+
const kind = (frame as Record<string, unknown>)["mesh"];
|
|
76
|
+
if (kind === "ping" || kind === "pong") return { mesh: kind };
|
|
77
|
+
if (kind !== "proof") return undefined;
|
|
78
|
+
const jws = (frame as Record<string, unknown>)["jws"];
|
|
79
|
+
return typeof jws === "string" ? { mesh: "proof", jws } : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What a key request carries: the challenge the proof must sign (§6). */
|
|
83
|
+
export interface JwkRequest {
|
|
84
|
+
readonly ver: number;
|
|
85
|
+
readonly challenge: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface JwkResponse {
|
|
89
|
+
readonly jwk: MeshJwk;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What a self-identification probe carries (mesh-self-identification §5.1). */
|
|
93
|
+
export interface ProbeBody {
|
|
94
|
+
readonly ver: number;
|
|
95
|
+
readonly token: string;
|
|
96
|
+
}
|