@chances-ai/wire 24.0.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/dist/rpc/acp/adapter.d.ts +32 -0
- package/dist/rpc/acp/adapter.d.ts.map +1 -0
- package/dist/rpc/acp/adapter.js +185 -0
- package/dist/rpc/acp/adapter.js.map +1 -0
- package/dist/rpc/acp/engine-driver.d.ts +128 -0
- package/dist/rpc/acp/engine-driver.d.ts.map +1 -0
- package/dist/rpc/acp/engine-driver.js +550 -0
- package/dist/rpc/acp/engine-driver.js.map +1 -0
- package/dist/rpc/acp/event-map.d.ts +22 -0
- package/dist/rpc/acp/event-map.d.ts.map +1 -0
- package/dist/rpc/acp/event-map.js +205 -0
- package/dist/rpc/acp/event-map.js.map +1 -0
- package/dist/rpc/acp/load-sdk.d.ts +3 -0
- package/dist/rpc/acp/load-sdk.d.ts.map +1 -0
- package/dist/rpc/acp/load-sdk.js +24 -0
- package/dist/rpc/acp/load-sdk.js.map +1 -0
- package/dist/rpc/acp/workspace-query.d.ts +41 -0
- package/dist/rpc/acp/workspace-query.d.ts.map +1 -0
- package/dist/rpc/acp/workspace-query.js +89 -0
- package/dist/rpc/acp/workspace-query.js.map +1 -0
- package/dist/rpc/driver.d.ts +42 -0
- package/dist/rpc/driver.d.ts.map +1 -0
- package/dist/rpc/driver.js +7 -0
- package/dist/rpc/driver.js.map +1 -0
- package/dist/rpc/event-map.d.ts +8 -0
- package/dist/rpc/event-map.d.ts.map +1 -0
- package/dist/rpc/event-map.js +91 -0
- package/dist/rpc/event-map.js.map +1 -0
- package/dist/rpc/index.d.ts +13 -0
- package/dist/rpc/index.d.ts.map +1 -0
- package/dist/rpc/index.js +18 -0
- package/dist/rpc/index.js.map +1 -0
- package/dist/rpc/lines.d.ts +2 -0
- package/dist/rpc/lines.d.ts.map +1 -0
- package/dist/rpc/lines.js +24 -0
- package/dist/rpc/lines.js.map +1 -0
- package/dist/rpc/protocol.d.ts +315 -0
- package/dist/rpc/protocol.d.ts.map +1 -0
- package/dist/rpc/protocol.js +70 -0
- package/dist/rpc/protocol.js.map +1 -0
- package/dist/rpc/rpc-server.d.ts +56 -0
- package/dist/rpc/rpc-server.d.ts.map +1 -0
- package/dist/rpc/rpc-server.js +305 -0
- package/dist/rpc/rpc-server.js.map +1 -0
- package/dist/rpc/stdout-guard.d.ts +5 -0
- package/dist/rpc/stdout-guard.d.ts.map +1 -0
- package/dist/rpc/stdout-guard.js +31 -0
- package/dist/rpc/stdout-guard.js.map +1 -0
- package/dist/rpc/writer.d.ts +34 -0
- package/dist/rpc/writer.d.ts.map +1 -0
- package/dist/rpc/writer.js +85 -0
- package/dist/rpc/writer.js.map +1 -0
- package/dist/serve/acp-session-host.d.ts +120 -0
- package/dist/serve/acp-session-host.d.ts.map +1 -0
- package/dist/serve/acp-session-host.js +276 -0
- package/dist/serve/acp-session-host.js.map +1 -0
- package/dist/serve/auth.d.ts +21 -0
- package/dist/serve/auth.d.ts.map +1 -0
- package/dist/serve/auth.js +58 -0
- package/dist/serve/auth.js.map +1 -0
- package/dist/serve/highlight.d.ts +25 -0
- package/dist/serve/highlight.d.ts.map +1 -0
- package/dist/serve/highlight.js +28 -0
- package/dist/serve/highlight.js.map +1 -0
- package/dist/serve/index.d.ts +14 -0
- package/dist/serve/index.d.ts.map +1 -0
- package/dist/serve/index.js +23 -0
- package/dist/serve/index.js.map +1 -0
- package/dist/serve/pairing.d.ts +25 -0
- package/dist/serve/pairing.d.ts.map +1 -0
- package/dist/serve/pairing.js +10 -0
- package/dist/serve/pairing.js.map +1 -0
- package/dist/serve/relay-frames.d.ts +29 -0
- package/dist/serve/relay-frames.d.ts.map +1 -0
- package/dist/serve/relay-frames.js +54 -0
- package/dist/serve/relay-frames.js.map +1 -0
- package/dist/serve/relay.d.ts +146 -0
- package/dist/serve/relay.d.ts.map +1 -0
- package/dist/serve/relay.js +475 -0
- package/dist/serve/relay.js.map +1 -0
- package/dist/serve/replay-hub.d.ts +102 -0
- package/dist/serve/replay-hub.d.ts.map +1 -0
- package/dist/serve/replay-hub.js +176 -0
- package/dist/serve/replay-hub.js.map +1 -0
- package/dist/serve/tls.d.ts +20 -0
- package/dist/serve/tls.d.ts.map +1 -0
- package/dist/serve/tls.js +64 -0
- package/dist/serve/tls.js.map +1 -0
- package/dist/serve/ws-transport.d.ts +64 -0
- package/dist/serve/ws-transport.d.ts.map +1 -0
- package/dist/serve/ws-transport.js +92 -0
- package/dist/serve/ws-transport.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v20 M3 / docs/6.4a §3; v21 M4 lease / docs/6.5 §5) `AcpSessionHost` — the ACP
|
|
3
|
+
* twin of the bespoke session host. ONE persistent engine session per process,
|
|
4
|
+
* sockets attach/detach over a seq-stamping `ReplayHub` fan-out; the inner engine
|
|
5
|
+
* is the `AcpEngineDriver` (ACP JSON-RPC wire).
|
|
6
|
+
*
|
|
7
|
+
* Two deliberate differences from the M2 design:
|
|
8
|
+
* 1. **State source.** The reconnect snapshot (`busy` / `pendingPermissionIds`)
|
|
9
|
+
* is read straight off the driver (authoritative). The `ReplayHub` stays a
|
|
10
|
+
* pure payload-agnostic seq/replay/fan-out layer — it does NOT parse the
|
|
11
|
+
* outbound bytes for session state (a byte-snoop lived there in the
|
|
12
|
+
* chances-rpc era; removed with the M3 hard cutover, since ACP frames' first
|
|
13
|
+
* key is `jsonrpc`/`rseq`, never `type`).
|
|
14
|
+
* 2. **No `ready` re-send.** ACP is client-initiated: a fresh client sends
|
|
15
|
+
* `initialize`/`session/new` and the driver replies; a cold-reloaded client
|
|
16
|
+
* re-initializes (client-core, c4).
|
|
17
|
+
*
|
|
18
|
+
* **Controller lease (v21 M4 §5).** Disabled by default ⇒ fan-out: any paired
|
|
19
|
+
* device drives, the engine serializes turns via `AGENT_BUSY` (the M2 model).
|
|
20
|
+
* When enabled, this host gates every inbound command by its source clientId —
|
|
21
|
+
* only the lease HOLDER may drive (`session/prompt`/`cancel`), answer a
|
|
22
|
+
* permission, or change the model; everyone else is a read-only VIEWER and gets a
|
|
23
|
+
* typed `not_controller`. `_chances/unstable/take_control` hands control over
|
|
24
|
+
* (any authed client may claim it — single-user multi-device) and bumps
|
|
25
|
+
* `controlEpoch`; the new holder + epoch are broadcast as a
|
|
26
|
+
* `_chances/unstable/control` notification. The gate lives HERE (it needs the
|
|
27
|
+
* socket→clientId map) so the driver stays a protocol-pure turn runner.
|
|
28
|
+
*
|
|
29
|
+
* **Threat model (codex M4-review).** The lease is COORDINATION among one user's
|
|
30
|
+
* equally-authed devices, NOT an authorization boundary. The security boundary is
|
|
31
|
+
* the pairing token (every connection is already token-authed); `clientId` is a
|
|
32
|
+
* self-reported coordination tag, not a trusted identity. A device could spoof
|
|
33
|
+
* another's clientId to "impersonate" the holder — but it gains nothing
|
|
34
|
+
* `take_control` doesn't already grant (any authed client may claim control), so
|
|
35
|
+
* there is no privilege escalation among your own devices. A truly-restricted
|
|
36
|
+
* viewer (read-only access shared with a THIRD party) needs server-issued identity
|
|
37
|
+
* and belongs to the multi-tenant Team era — out of scope here.
|
|
38
|
+
*
|
|
39
|
+
* The seq/replay layer (`ReplayHub`) is reused UNCHANGED — it stamps `rseq` on
|
|
40
|
+
* the serialized ACP JSON-RPC bytes (payload-agnostic, docs/6.2 §3.2).
|
|
41
|
+
*/
|
|
42
|
+
import { AcpEngineDriver, dispatchWorkspaceQuery, hostSupportsWorkspaceQueries, isWorkspaceQueryMethod, } from "../rpc/index.js";
|
|
43
|
+
import { ReplayHub } from "./replay-hub.js";
|
|
44
|
+
import { MessageQueue } from "./ws-transport.js";
|
|
45
|
+
/** Lease wire methods (chances extensions; a plain ACP client never sends them). */
|
|
46
|
+
const METHOD_TAKE_CONTROL = "_chances/unstable/take_control";
|
|
47
|
+
const METHOD_CONTROL_STATE = "_chances/unstable/control";
|
|
48
|
+
/** JSON-RPC error for a viewer's privileged command while a lease is held. */
|
|
49
|
+
const NOT_CONTROLLER = -32010;
|
|
50
|
+
/** (v23 M5) Bound a read-only workspace query so a hung git/fs can't park a
|
|
51
|
+
* fire-and-forget handler forever. */
|
|
52
|
+
const QUERY_TIMEOUT_MS = 10_000;
|
|
53
|
+
export class AcpSessionHost {
|
|
54
|
+
hub;
|
|
55
|
+
queue = new MessageQueue();
|
|
56
|
+
driver;
|
|
57
|
+
host;
|
|
58
|
+
done;
|
|
59
|
+
epochCounter = 0;
|
|
60
|
+
shuttingDown = false;
|
|
61
|
+
// -- controller lease (§5) --
|
|
62
|
+
leaseEnabled;
|
|
63
|
+
holder = null;
|
|
64
|
+
controlEpoch = 0;
|
|
65
|
+
/** clientId → socket, for TARGETED lease replies (not_controller / ack). */
|
|
66
|
+
socketsByClient = new Map();
|
|
67
|
+
constructor(opts) {
|
|
68
|
+
this.hub = new ReplayHub(opts.ring);
|
|
69
|
+
this.host = opts.host;
|
|
70
|
+
this.leaseEnabled = opts.controllerLease ?? false;
|
|
71
|
+
this.driver = new AcpEngineDriver({
|
|
72
|
+
host: opts.host,
|
|
73
|
+
sink: this.hub,
|
|
74
|
+
agent: opts.agent,
|
|
75
|
+
autoApprove: opts.autoApprove ?? false,
|
|
76
|
+
logSink: opts.logSink,
|
|
77
|
+
// (v23 M5) Advertise read-only workspace queries when the host supports
|
|
78
|
+
// them — this host intercepts + answers them off-ring (the driver itself
|
|
79
|
+
// never handles them), so the capability is honest only under the relay.
|
|
80
|
+
workspaceQueries: hostSupportsWorkspaceQueries(opts.host),
|
|
81
|
+
});
|
|
82
|
+
// Build + drive the one session immediately; `run` reads the persistent
|
|
83
|
+
// queue until it ends (process shutdown). Fire-and-forget; run catches.
|
|
84
|
+
this.done = this.driver.run(this.queue).catch(() => 1);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Attach a socket, synchronously replaying everything since `lastSeq` then
|
|
88
|
+
* going live (same disjoint-and-ordered guarantee as the M2 host). `clientId`
|
|
89
|
+
* (from the `?client_id=` connect query) attributes this socket for the lease.
|
|
90
|
+
* Wire order: `relay_welcome` → replayed gap → re-sent open permissions → [live].
|
|
91
|
+
*/
|
|
92
|
+
attach(socket, lastSeq, clientId) {
|
|
93
|
+
const epoch = ++this.epochCounter;
|
|
94
|
+
const slice = this.hub.replaySince(lastSeq);
|
|
95
|
+
const welcome = {
|
|
96
|
+
type: "relay_welcome",
|
|
97
|
+
epoch,
|
|
98
|
+
headSeq: this.hub.headSeq,
|
|
99
|
+
reset: slice.reset,
|
|
100
|
+
busy: this.driver.busy,
|
|
101
|
+
pendingPermissionIds: this.driver.pendingPermissionIds(),
|
|
102
|
+
lease: this.leaseEnabled,
|
|
103
|
+
holder: this.holder,
|
|
104
|
+
controlEpoch: this.controlEpoch,
|
|
105
|
+
};
|
|
106
|
+
if (!this.hub.sendTo(socket, frameLine(welcome)))
|
|
107
|
+
return deadAttachment(epoch);
|
|
108
|
+
for (const line of slice.frames) {
|
|
109
|
+
if (!this.hub.sendTo(socket, line))
|
|
110
|
+
return deadAttachment(epoch);
|
|
111
|
+
}
|
|
112
|
+
// Re-send still-open permission asks (the ring may have evicted them); the
|
|
113
|
+
// client de-dups by permission id, so one also present in the replay is a no-op.
|
|
114
|
+
for (const open of this.driver.openPermissionFrames()) {
|
|
115
|
+
if (!this.hub.sendTo(socket, open))
|
|
116
|
+
return deadAttachment(epoch);
|
|
117
|
+
}
|
|
118
|
+
this.hub.addSocket(socket);
|
|
119
|
+
if (clientId)
|
|
120
|
+
this.socketsByClient.set(clientId, socket);
|
|
121
|
+
return {
|
|
122
|
+
epoch,
|
|
123
|
+
detach: () => {
|
|
124
|
+
this.hub.removeSocket(socket);
|
|
125
|
+
// Only drop the mapping if it still points at THIS socket — a reconnect
|
|
126
|
+
// with the same clientId installs a new socket we must not evict. The
|
|
127
|
+
// holder is intentionally retained: the same client reconnecting resumes
|
|
128
|
+
// control; another device can always `take_control` if it left for good.
|
|
129
|
+
if (clientId && this.socketsByClient.get(clientId) === socket)
|
|
130
|
+
this.socketsByClient.delete(clientId);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* One inbound text message (ACP JSON-RPC). Split LF-batched lines; each line
|
|
136
|
+
* passes the lease {@link gate} (a no-op when the lease is disabled) before
|
|
137
|
+
* reaching the ONE persistent queue. `fromClientId` (the attach's clientId)
|
|
138
|
+
* attributes the command to a client for the lease.
|
|
139
|
+
*/
|
|
140
|
+
onMessage(text, fromClientId) {
|
|
141
|
+
for (const part of text.split("\n")) {
|
|
142
|
+
const line = part.trim();
|
|
143
|
+
if (!line)
|
|
144
|
+
continue;
|
|
145
|
+
// (v23 M5) Read-only workspace queries (file tree / read / git) are
|
|
146
|
+
// intercepted BEFORE the lease gate — they are non-privileged, so a viewer
|
|
147
|
+
// may browse — and answered targeted + off-ring (never enqueued, never
|
|
148
|
+
// stamped into the replay ring; a big `read_file` must not fan out to every
|
|
149
|
+
// device or get replayed on reconnect).
|
|
150
|
+
const msg = tryParseMessage(line);
|
|
151
|
+
if (msg && typeof msg.method === "string" && isWorkspaceQueryMethod(msg.method)) {
|
|
152
|
+
void this.handleQuery(msg, fromClientId);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (this.gate(line, fromClientId))
|
|
156
|
+
this.queue.push(line);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Answer one read-only workspace query: dispatch to the host's query
|
|
160
|
+
* primitives and `replyTo` the originating socket only (off-ring). Needs an id
|
|
161
|
+
* (to respond) and a clientId (to target); a query missing either is dropped
|
|
162
|
+
* (a conformant client always sends both). */
|
|
163
|
+
async handleQuery(msg, fromClientId) {
|
|
164
|
+
if (msg.id === undefined || fromClientId === undefined)
|
|
165
|
+
return;
|
|
166
|
+
const outcome = await dispatchWorkspaceQuery(this.host, msg.method, msg.params, AbortSignal.timeout(QUERY_TIMEOUT_MS));
|
|
167
|
+
this.replyTo(fromClientId, outcome.error ? { jsonrpc: "2.0", id: msg.id, error: outcome.error } : { jsonrpc: "2.0", id: msg.id, result: outcome.result });
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Controller-lease gate. Returns true ⇒ the line may proceed to the driver;
|
|
171
|
+
* false ⇒ the host handled it (a handoff) or rejected it (a viewer's privileged
|
|
172
|
+
* command — a `not_controller` reply was sent to the source). A no-op (always
|
|
173
|
+
* true) when the lease is disabled.
|
|
174
|
+
*/
|
|
175
|
+
gate(line, fromClientId) {
|
|
176
|
+
if (!this.leaseEnabled)
|
|
177
|
+
return true; // default: fan-out, anyone drives
|
|
178
|
+
const msg = tryParseMessage(line);
|
|
179
|
+
if (!msg)
|
|
180
|
+
return true; // unparseable → let the driver reject it
|
|
181
|
+
// Handoff: any authed client may take control (single-user multi-device).
|
|
182
|
+
if (msg.method === METHOD_TAKE_CONTROL) {
|
|
183
|
+
if (fromClientId) {
|
|
184
|
+
this.holder = fromClientId;
|
|
185
|
+
this.controlEpoch++;
|
|
186
|
+
this.broadcastControlState();
|
|
187
|
+
if (msg.id !== undefined) {
|
|
188
|
+
this.replyTo(fromClientId, { jsonrpc: "2.0", id: msg.id, result: { holder: this.holder, controlEpoch: this.controlEpoch } });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false; // host-handled; never a driver command
|
|
192
|
+
}
|
|
193
|
+
if (!isPrivileged(msg))
|
|
194
|
+
return true; // read-only (initialize/get_state/ping) → viewers allowed
|
|
195
|
+
// Privileged (drive a turn / answer a permission / change model). Auto-claim
|
|
196
|
+
// an unheld lease for the first driver, then enforce the holder.
|
|
197
|
+
if (this.holder === null && fromClientId) {
|
|
198
|
+
this.holder = fromClientId;
|
|
199
|
+
this.broadcastControlState();
|
|
200
|
+
}
|
|
201
|
+
if (fromClientId === undefined || fromClientId !== this.holder) {
|
|
202
|
+
if (fromClientId !== undefined && msg.id !== undefined) {
|
|
203
|
+
this.replyTo(fromClientId, {
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
id: msg.id,
|
|
206
|
+
error: { code: NOT_CONTROLLER, message: "not the controller — a lease is active; take control first" },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return false; // a stale command from a former holder lands here too (id ≠ holder)
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
/** Broadcast the current lease state to every socket (stamped + fanned out). */
|
|
214
|
+
broadcastControlState() {
|
|
215
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", method: METHOD_CONTROL_STATE, params: { lease: this.leaseEnabled, holder: this.holder, controlEpoch: this.controlEpoch } }) + "\n";
|
|
216
|
+
this.hub.write(frame);
|
|
217
|
+
}
|
|
218
|
+
/** Send a single frame to ONE client (targeted; not fanned out, not stamped). */
|
|
219
|
+
replyTo(clientId, obj) {
|
|
220
|
+
const socket = this.socketsByClient.get(clientId);
|
|
221
|
+
if (socket)
|
|
222
|
+
this.hub.sendTo(socket, JSON.stringify(obj) + "\n");
|
|
223
|
+
}
|
|
224
|
+
get headSeq() {
|
|
225
|
+
return this.hub.headSeq;
|
|
226
|
+
}
|
|
227
|
+
get socketCount() {
|
|
228
|
+
return this.hub.socketCount;
|
|
229
|
+
}
|
|
230
|
+
get busy() {
|
|
231
|
+
return this.driver.busy;
|
|
232
|
+
}
|
|
233
|
+
/** Current lease holder (clientId) or null. Exposed for tests/diagnostics. */
|
|
234
|
+
get controllerHolder() {
|
|
235
|
+
return this.holder;
|
|
236
|
+
}
|
|
237
|
+
/** End the persistent queue → `run` returns → graceful driver shutdown. */
|
|
238
|
+
async shutdown() {
|
|
239
|
+
if (!this.shuttingDown) {
|
|
240
|
+
this.shuttingDown = true;
|
|
241
|
+
this.queue.end();
|
|
242
|
+
}
|
|
243
|
+
return this.done;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function frameLine(frame) {
|
|
247
|
+
return JSON.stringify(frame) + "\n";
|
|
248
|
+
}
|
|
249
|
+
function deadAttachment(epoch) {
|
|
250
|
+
return { epoch, detach: () => { } };
|
|
251
|
+
}
|
|
252
|
+
function tryParseMessage(line) {
|
|
253
|
+
try {
|
|
254
|
+
const m = JSON.parse(line);
|
|
255
|
+
return typeof m === "object" && m !== null ? m : null;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** A command that DRIVES the session (only the lease holder may send it): drive a
|
|
262
|
+
* turn, change the model/options, or ANSWER a permission (a JSON-RPC response —
|
|
263
|
+
* no method, an id, and a result/error). Read-only methods
|
|
264
|
+
* (initialize/session.new/get_state/get_models/ping) are NOT privileged so a
|
|
265
|
+
* viewer can still connect + observe. */
|
|
266
|
+
function isPrivileged(msg) {
|
|
267
|
+
const m = msg.method;
|
|
268
|
+
if (m === "session/prompt" || m === "session/cancel")
|
|
269
|
+
return true;
|
|
270
|
+
if (m === "_chances/unstable/set_model" || m === "_chances/unstable/set_options")
|
|
271
|
+
return true;
|
|
272
|
+
if (m === undefined && msg.id !== undefined && ("result" in msg || "error" in msg))
|
|
273
|
+
return true;
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=acp-session-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acp-session-host.js","sourceRoot":"","sources":["../../src/serve/acp-session-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
2
|
+
/** Header carrying the pairing token (non-browser clients). Lowercased — Node
|
|
3
|
+
* normalizes incoming header names to lowercase. */
|
|
4
|
+
export declare const PAIRING_HEADER = "x-chances-pair";
|
|
5
|
+
/** Mint a fresh 256-bit pairing token (64 lowercase hex chars). */
|
|
6
|
+
export declare function mintPairingToken(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Pull a pairing token off a WS upgrade request: the `X-Chances-Pair` header
|
|
9
|
+
* first (non-browser), else the `?token=` query param (browser fallback).
|
|
10
|
+
* `baseUrl` is the relay's OWN bound base (the `Host` header is not trusted —
|
|
11
|
+
* consistent with `requestUrl`). Returns undefined when neither carries one.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractToken(reqUrl: string | undefined, headers: IncomingHttpHeaders, baseUrl: string): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Constant-time token check. Fail-closed: an absent/empty/length-mismatched/
|
|
16
|
+
* wrong token all return false. (`timingSafeEqual` THROWS on unequal-length
|
|
17
|
+
* buffers, so the length guard is required; the token length is fixed and public,
|
|
18
|
+
* not a secret, so an early length return leaks nothing useful.)
|
|
19
|
+
*/
|
|
20
|
+
export declare function checkToken(provided: string | undefined, expected: string): boolean;
|
|
21
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/serve/auth.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD;qDACqD;AACrD,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,mEAAmE;AACnE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS1H;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMlF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v21 M4 / docs/6.5 §2) Pairing-token auth primitives for the relay edge.
|
|
3
|
+
*
|
|
4
|
+
* Pure + stateless: minting/storage live in the CLI (they need the vault +
|
|
5
|
+
* runtime); the relay only CHECKS a token at the WS `upgrade` edge — fail-closed,
|
|
6
|
+
* BEFORE a socket ever reaches `AcpSessionHost`. Loopback needs no token (the
|
|
7
|
+
* trust boundary is "any local process is you"); a non-loopback bind REQUIRES one
|
|
8
|
+
* (enforced in Stage 2 together with TLS, so a token never crosses the wire in
|
|
9
|
+
* cleartext).
|
|
10
|
+
*
|
|
11
|
+
* Token shape mirrors goose's `X-Secret-Key` (`crates/goose-server/src/auth.rs`):
|
|
12
|
+
* a 256-bit random hex, accepted from the `X-Chances-Pair` header OR the `?token=`
|
|
13
|
+
* connect-URL query (a browser WebSocket can't set headers — the same reason M2
|
|
14
|
+
* put the replay cursor in the query). Compared constant-time (`timingSafeEqual`)
|
|
15
|
+
* to blunt token-probing timing attacks.
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
18
|
+
/** Header carrying the pairing token (non-browser clients). Lowercased — Node
|
|
19
|
+
* normalizes incoming header names to lowercase. */
|
|
20
|
+
export const PAIRING_HEADER = "x-chances-pair";
|
|
21
|
+
/** Mint a fresh 256-bit pairing token (64 lowercase hex chars). */
|
|
22
|
+
export function mintPairingToken() {
|
|
23
|
+
return randomBytes(32).toString("hex");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pull a pairing token off a WS upgrade request: the `X-Chances-Pair` header
|
|
27
|
+
* first (non-browser), else the `?token=` query param (browser fallback).
|
|
28
|
+
* `baseUrl` is the relay's OWN bound base (the `Host` header is not trusted —
|
|
29
|
+
* consistent with `requestUrl`). Returns undefined when neither carries one.
|
|
30
|
+
*/
|
|
31
|
+
export function extractToken(reqUrl, headers, baseUrl) {
|
|
32
|
+
const h = headers[PAIRING_HEADER];
|
|
33
|
+
const headerVal = Array.isArray(h) ? h[0] : h;
|
|
34
|
+
if (headerVal)
|
|
35
|
+
return headerVal;
|
|
36
|
+
try {
|
|
37
|
+
return new URL(reqUrl ?? "/", baseUrl).searchParams.get("token") ?? undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined; // malformed URL → no token
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Constant-time token check. Fail-closed: an absent/empty/length-mismatched/
|
|
45
|
+
* wrong token all return false. (`timingSafeEqual` THROWS on unequal-length
|
|
46
|
+
* buffers, so the length guard is required; the token length is fixed and public,
|
|
47
|
+
* not a secret, so an early length return leaks nothing useful.)
|
|
48
|
+
*/
|
|
49
|
+
export function checkToken(provided, expected) {
|
|
50
|
+
if (!provided || !expected)
|
|
51
|
+
return false;
|
|
52
|
+
const a = Buffer.from(provided);
|
|
53
|
+
const b = Buffer.from(expected);
|
|
54
|
+
if (a.length !== b.length)
|
|
55
|
+
return false;
|
|
56
|
+
return timingSafeEqual(a, b);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/serve/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG3D;qDACqD;AACrD,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C,mEAAmE;AACnE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAA0B,EAAE,OAA4B,EAAE,OAAe;IACpG,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,2BAA2B;IAC/C,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,QAA4B,EAAE,QAAgB;IACvE,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v17 M0 / docs/6.1 §1.4 res #2) Server-side syntax highlighting. The relay
|
|
3
|
+
* reuses ui-core's PURE `highlightToSegments`, so the wire can carry themed
|
|
4
|
+
* `Segment[]` and NO client ships a highlighter (shiki is dropped): the TUI,
|
|
5
|
+
* web, desktop, and mobile all render byte-identical segments produced by the
|
|
6
|
+
* same function. This is the load-bearing reuse that makes res #2 work.
|
|
7
|
+
*
|
|
8
|
+
* M0 wires the SEAM (a server-callable highlighter + golden parity test); the
|
|
9
|
+
* full frame-emit path — attaching segments to code/markdown frames on the
|
|
10
|
+
* `chances-rpc` wire — lands with the web renderer in M1.
|
|
11
|
+
*/
|
|
12
|
+
import { type Segment } from "@chances-ai/ui-core";
|
|
13
|
+
export type { Segment };
|
|
14
|
+
/**
|
|
15
|
+
* Highlight `code` as `lang` into themed segments for the wire. Thin pass-through
|
|
16
|
+
* to ui-core's `highlightToSegments` (kept as a named server entry so M1 can wrap
|
|
17
|
+
* it with frame envelopes without callers reaching into ui-core directly).
|
|
18
|
+
* Unknown/absent language or any highlighter error degrades to a single `plain`
|
|
19
|
+
* segment — never throws.
|
|
20
|
+
*/
|
|
21
|
+
export declare function highlightForWire(code: string, lang: string | undefined): Segment[];
|
|
22
|
+
/** Whether a language label resolves to a registered grammar (so the relay can
|
|
23
|
+
* decide to highlight vs. ship plain). Mirrors the TUI's gate. */
|
|
24
|
+
export declare function canHighlight(lang: string | undefined): boolean;
|
|
25
|
+
//# sourceMappingURL=highlight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../../src/serve/highlight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAwC,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEzF,YAAY,EAAE,OAAO,EAAE,CAAC;AAExB;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,EAAE,CAElF;AAED;mEACmE;AACnE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE9D"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v17 M0 / docs/6.1 §1.4 res #2) Server-side syntax highlighting. The relay
|
|
3
|
+
* reuses ui-core's PURE `highlightToSegments`, so the wire can carry themed
|
|
4
|
+
* `Segment[]` and NO client ships a highlighter (shiki is dropped): the TUI,
|
|
5
|
+
* web, desktop, and mobile all render byte-identical segments produced by the
|
|
6
|
+
* same function. This is the load-bearing reuse that makes res #2 work.
|
|
7
|
+
*
|
|
8
|
+
* M0 wires the SEAM (a server-callable highlighter + golden parity test); the
|
|
9
|
+
* full frame-emit path — attaching segments to code/markdown frames on the
|
|
10
|
+
* `chances-rpc` wire — lands with the web renderer in M1.
|
|
11
|
+
*/
|
|
12
|
+
import { highlightToSegments, resolveLanguage } from "@chances-ai/ui-core";
|
|
13
|
+
/**
|
|
14
|
+
* Highlight `code` as `lang` into themed segments for the wire. Thin pass-through
|
|
15
|
+
* to ui-core's `highlightToSegments` (kept as a named server entry so M1 can wrap
|
|
16
|
+
* it with frame envelopes without callers reaching into ui-core directly).
|
|
17
|
+
* Unknown/absent language or any highlighter error degrades to a single `plain`
|
|
18
|
+
* segment — never throws.
|
|
19
|
+
*/
|
|
20
|
+
export function highlightForWire(code, lang) {
|
|
21
|
+
return highlightToSegments(code, lang);
|
|
22
|
+
}
|
|
23
|
+
/** Whether a language label resolves to a registered grammar (so the relay can
|
|
24
|
+
* decide to highlight vs. ship plain). Mirrors the TUI's gate. */
|
|
25
|
+
export function canHighlight(lang) {
|
|
26
|
+
return resolveLanguage(lang) !== null;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=highlight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.js","sourceRoot":"","sources":["../../src/serve/highlight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAIzF;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAAwB;IACrE,OAAO,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC;AAED;mEACmE;AACnE,MAAM,UAAU,YAAY,CAAC,IAAwB;IACnD,OAAO,eAAe,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chances-ai/serve` — the thin local relay exposing the engine to web +
|
|
3
|
+
* mobile clients (docs/6.1 §3.1 / 6.4a M3). A default-loopback HTTP server with
|
|
4
|
+
* `/health` + a persistent WS `/acp` ACP control channel (replay/reconnect) +
|
|
5
|
+
* static SPA serving. No agent logic lives here — the engine is reached through
|
|
6
|
+
* the `@chances-ai/rpc` `EngineHost` seam (the `AcpEngineDriver` projection).
|
|
7
|
+
*/
|
|
8
|
+
export { LOOPBACK, DEFAULT_PORT, type BindOptions, type BindAddress, resolveBindAddress, isLoopbackHost, type RelayDeps, type RpcRelayDeps, handleRequest, type RelayHandle, startRelay, parsePortFlag, parseHostFlag, serveStatic, } from "./relay.js";
|
|
9
|
+
export { generateSelfSignedCert, fingerprintOf, type SelfSignedCert } from "./tls.js";
|
|
10
|
+
export { buildPairingDeepLink, type PairingPayload } from "./pairing.js";
|
|
11
|
+
export { MessageQueue, perConnectionHost, type ServerSocket } from "./ws-transport.js";
|
|
12
|
+
export { highlightForWire, canHighlight, type Segment } from "./highlight.js";
|
|
13
|
+
export { PAIRING_HEADER, mintPairingToken, extractToken, checkToken } from "./auth.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/serve/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,kBAAkB,EAClB,cAAc,EACd,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,aAAa,EACb,KAAK,WAAW,EAChB,UAAU,EACV,aAAa,EACb,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAGtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIvF,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAI9E,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chances-ai/serve` — the thin local relay exposing the engine to web +
|
|
3
|
+
* mobile clients (docs/6.1 §3.1 / 6.4a M3). A default-loopback HTTP server with
|
|
4
|
+
* `/health` + a persistent WS `/acp` ACP control channel (replay/reconnect) +
|
|
5
|
+
* static SPA serving. No agent logic lives here — the engine is reached through
|
|
6
|
+
* the `@chances-ai/rpc` `EngineHost` seam (the `AcpEngineDriver` projection).
|
|
7
|
+
*/
|
|
8
|
+
export { LOOPBACK, DEFAULT_PORT, resolveBindAddress, isLoopbackHost, handleRequest, startRelay, parsePortFlag, parseHostFlag, serveStatic, } from "./relay.js";
|
|
9
|
+
// (v21 M4 / docs/6.5 §3) Self-signed TLS cert generation + SHA-256 fingerprint
|
|
10
|
+
// for a non-loopback bind. Pure; the CLI caches the PEM in the vault.
|
|
11
|
+
export { generateSelfSignedCert, fingerprintOf } from "./tls.js";
|
|
12
|
+
// (v21 M4 / docs/6.5 §4) QR pairing deep link (carries token + fingerprint + url).
|
|
13
|
+
export { buildPairingDeepLink } from "./pairing.js";
|
|
14
|
+
// (M3) Relay transport primitives reused by the persistent ACP `/acp` control
|
|
15
|
+
// channel (M1's per-socket `RpcServer` binding was retired in the hard cutover).
|
|
16
|
+
export { MessageQueue, perConnectionHost } from "./ws-transport.js";
|
|
17
|
+
// (res #2) Server-side highlight seam — the relay emits themed segments so no
|
|
18
|
+
// client ships a highlighter; the same ui-core function the TUI uses.
|
|
19
|
+
export { highlightForWire, canHighlight } from "./highlight.js";
|
|
20
|
+
// (v21 M4 / docs/6.5 §2) Pairing-token auth primitives for the relay edge. Pure
|
|
21
|
+
// + stateless; minting/storage live in the CLI (they need the vault).
|
|
22
|
+
export { PAIRING_HEADER, mintPairingToken, extractToken, checkToken } from "./auth.js";
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/serve/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,QAAQ,EACR,YAAY,EAGZ,kBAAkB,EAClB,cAAc,EAGd,aAAa,EAEb,UAAU,EACV,aAAa,EACb,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,sEAAsE;AACtE,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAuB,MAAM,UAAU,CAAC;AAEtF,mFAAmF;AACnF,OAAO,EAAE,oBAAoB,EAAuB,MAAM,cAAc,CAAC;AAEzE,8EAA8E;AAC9E,iFAAiF;AACjF,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAqB,MAAM,mBAAmB,CAAC;AAEvF,8EAA8E;AAC9E,sEAAsE;AACtE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAgB,MAAM,gBAAgB,CAAC;AAE9E,gFAAgF;AAChF,sEAAsE;AACtE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v21 M4 / docs/6.5 §4) Pairing payload + deep link for QR-based pairing.
|
|
3
|
+
*
|
|
4
|
+
* A single scan conveys BOTH who-to-trust (the cert SHA-256 fingerprint, pinned
|
|
5
|
+
* TOFU) and proof-of-authorization (the pairing token), plus where (the wss URL)
|
|
6
|
+
* and a human label — so a phone (M6) or a second device pairs in one step. This
|
|
7
|
+
* is pure string building; rendering the QR to the terminal is the CLI's job.
|
|
8
|
+
*/
|
|
9
|
+
export interface PairingPayload {
|
|
10
|
+
/** The relay's wss URL, e.g. `wss://192.168.1.5:4517/acp`. */
|
|
11
|
+
url: string;
|
|
12
|
+
/** The pairing token (proof of authorization). */
|
|
13
|
+
token: string;
|
|
14
|
+
/** The cert SHA-256 fingerprint to pin (TOFU). */
|
|
15
|
+
fingerprint: string;
|
|
16
|
+
/** A human label for the host, e.g. the machine name. */
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Encode a pairing payload as a `chances://pair?…` deep link (a mobile custom
|
|
21
|
+
* scheme / universal link). All fields are URL-encoded; a client parses it back
|
|
22
|
+
* with `new URL(link).searchParams` (`url`/`token`/`fp`/`name`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildPairingDeepLink(p: PairingPayload): string;
|
|
25
|
+
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/serve/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAG9D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode a pairing payload as a `chances://pair?…` deep link (a mobile custom
|
|
3
|
+
* scheme / universal link). All fields are URL-encoded; a client parses it back
|
|
4
|
+
* with `new URL(link).searchParams` (`url`/`token`/`fp`/`name`).
|
|
5
|
+
*/
|
|
6
|
+
export function buildPairingDeepLink(p) {
|
|
7
|
+
const q = new URLSearchParams({ url: p.url, token: p.token, fp: p.fingerprint, name: p.name });
|
|
8
|
+
return `chances://pair?${q.toString()}`;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/serve/pairing.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,CAAiB;IACpD,MAAM,CAAC,GAAG,IAAI,eAAe,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/F,OAAO,kBAAkB,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v19 M2 / docs/6.2 §3.3) Relay-side reconnect helpers. The `RelayFrame` types
|
|
3
|
+
* + `stampSeq` live in `@chances-ai/rpc` (browser-safe, shared with client-core);
|
|
4
|
+
* this module holds the serve-only transport-parsing piece.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Read the reconnect cursor a client carries on its `/acp` WebSocket upgrade.
|
|
8
|
+
*
|
|
9
|
+
* The cursor rides a **query parameter** `?last_event_id=N`, NOT an HTTP header
|
|
10
|
+
* — a browser `WebSocket` constructor cannot set request headers, so the header
|
|
11
|
+
* form is unreachable from the web client (claude-code makes the same choice
|
|
12
|
+
* with its `from_sequence_num` query param). The `Last-Event-ID` **header** is
|
|
13
|
+
* still honoured as a fallback for non-browser clients (and aligns the future
|
|
14
|
+
* M5 SSE leg with native `EventSource` reconnect). Query param wins if both are
|
|
15
|
+
* present.
|
|
16
|
+
*
|
|
17
|
+
* Returns `0` (a fresh client ⇒ full ring replay) when absent, empty, or
|
|
18
|
+
* malformed — never throws, never a negative or fractional cursor.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseLastEventId(urlPathAndQuery: string, headers: {
|
|
21
|
+
readonly [key: string]: string | string[] | undefined;
|
|
22
|
+
}): number;
|
|
23
|
+
/**
|
|
24
|
+
* (v21 M4 / docs/6.5 §5) Read the client id a client carries on its `/acp`
|
|
25
|
+
* upgrade (`?client_id=`), so the relay can attribute commands to a client for
|
|
26
|
+
* the controller lease. Returns undefined when absent/empty/malformed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseClientId(urlPathAndQuery: string): string | undefined;
|
|
29
|
+
//# sourceMappingURL=relay-frames.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-frames.d.ts","sourceRoot":"","sources":["../../src/serve/relay-frames.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;CAAE,GACjE,MAAM,CAYR;AAQD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v19 M2 / docs/6.2 §3.3) Relay-side reconnect helpers. The `RelayFrame` types
|
|
3
|
+
* + `stampSeq` live in `@chances-ai/rpc` (browser-safe, shared with client-core);
|
|
4
|
+
* this module holds the serve-only transport-parsing piece.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Read the reconnect cursor a client carries on its `/acp` WebSocket upgrade.
|
|
8
|
+
*
|
|
9
|
+
* The cursor rides a **query parameter** `?last_event_id=N`, NOT an HTTP header
|
|
10
|
+
* — a browser `WebSocket` constructor cannot set request headers, so the header
|
|
11
|
+
* form is unreachable from the web client (claude-code makes the same choice
|
|
12
|
+
* with its `from_sequence_num` query param). The `Last-Event-ID` **header** is
|
|
13
|
+
* still honoured as a fallback for non-browser clients (and aligns the future
|
|
14
|
+
* M5 SSE leg with native `EventSource` reconnect). Query param wins if both are
|
|
15
|
+
* present.
|
|
16
|
+
*
|
|
17
|
+
* Returns `0` (a fresh client ⇒ full ring replay) when absent, empty, or
|
|
18
|
+
* malformed — never throws, never a negative or fractional cursor.
|
|
19
|
+
*/
|
|
20
|
+
export function parseLastEventId(urlPathAndQuery, headers) {
|
|
21
|
+
// `urlPathAndQuery` is `req.url` ("/acp?last_event_id=42"); resolve against a
|
|
22
|
+
// dummy base so only the query matters (the host is irrelevant here).
|
|
23
|
+
let fromQuery = null;
|
|
24
|
+
try {
|
|
25
|
+
fromQuery = new URL(urlPathAndQuery, "http://relay.invalid").searchParams.get("last_event_id");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
fromQuery = null;
|
|
29
|
+
}
|
|
30
|
+
const headerRaw = headers["last-event-id"];
|
|
31
|
+
const fromHeader = Array.isArray(headerRaw) ? headerRaw[0] : headerRaw;
|
|
32
|
+
return toCursor(fromQuery) ?? toCursor(fromHeader) ?? 0;
|
|
33
|
+
}
|
|
34
|
+
function toCursor(raw) {
|
|
35
|
+
if (raw === null || raw === undefined || raw === "")
|
|
36
|
+
return undefined;
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* (v21 M4 / docs/6.5 §5) Read the client id a client carries on its `/acp`
|
|
42
|
+
* upgrade (`?client_id=`), so the relay can attribute commands to a client for
|
|
43
|
+
* the controller lease. Returns undefined when absent/empty/malformed.
|
|
44
|
+
*/
|
|
45
|
+
export function parseClientId(urlPathAndQuery) {
|
|
46
|
+
try {
|
|
47
|
+
const id = new URL(urlPathAndQuery, "http://relay.invalid").searchParams.get("client_id");
|
|
48
|
+
return id && id.length > 0 ? id : undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=relay-frames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-frames.js","sourceRoot":"","sources":["../../src/serve/relay-frames.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,eAAuB,EACvB,OAAkE;IAElE,8EAA8E;IAC9E,sEAAsE;IACtE,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,GAAG,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjG,CAAC;IAAC,MAAM,CAAC;QACP,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACvE,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,GAA8B;IAC9C,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,eAAuB;IACnD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC1F,OAAO,EAAE,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|