@chatlychat/cli 0.2.0 → 0.4.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/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +29 -3
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/listen-bot.d.ts +52 -0
- package/dist/commands/listen-bot.d.ts.map +1 -0
- package/dist/commands/listen-bot.js +253 -0
- package/dist/commands/listen-bot.js.map +1 -0
- package/dist/commands/listen-core.d.ts +107 -0
- package/dist/commands/listen-core.d.ts.map +1 -0
- package/dist/commands/listen-core.js +334 -0
- package/dist/commands/listen-core.js.map +1 -0
- package/dist/commands/listen-relay-harness.d.ts +80 -0
- package/dist/commands/listen-relay-harness.d.ts.map +1 -0
- package/dist/commands/listen-relay-harness.js +163 -0
- package/dist/commands/listen-relay-harness.js.map +1 -0
- package/dist/commands/listen-relay-options.d.ts +78 -0
- package/dist/commands/listen-relay-options.d.ts.map +1 -0
- package/dist/commands/listen-relay-options.js +195 -0
- package/dist/commands/listen-relay-options.js.map +1 -0
- package/dist/commands/listen-relay-sign.d.ts +36 -0
- package/dist/commands/listen-relay-sign.d.ts.map +1 -0
- package/dist/commands/listen-relay-sign.js +136 -0
- package/dist/commands/listen-relay-sign.js.map +1 -0
- package/dist/commands/listen-relay.d.ts +43 -0
- package/dist/commands/listen-relay.d.ts.map +1 -0
- package/dist/commands/listen-relay.js +434 -0
- package/dist/commands/listen-relay.js.map +1 -0
- package/dist/commands/listen.d.ts +2 -39
- package/dist/commands/listen.d.ts.map +1 -1
- package/dist/commands/listen.js +73 -208
- package/dist/commands/listen.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +19 -3
- package/dist/help.js.map +1 -1
- package/dist/run.js +1 -1
- package/package.json +3 -4
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `chatly listen` does the same way for every surface it tails.
|
|
3
|
+
*
|
|
4
|
+
* ═══ WHY THIS FILE EXISTS ═════════════════════════════════════════════════
|
|
5
|
+
*
|
|
6
|
+
* `listen` tails two ledgers now — webhook subscriptions
|
|
7
|
+
* (`/v1/webhooks/deliveries`) and an external agent's push deliveries
|
|
8
|
+
* (`/v1/bots/{id}/deliveries`) — and will tail more. The poll loop differs
|
|
9
|
+
* per ledger; the flags, the seen-set, the signing and the forward leg do
|
|
10
|
+
* not. They are here so there is one of each, and so `listen.ts` stays under
|
|
11
|
+
* this repo's 650-line ceiling.
|
|
12
|
+
*
|
|
13
|
+
* ═══ THE SIGNATURE IT MINTS, AND THE SELF-CHECK ═══════════════════════════
|
|
14
|
+
*
|
|
15
|
+
* Neither ledger stores the signature Chatly sent — `webhook_deliveries` and
|
|
16
|
+
* `agent_deliveries` both store the PAYLOAD as `jsonb` and nothing else, and
|
|
17
|
+
* `jsonb` does not preserve key order, so the exact signed bytes are
|
|
18
|
+
* unreconstructable by construction rather than by omission. So a forwarded
|
|
19
|
+
* request is re-signed here, from `--secret`, and every forward carries TWO
|
|
20
|
+
* headers:
|
|
21
|
+
*
|
|
22
|
+
* - the surface's own legacy header (`x-livechat-signature` for an event
|
|
23
|
+
* webhook, `x-chatly-signature` for an agent delivery), `t=…,v1=…` over
|
|
24
|
+
* `<t>.<rawBody>` — what the platform sends today;
|
|
25
|
+
* - `x-chatly-signature-v2`, `t=…,v1=…` over
|
|
26
|
+
* `<t>.<METHOD>.<path>.<rawBody>` — the unified construction every
|
|
27
|
+
* surface is moving to (C-02). The platform does not send this on either
|
|
28
|
+
* surface yet, which is exactly why minting it locally is useful: a
|
|
29
|
+
* developer can write and test the verifier they will need before the
|
|
30
|
+
* sender moves.
|
|
31
|
+
*
|
|
32
|
+
* `path` in that base string is the path of the request being signed — the
|
|
33
|
+
* local `--forward-to` path, because that is the request the receiver
|
|
34
|
+
* verifies. Both headers are verified with the SDK's own
|
|
35
|
+
* `verifyChatlySignature` BEFORE the request goes out, and a forward whose
|
|
36
|
+
* own signature does not verify is refused rather than sent. That self-check
|
|
37
|
+
* is not ceremony: it is the one thing that catches a signer and a verifier
|
|
38
|
+
* drifting on construction or on timestamp unit, which is a bug this product
|
|
39
|
+
* has actually shipped — the scheme once emitted milliseconds in the header
|
|
40
|
+
* while signing a seconds value, and every correct handler rejected us.
|
|
41
|
+
*/
|
|
42
|
+
import { createHmac } from 'node:crypto';
|
|
43
|
+
import { chatlyV2SigningPath, signChatlyV2, verifyChatlySignature } from '@chatlychat/sdk-node';
|
|
44
|
+
import { bool, num, str } from '../args.js';
|
|
45
|
+
export const WEBHOOK_SIGN_PLAN = {
|
|
46
|
+
surface: 'event_webhook',
|
|
47
|
+
legacyHeader: 'x-livechat-signature',
|
|
48
|
+
timestampHeader: 'x-livechat-timestamp',
|
|
49
|
+
};
|
|
50
|
+
export const AGENT_SIGN_PLAN = {
|
|
51
|
+
surface: 'agent_delivery',
|
|
52
|
+
legacyHeader: 'x-chatly-signature',
|
|
53
|
+
timestampHeader: null,
|
|
54
|
+
};
|
|
55
|
+
/** Deliveries remembered before the oldest id is dropped. */
|
|
56
|
+
const SEEN_CAP = 5_000;
|
|
57
|
+
const DEFAULT_INTERVAL_SECONDS = 2;
|
|
58
|
+
const MIN_INTERVAL_SECONDS = 1;
|
|
59
|
+
const DEFAULT_PAGE_LIMIT = 50;
|
|
60
|
+
/**
|
|
61
|
+
* The four words both ledgers use, with the same meanings: `failed` will be
|
|
62
|
+
* retried and `dead` will not. Validated here because the bot ledger answers
|
|
63
|
+
* a **400** on a word outside the four rather than an empty page.
|
|
64
|
+
*/
|
|
65
|
+
export const DELIVERY_STATUSES = ['pending', 'delivered', 'failed', 'dead'];
|
|
66
|
+
/** Drop the oldest ids once the set is at its cap. Sets iterate in insertion order. */
|
|
67
|
+
export function remember(seen, id) {
|
|
68
|
+
seen.add(id);
|
|
69
|
+
if (seen.size > SEEN_CAP) {
|
|
70
|
+
for (const old of seen) {
|
|
71
|
+
seen.delete(old);
|
|
72
|
+
if (seen.size <= SEEN_CAP) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** `t=<unix seconds>,v1=<hex>` over `<t>.<body>`, the way the worker signs. */
|
|
79
|
+
function tv1(body, secret, t, prefix = '') {
|
|
80
|
+
const v1 = createHmac('sha256', secret).update(`${t}.${prefix}${body}`).digest('hex');
|
|
81
|
+
return `t=${t},v1=${v1}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The signature headers for one forwarded request, self-checked.
|
|
85
|
+
*
|
|
86
|
+
* Returns the headers to merge, or a message naming what went wrong — which
|
|
87
|
+
* can only be a bug in this CLI, since it signed the bytes itself moments
|
|
88
|
+
* earlier. See this file's header for why both headers are minted.
|
|
89
|
+
*/
|
|
90
|
+
export function signedForwardHeaders(input) {
|
|
91
|
+
const t = Math.floor(input.nowMs / 1000).toString();
|
|
92
|
+
const { plan, body, secret, nowMs } = input;
|
|
93
|
+
const legacy = tv1(body, secret, t);
|
|
94
|
+
const legacyCheck = verifyChatlySignature({
|
|
95
|
+
surface: plan.surface,
|
|
96
|
+
body,
|
|
97
|
+
headers: { [plan.legacyHeader]: legacy },
|
|
98
|
+
secret,
|
|
99
|
+
nowMs,
|
|
100
|
+
// The v2 header is minted below and lives in the same bag on the wire;
|
|
101
|
+
// this check is specifically of the LEGACY construction, so the policy
|
|
102
|
+
// that would prefer v2 has to be off.
|
|
103
|
+
v2: 'ignore',
|
|
104
|
+
});
|
|
105
|
+
if (!legacyCheck.ok) {
|
|
106
|
+
return { ok: false, message: `${legacyCheck.reason}: ${legacyCheck.message}` };
|
|
107
|
+
}
|
|
108
|
+
const headers = { [plan.legacyHeader]: legacy };
|
|
109
|
+
if (plan.timestampHeader !== null) {
|
|
110
|
+
headers[plan.timestampHeader] = t;
|
|
111
|
+
}
|
|
112
|
+
/*
|
|
113
|
+
* The unified v2 header. `method` is POST because that is what this
|
|
114
|
+
* forwards, and `path` is the target's own path — the receiver verifies
|
|
115
|
+
* the request IT received, so the path in the digest has to be the one on
|
|
116
|
+
* that request line.
|
|
117
|
+
*/
|
|
118
|
+
const path = chatlyV2SigningPath(input.targetUrl);
|
|
119
|
+
if (path === null) {
|
|
120
|
+
return { ok: false, message: `--forward-to "${input.targetUrl}" is not a URL` };
|
|
121
|
+
}
|
|
122
|
+
/*
|
|
123
|
+
* Through the shared signer, so this emits the `b=t` base marker.
|
|
124
|
+
*
|
|
125
|
+
* It used to build the value by hand, and its own docblock said "the
|
|
126
|
+
* platform does not send this on either surface yet" — which is now false
|
|
127
|
+
* for both. Worse, a hand-rolled value carries no marker, and an absent
|
|
128
|
+
* marker means the LEGACY `<t>.<rawBody>` construction: a receiver reading
|
|
129
|
+
* `b=` to decide what to verify would have been told, by the one tool whose
|
|
130
|
+
* job is to rehearse production, that the unified base is the body base.
|
|
131
|
+
*
|
|
132
|
+
* From `@chatlychat/sdk-node`, NOT from `@livechat/shared`. This package
|
|
133
|
+
* builds with plain `tsc` and publishes `dist` declaring only the SDK, so
|
|
134
|
+
* an import of the internal scope would emit a reference to a package that
|
|
135
|
+
* is not on npm and `npm i -g @chatlychat/cli` would fail at run time with
|
|
136
|
+
* MODULE_NOT_FOUND — a break no test here would see, because in the
|
|
137
|
+
* monorepo the import resolves. The SDK's copy is held byte-for-byte
|
|
138
|
+
* against the one the API and worker send with by
|
|
139
|
+
* `packages/shared/src/webhook-signature/v2.parity.spec.ts`, so this still
|
|
140
|
+
* teaches exactly the shape production sends.
|
|
141
|
+
*/
|
|
142
|
+
const v2 = signChatlyV2({
|
|
143
|
+
base: 'target',
|
|
144
|
+
secret,
|
|
145
|
+
timestampSeconds: Number(t),
|
|
146
|
+
method: 'POST',
|
|
147
|
+
path,
|
|
148
|
+
body,
|
|
149
|
+
});
|
|
150
|
+
const v2Check = verifyChatlySignature({
|
|
151
|
+
surface: 'unified_v2',
|
|
152
|
+
body,
|
|
153
|
+
headers: { 'x-chatly-signature-v2': v2 },
|
|
154
|
+
secret,
|
|
155
|
+
method: 'POST',
|
|
156
|
+
path,
|
|
157
|
+
nowMs,
|
|
158
|
+
});
|
|
159
|
+
if (!v2Check.ok) {
|
|
160
|
+
return { ok: false, message: `${v2Check.reason}: ${v2Check.message}` };
|
|
161
|
+
}
|
|
162
|
+
headers['x-chatly-signature-v2'] = v2;
|
|
163
|
+
return { ok: true, headers };
|
|
164
|
+
}
|
|
165
|
+
/** POST one event on, verifying the headers we just minted before sending. */
|
|
166
|
+
export async function forward(opts, deps, req) {
|
|
167
|
+
if (opts.forwardTo === null) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const headers = {
|
|
171
|
+
'content-type': 'application/json',
|
|
172
|
+
...req.headers,
|
|
173
|
+
// Not a header Chatly sends. A handler that logs it can tell a replayed
|
|
174
|
+
// local forward from a real delivery, which matters the first time
|
|
175
|
+
// somebody points this at a staging environment.
|
|
176
|
+
'x-chatly-cli-forwarded': '1',
|
|
177
|
+
};
|
|
178
|
+
if (opts.secret !== null) {
|
|
179
|
+
const signed = signedForwardHeaders({
|
|
180
|
+
plan: req.plan,
|
|
181
|
+
body: req.body,
|
|
182
|
+
secret: opts.secret,
|
|
183
|
+
nowMs: deps.now(),
|
|
184
|
+
targetUrl: opts.forwardTo,
|
|
185
|
+
});
|
|
186
|
+
if (!signed.ok) {
|
|
187
|
+
deps.err(`refusing to forward ${req.id}: the signature this CLI minted does not verify ` +
|
|
188
|
+
`(${signed.message}). This is a bug in chatly listen, not in your handler — ` +
|
|
189
|
+
'please report it.\n');
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
Object.assign(headers, signed.headers);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const res = await deps.fetchFn(opts.forwardTo, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers,
|
|
198
|
+
body: req.body,
|
|
199
|
+
signal: deps.signal,
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
deps.err(` ↳ ${opts.forwardTo} answered ${String(res.status)}. ` +
|
|
203
|
+
(res.status === 401 || res.status === 403
|
|
204
|
+
? 'That is usually signature verification — check --secret matches the endpoint.\n'
|
|
205
|
+
: '\n'));
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
deps.out(` ↳ ${String(res.status)} from ${opts.forwardTo}`);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
213
|
+
deps.err(` ↳ ${opts.forwardTo} unreachable: ${reason}. Is your server running on that port?\n`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function parseEventFilter(args) {
|
|
218
|
+
const raw = args.flags['event'];
|
|
219
|
+
if (raw === undefined) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const list = (Array.isArray(raw) ? raw : [String(raw)]).flatMap((v) => v
|
|
223
|
+
.split(',')
|
|
224
|
+
.map((s) => s.trim())
|
|
225
|
+
.filter((s) => s.length > 0));
|
|
226
|
+
return list.length === 0 ? null : new Set(list);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Read the flags, refusing anything that cannot work, with the fix named.
|
|
230
|
+
*
|
|
231
|
+
* The two secret prefixes are the one refusal worth reading twice: a webhook
|
|
232
|
+
* endpoint's secret is `whsec_` and an external agent's is `chsec_`, they are
|
|
233
|
+
* minted by different routes, and neither verifies the other's traffic. The
|
|
234
|
+
* symptom of crossing them is a 401 from your own handler, which looks
|
|
235
|
+
* exactly like a verifier bug.
|
|
236
|
+
*/
|
|
237
|
+
export function parseListenOptions(args) {
|
|
238
|
+
const forwardTo = str(args.flags, 'forward-to') ?? null;
|
|
239
|
+
if (forwardTo !== null) {
|
|
240
|
+
let parsed = null;
|
|
241
|
+
try {
|
|
242
|
+
parsed = new URL(forwardTo);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
parsed = null;
|
|
246
|
+
}
|
|
247
|
+
/*
|
|
248
|
+
* One message for both failures, because `localhost:3000` — the mistake
|
|
249
|
+
* everyone makes — hits the SECOND branch, not the first: the WHATWG
|
|
250
|
+
* parser reads `localhost:` as a SCHEME and succeeds. A test caught this
|
|
251
|
+
* command telling a developer who typed `localhost:3000` that the scheme
|
|
252
|
+
* `"localhost:"` was unsupported, which is true and useless.
|
|
253
|
+
*/
|
|
254
|
+
if (parsed === null || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
|
|
255
|
+
throw new Error(`--forward-to "${forwardTo}" is not an http(s) URL${parsed === null ? '' : ` (it parsed as the scheme "${parsed.protocol}")`}. Include the scheme:\n` + ' chatly listen --forward-to http://localhost:3000/webhooks');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const botId = str(args.flags, 'bot') ?? null;
|
|
259
|
+
const endpointId = str(args.flags, 'endpoint') ?? null;
|
|
260
|
+
if (botId !== null && endpointId !== null) {
|
|
261
|
+
throw new Error('--bot and --endpoint tail different ledgers and cannot be combined. --bot <id> tails one ' +
|
|
262
|
+
"external agent's push deliveries; --endpoint <id> tails one webhook subscription's.");
|
|
263
|
+
}
|
|
264
|
+
const intervalSeconds = num(args.flags, 'interval') ?? DEFAULT_INTERVAL_SECONDS;
|
|
265
|
+
if (intervalSeconds < MIN_INTERVAL_SECONDS) {
|
|
266
|
+
throw new Error(`--interval ${String(intervalSeconds)} is below the ${String(MIN_INTERVAL_SECONDS)}s floor. ` +
|
|
267
|
+
'This polls a real API on your own rate limit; a tighter loop buys nothing, because the ' +
|
|
268
|
+
'delivery row is written after the attempt completes.');
|
|
269
|
+
}
|
|
270
|
+
const pageLimit = num(args.flags, 'limit') ?? DEFAULT_PAGE_LIMIT;
|
|
271
|
+
if (pageLimit < 1) {
|
|
272
|
+
throw new Error(`--limit ${String(pageLimit)} must be at least 1.`);
|
|
273
|
+
}
|
|
274
|
+
const replay = num(args.flags, 'replay') ?? 0;
|
|
275
|
+
if (replay < 0) {
|
|
276
|
+
throw new Error(`--replay ${String(replay)} cannot be negative.`);
|
|
277
|
+
}
|
|
278
|
+
if (replay > pageLimit) {
|
|
279
|
+
throw new Error(`--replay ${String(replay)} is more than --limit ${String(pageLimit)}, and this command ` +
|
|
280
|
+
'only ever reads one page per poll. Raise --limit to match.');
|
|
281
|
+
}
|
|
282
|
+
const status = str(args.flags, 'status') ?? null;
|
|
283
|
+
if (botId !== null && status !== null && !DELIVERY_STATUSES.includes(status)) {
|
|
284
|
+
// The route validates this against a zod enum and answers 400, not an
|
|
285
|
+
// empty page. Refusing here names the four words.
|
|
286
|
+
throw new Error(`--status "${status}" is not one of ${DELIVERY_STATUSES.join(', ')}. ` +
|
|
287
|
+
'`failed` will be retried; `dead` will not.');
|
|
288
|
+
}
|
|
289
|
+
const secret = str(args.flags, 'secret') ?? null;
|
|
290
|
+
if (secret !== null && botId === null && !secret.startsWith('whsec_')) {
|
|
291
|
+
throw new Error(`--secret should be the endpoint's whsec_ signing secret (got "${secret.slice(0, 6)}…").\n` +
|
|
292
|
+
'It is returned exactly once, by `chatly webhooks create`, and by no other route — if ' +
|
|
293
|
+
'you no longer have it, create another endpoint.\n' +
|
|
294
|
+
"With --bot, pass the AGENT's chsec_ secret instead; the two do not verify each other's " +
|
|
295
|
+
'traffic.');
|
|
296
|
+
}
|
|
297
|
+
if (secret !== null && botId !== null && !secret.startsWith('chsec_')) {
|
|
298
|
+
throw new Error(`--secret should be the external agent's chsec_ signing secret (got "${secret.slice(0, 6)}…").\n` +
|
|
299
|
+
'It is shown once by `chatly bots create` and once by `chatly bots rotate-secret <id>`, ' +
|
|
300
|
+
'and no read route returns it.\n' +
|
|
301
|
+
"A webhook endpoint's whsec_ secret signs a different surface and will not verify here.");
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
forwardTo,
|
|
305
|
+
botId,
|
|
306
|
+
endpointId,
|
|
307
|
+
events: parseEventFilter(args),
|
|
308
|
+
status,
|
|
309
|
+
secret,
|
|
310
|
+
intervalMs: intervalSeconds * 1000,
|
|
311
|
+
replay,
|
|
312
|
+
pageLimit,
|
|
313
|
+
json: bool(args.flags, 'json'),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** An abortable sleep, so Ctrl-C does not wait out the poll interval. */
|
|
317
|
+
export function sleepUntilAborted(ms, signal) {
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
if (signal.aborted) {
|
|
320
|
+
resolve();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const timer = setTimeout(() => {
|
|
324
|
+
signal.removeEventListener('abort', onAbort);
|
|
325
|
+
resolve();
|
|
326
|
+
}, ms);
|
|
327
|
+
function onAbort() {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
resolve();
|
|
330
|
+
}
|
|
331
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
//# sourceMappingURL=listen-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-core.js","sourceRoot":"","sources":["../../src/commands/listen-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scripted server, the frame fixture and the deps both relay specs drive.
|
|
3
|
+
*
|
|
4
|
+
* Extracted when `listen-relay.spec.ts` crossed the 650-line ceiling and had
|
|
5
|
+
* to become two files. It is a FIXTURE module and not a spec: nothing here
|
|
6
|
+
* asserts anything, and both specs' caveats live on them rather than here.
|
|
7
|
+
*
|
|
8
|
+
* ── The one thing worth repeating here ───────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* The "server" is a fake `fetch` under the REAL `ChatlyClient`, so the SDK's
|
|
11
|
+
* transport, retries and error mapping all run. What it CANNOT do is tell you
|
|
12
|
+
* the server answers this way: `server()` answers what
|
|
13
|
+
* `RELAY-SERVER-SPEC.md` says, which is the contract and not an observation.
|
|
14
|
+
* A fake that agrees with whatever it is asked is how four mobile SDKs
|
|
15
|
+
* shipped a realtime client that could not connect.
|
|
16
|
+
*
|
|
17
|
+
* (Original header follows.)
|
|
18
|
+
*
|
|
19
|
+
* `chatly listen --relay` — the CLI half, against a scripted server.
|
|
20
|
+
*
|
|
21
|
+
* ═══ WHAT THIS CAN AND CANNOT PROVE ═══════════════════════════════════════
|
|
22
|
+
*
|
|
23
|
+
* The server half is `apps/api/src/modules/relay/`, but this spec does not run
|
|
24
|
+
* it: the "server" here is a fake `fetch` under the REAL `ChatlyClient` — the
|
|
25
|
+
* SDK's transport, retries and error mapping all run — answering the routes
|
|
26
|
+
* `RELAY-SERVER-SPEC.md` defines.
|
|
27
|
+
* That proves what the CLI SENDS (the attach body, the answer it posts, the
|
|
28
|
+
* request it makes to localhost and its signature) and how it reacts to each
|
|
29
|
+
* answer the spec allows. It cannot prove the server will answer that way,
|
|
30
|
+
* and nothing here should be read as evidence that a relay works end to end.
|
|
31
|
+
*
|
|
32
|
+
* Assertions are on the requests the CLI made, not on a canned response it
|
|
33
|
+
* echoed: a fake that agrees with whatever it is asked is how four mobile
|
|
34
|
+
* SDKs shipped a realtime client that could not connect.
|
|
35
|
+
*/
|
|
36
|
+
import { ChatlyClient } from '@chatlychat/sdk-node';
|
|
37
|
+
import { type RelayFrame, type RelayOptions } from './listen-relay.js';
|
|
38
|
+
import type { ListenDeps } from './listen-core.js';
|
|
39
|
+
export declare const NOW_MS = 1760000000123;
|
|
40
|
+
export declare const SESSION = "rs_1";
|
|
41
|
+
export declare const TOOL = "tool_orders";
|
|
42
|
+
export declare const CARD = "card_billing";
|
|
43
|
+
export declare const CANVAS = "ch_web";
|
|
44
|
+
export declare const SECRET = "tool-secret-from-the-form";
|
|
45
|
+
export interface Call {
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly method: string;
|
|
48
|
+
readonly headers: Record<string, string>;
|
|
49
|
+
readonly body: string | null;
|
|
50
|
+
}
|
|
51
|
+
export declare function headersOf(init?: RequestInit): Record<string, string>;
|
|
52
|
+
export declare function frame(over?: Partial<RelayFrame>): RelayFrame;
|
|
53
|
+
export type Answer = {
|
|
54
|
+
status: number;
|
|
55
|
+
body?: unknown;
|
|
56
|
+
code?: string;
|
|
57
|
+
};
|
|
58
|
+
/** A scripted relay server: attach, a queue of poll answers, answer + detach. */
|
|
59
|
+
export declare function server(opts?: {
|
|
60
|
+
attach?: Answer;
|
|
61
|
+
polls?: ReadonlyArray<Answer>;
|
|
62
|
+
}): {
|
|
63
|
+
chatly: ChatlyClient;
|
|
64
|
+
calls: Call[];
|
|
65
|
+
};
|
|
66
|
+
/** The local handler: records every forward, answers as scripted. */
|
|
67
|
+
export declare function harness(over?: {
|
|
68
|
+
respond?: (call: Call) => Response | Promise<Response>;
|
|
69
|
+
now?: () => number;
|
|
70
|
+
maxPolls?: number;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}): {
|
|
73
|
+
deps: ListenDeps;
|
|
74
|
+
forwards: Call[];
|
|
75
|
+
out: string[];
|
|
76
|
+
err: string[];
|
|
77
|
+
};
|
|
78
|
+
export declare function options(argv: string): RelayOptions;
|
|
79
|
+
export declare const ALL = "--tool tool_orders --card card_billing --canvas ch_web --forward-to http://localhost:3000";
|
|
80
|
+
//# sourceMappingURL=listen-relay-harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-relay-harness.d.ts","sourceRoot":"","sources":["../../src/commands/listen-relay-harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,eAAO,MAAM,MAAM,gBAAoB,CAAC;AACxC,eAAO,MAAM,OAAO,SAAS,CAAC;AAC9B,eAAO,MAAM,IAAI,gBAAgB,CAAC;AAClC,eAAO,MAAM,IAAI,iBAAiB,CAAC;AACnC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,MAAM,8BAA8B,CAAC;AAElD,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMpE;AAED,wBAAgB,KAAK,CAAC,IAAI,GAAE,OAAO,CAAC,UAAU,CAAM,GAAG,UAAU,CAgBhE;AAED,MAAM,MAAM,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAO,GAAG;IACrF,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf,CAyDA;AAED,qEAAqE;AACrE,wBAAgB,OAAO,CACrB,IAAI,GAAE;IACJ,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,CAiCtE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAElD;AAED,eAAO,MAAM,GAAG,8FAAwF,CAAC"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scripted server, the frame fixture and the deps both relay specs drive.
|
|
3
|
+
*
|
|
4
|
+
* Extracted when `listen-relay.spec.ts` crossed the 650-line ceiling and had
|
|
5
|
+
* to become two files. It is a FIXTURE module and not a spec: nothing here
|
|
6
|
+
* asserts anything, and both specs' caveats live on them rather than here.
|
|
7
|
+
*
|
|
8
|
+
* ── The one thing worth repeating here ───────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* The "server" is a fake `fetch` under the REAL `ChatlyClient`, so the SDK's
|
|
11
|
+
* transport, retries and error mapping all run. What it CANNOT do is tell you
|
|
12
|
+
* the server answers this way: `server()` answers what
|
|
13
|
+
* `RELAY-SERVER-SPEC.md` says, which is the contract and not an observation.
|
|
14
|
+
* A fake that agrees with whatever it is asked is how four mobile SDKs
|
|
15
|
+
* shipped a realtime client that could not connect.
|
|
16
|
+
*
|
|
17
|
+
* (Original header follows.)
|
|
18
|
+
*
|
|
19
|
+
* `chatly listen --relay` — the CLI half, against a scripted server.
|
|
20
|
+
*
|
|
21
|
+
* ═══ WHAT THIS CAN AND CANNOT PROVE ═══════════════════════════════════════
|
|
22
|
+
*
|
|
23
|
+
* The server half is `apps/api/src/modules/relay/`, but this spec does not run
|
|
24
|
+
* it: the "server" here is a fake `fetch` under the REAL `ChatlyClient` — the
|
|
25
|
+
* SDK's transport, retries and error mapping all run — answering the routes
|
|
26
|
+
* `RELAY-SERVER-SPEC.md` defines.
|
|
27
|
+
* That proves what the CLI SENDS (the attach body, the answer it posts, the
|
|
28
|
+
* request it makes to localhost and its signature) and how it reacts to each
|
|
29
|
+
* answer the spec allows. It cannot prove the server will answer that way,
|
|
30
|
+
* and nothing here should be read as evidence that a relay works end to end.
|
|
31
|
+
*
|
|
32
|
+
* Assertions are on the requests the CLI made, not on a canned response it
|
|
33
|
+
* echoed: a fake that agrees with whatever it is asked is how four mobile
|
|
34
|
+
* SDKs shipped a realtime client that could not connect.
|
|
35
|
+
*/
|
|
36
|
+
import { ChatlyClient } from '@chatlychat/sdk-node';
|
|
37
|
+
import { parseRelayOptions, } from './listen-relay.js';
|
|
38
|
+
import { parseArgs } from '../args.js';
|
|
39
|
+
export const NOW_MS = 1_760_000_000_123;
|
|
40
|
+
export const SESSION = 'rs_1';
|
|
41
|
+
export const TOOL = 'tool_orders';
|
|
42
|
+
export const CARD = 'card_billing';
|
|
43
|
+
export const CANVAS = 'ch_web';
|
|
44
|
+
export const SECRET = 'tool-secret-from-the-form';
|
|
45
|
+
export function headersOf(init) {
|
|
46
|
+
const h = init?.headers;
|
|
47
|
+
if (h instanceof Headers) {
|
|
48
|
+
return Object.fromEntries(h.entries());
|
|
49
|
+
}
|
|
50
|
+
return { ...(h ?? {}) };
|
|
51
|
+
}
|
|
52
|
+
export function frame(over = {}) {
|
|
53
|
+
return {
|
|
54
|
+
id: 'fr_1',
|
|
55
|
+
kind: 'ai_tool_call',
|
|
56
|
+
targetId: TOOL,
|
|
57
|
+
method: 'GET',
|
|
58
|
+
path: '/orders/42?expand=items',
|
|
59
|
+
headers: {
|
|
60
|
+
'x-chatly-contact-external-id': 'u_7',
|
|
61
|
+
'x-chatly-identity-verified': 'true',
|
|
62
|
+
'x-chatly-conversation-id': 'c_1',
|
|
63
|
+
},
|
|
64
|
+
body: '',
|
|
65
|
+
deadlineAt: new Date(NOW_MS + 30_000).toISOString(),
|
|
66
|
+
...over,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A scripted relay server: attach, a queue of poll answers, answer + detach. */
|
|
70
|
+
export function server(opts = {}) {
|
|
71
|
+
const calls = [];
|
|
72
|
+
let poll = 0;
|
|
73
|
+
const fetchFn = (async (url, init) => {
|
|
74
|
+
const href = String(url);
|
|
75
|
+
const method = init?.method ?? 'GET';
|
|
76
|
+
calls.push({
|
|
77
|
+
url: href,
|
|
78
|
+
method,
|
|
79
|
+
headers: headersOf(init),
|
|
80
|
+
body: typeof init?.body === 'string' ? init.body : null,
|
|
81
|
+
});
|
|
82
|
+
const path = new URL(href).pathname;
|
|
83
|
+
const reply = (a) => {
|
|
84
|
+
if (a.status === 204) {
|
|
85
|
+
return new Response(null, { status: 204 });
|
|
86
|
+
}
|
|
87
|
+
const body = a.code === undefined ? (a.body ?? {}) : { error: { code: a.code, message: a.code } };
|
|
88
|
+
return new Response(JSON.stringify(body), {
|
|
89
|
+
status: a.status,
|
|
90
|
+
headers: { 'content-type': 'application/json' },
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
if (method === 'POST' && path === '/v1/relay-sessions') {
|
|
94
|
+
return reply(opts.attach ?? {
|
|
95
|
+
status: 201,
|
|
96
|
+
body: { id: SESSION, attachedBy: { displayName: 'key "laptop" (Ada)' } },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (method === 'GET' && path === `/v1/relay-sessions/${SESSION}/frames`) {
|
|
100
|
+
const list = opts.polls ?? [{ status: 200, body: { frames: [] } }];
|
|
101
|
+
const a = list[Math.min(poll, list.length - 1)] ?? { status: 200, body: { frames: [] } };
|
|
102
|
+
poll += 1;
|
|
103
|
+
return reply(a);
|
|
104
|
+
}
|
|
105
|
+
if (method === 'POST' && path.startsWith(`/v1/relay-sessions/${SESSION}/frames/`)) {
|
|
106
|
+
return reply({ status: 202, body: { accepted: true } });
|
|
107
|
+
}
|
|
108
|
+
if (method === 'DELETE' && path === `/v1/relay-sessions/${SESSION}`) {
|
|
109
|
+
return reply({ status: 204 });
|
|
110
|
+
}
|
|
111
|
+
return reply({ status: 599, body: { unexpected: path } });
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
chatly: new ChatlyClient({
|
|
115
|
+
apiKey: 'ck_test',
|
|
116
|
+
apiUrl: 'https://api.test',
|
|
117
|
+
fetchFn,
|
|
118
|
+
sleepFn: async () => undefined,
|
|
119
|
+
retry: { attempts: 1 },
|
|
120
|
+
}),
|
|
121
|
+
calls,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** The local handler: records every forward, answers as scripted. */
|
|
125
|
+
export function harness(over = {}) {
|
|
126
|
+
const forwards = [];
|
|
127
|
+
const out = [];
|
|
128
|
+
const err = [];
|
|
129
|
+
const fetchFn = (async (url, init) => {
|
|
130
|
+
const call = {
|
|
131
|
+
url: String(url),
|
|
132
|
+
method: init?.method ?? 'GET',
|
|
133
|
+
headers: headersOf(init),
|
|
134
|
+
body: typeof init?.body === 'string' ? init.body : null,
|
|
135
|
+
};
|
|
136
|
+
forwards.push(call);
|
|
137
|
+
return over.respond === undefined
|
|
138
|
+
? new Response('{"ok":true}', {
|
|
139
|
+
status: 200,
|
|
140
|
+
headers: { 'content-type': 'application/json' },
|
|
141
|
+
})
|
|
142
|
+
: over.respond(call);
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
deps: {
|
|
146
|
+
fetchFn,
|
|
147
|
+
now: over.now ?? (() => NOW_MS),
|
|
148
|
+
sleep: async () => undefined,
|
|
149
|
+
signal: over.signal ?? new AbortController().signal,
|
|
150
|
+
out: (l) => out.push(l),
|
|
151
|
+
err: (l) => err.push(l),
|
|
152
|
+
maxPolls: over.maxPolls ?? 1,
|
|
153
|
+
},
|
|
154
|
+
forwards,
|
|
155
|
+
out,
|
|
156
|
+
err,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function options(argv) {
|
|
160
|
+
return parseRelayOptions(parseArgs(['listen', '--relay', ...argv.split(' ').filter(Boolean)]));
|
|
161
|
+
}
|
|
162
|
+
export const ALL = `--tool ${TOOL} --card ${CARD} --canvas ${CANVAS} --forward-to http://localhost:3000`;
|
|
163
|
+
//# sourceMappingURL=listen-relay-harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-relay-harness.js","sourceRoot":"","sources":["../../src/commands/listen-relay-harness.ts"],"names":[],"mappings":"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"}
|