@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.
Files changed (93) hide show
  1. package/dist/rpc/acp/adapter.d.ts +32 -0
  2. package/dist/rpc/acp/adapter.d.ts.map +1 -0
  3. package/dist/rpc/acp/adapter.js +185 -0
  4. package/dist/rpc/acp/adapter.js.map +1 -0
  5. package/dist/rpc/acp/engine-driver.d.ts +128 -0
  6. package/dist/rpc/acp/engine-driver.d.ts.map +1 -0
  7. package/dist/rpc/acp/engine-driver.js +550 -0
  8. package/dist/rpc/acp/engine-driver.js.map +1 -0
  9. package/dist/rpc/acp/event-map.d.ts +22 -0
  10. package/dist/rpc/acp/event-map.d.ts.map +1 -0
  11. package/dist/rpc/acp/event-map.js +205 -0
  12. package/dist/rpc/acp/event-map.js.map +1 -0
  13. package/dist/rpc/acp/load-sdk.d.ts +3 -0
  14. package/dist/rpc/acp/load-sdk.d.ts.map +1 -0
  15. package/dist/rpc/acp/load-sdk.js +24 -0
  16. package/dist/rpc/acp/load-sdk.js.map +1 -0
  17. package/dist/rpc/acp/workspace-query.d.ts +41 -0
  18. package/dist/rpc/acp/workspace-query.d.ts.map +1 -0
  19. package/dist/rpc/acp/workspace-query.js +89 -0
  20. package/dist/rpc/acp/workspace-query.js.map +1 -0
  21. package/dist/rpc/driver.d.ts +42 -0
  22. package/dist/rpc/driver.d.ts.map +1 -0
  23. package/dist/rpc/driver.js +7 -0
  24. package/dist/rpc/driver.js.map +1 -0
  25. package/dist/rpc/event-map.d.ts +8 -0
  26. package/dist/rpc/event-map.d.ts.map +1 -0
  27. package/dist/rpc/event-map.js +91 -0
  28. package/dist/rpc/event-map.js.map +1 -0
  29. package/dist/rpc/index.d.ts +13 -0
  30. package/dist/rpc/index.d.ts.map +1 -0
  31. package/dist/rpc/index.js +18 -0
  32. package/dist/rpc/index.js.map +1 -0
  33. package/dist/rpc/lines.d.ts +2 -0
  34. package/dist/rpc/lines.d.ts.map +1 -0
  35. package/dist/rpc/lines.js +24 -0
  36. package/dist/rpc/lines.js.map +1 -0
  37. package/dist/rpc/protocol.d.ts +315 -0
  38. package/dist/rpc/protocol.d.ts.map +1 -0
  39. package/dist/rpc/protocol.js +70 -0
  40. package/dist/rpc/protocol.js.map +1 -0
  41. package/dist/rpc/rpc-server.d.ts +56 -0
  42. package/dist/rpc/rpc-server.d.ts.map +1 -0
  43. package/dist/rpc/rpc-server.js +305 -0
  44. package/dist/rpc/rpc-server.js.map +1 -0
  45. package/dist/rpc/stdout-guard.d.ts +5 -0
  46. package/dist/rpc/stdout-guard.d.ts.map +1 -0
  47. package/dist/rpc/stdout-guard.js +31 -0
  48. package/dist/rpc/stdout-guard.js.map +1 -0
  49. package/dist/rpc/writer.d.ts +34 -0
  50. package/dist/rpc/writer.d.ts.map +1 -0
  51. package/dist/rpc/writer.js +85 -0
  52. package/dist/rpc/writer.js.map +1 -0
  53. package/dist/serve/acp-session-host.d.ts +120 -0
  54. package/dist/serve/acp-session-host.d.ts.map +1 -0
  55. package/dist/serve/acp-session-host.js +276 -0
  56. package/dist/serve/acp-session-host.js.map +1 -0
  57. package/dist/serve/auth.d.ts +21 -0
  58. package/dist/serve/auth.d.ts.map +1 -0
  59. package/dist/serve/auth.js +58 -0
  60. package/dist/serve/auth.js.map +1 -0
  61. package/dist/serve/highlight.d.ts +25 -0
  62. package/dist/serve/highlight.d.ts.map +1 -0
  63. package/dist/serve/highlight.js +28 -0
  64. package/dist/serve/highlight.js.map +1 -0
  65. package/dist/serve/index.d.ts +14 -0
  66. package/dist/serve/index.d.ts.map +1 -0
  67. package/dist/serve/index.js +23 -0
  68. package/dist/serve/index.js.map +1 -0
  69. package/dist/serve/pairing.d.ts +25 -0
  70. package/dist/serve/pairing.d.ts.map +1 -0
  71. package/dist/serve/pairing.js +10 -0
  72. package/dist/serve/pairing.js.map +1 -0
  73. package/dist/serve/relay-frames.d.ts +29 -0
  74. package/dist/serve/relay-frames.d.ts.map +1 -0
  75. package/dist/serve/relay-frames.js +54 -0
  76. package/dist/serve/relay-frames.js.map +1 -0
  77. package/dist/serve/relay.d.ts +146 -0
  78. package/dist/serve/relay.d.ts.map +1 -0
  79. package/dist/serve/relay.js +475 -0
  80. package/dist/serve/relay.js.map +1 -0
  81. package/dist/serve/replay-hub.d.ts +102 -0
  82. package/dist/serve/replay-hub.d.ts.map +1 -0
  83. package/dist/serve/replay-hub.js +176 -0
  84. package/dist/serve/replay-hub.js.map +1 -0
  85. package/dist/serve/tls.d.ts +20 -0
  86. package/dist/serve/tls.d.ts.map +1 -0
  87. package/dist/serve/tls.js +64 -0
  88. package/dist/serve/tls.js.map +1 -0
  89. package/dist/serve/ws-transport.d.ts +64 -0
  90. package/dist/serve/ws-transport.d.ts.map +1 -0
  91. package/dist/serve/ws-transport.js +92 -0
  92. package/dist/serve/ws-transport.js.map +1 -0
  93. 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"}