chat-recall 0.5.21 → 0.5.23
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/cli.js +398 -271
- package/dist/cli.js.map +1 -1
- package/dist/conformant-server.d.ts +17 -0
- package/dist/conformant-server.d.ts.map +1 -0
- package/dist/conformant-server.js +116 -0
- package/dist/conformant-server.js.map +1 -0
- package/dist/device-auth.d.ts +0 -4
- package/dist/device-auth.d.ts.map +1 -1
- package/dist/device-auth.js +43 -2
- package/dist/device-auth.js.map +1 -1
- package/dist/hostile-server.d.ts +47 -0
- package/dist/hostile-server.d.ts.map +1 -0
- package/dist/hostile-server.js +95 -0
- package/dist/hostile-server.js.map +1 -0
- package/dist/install-report.d.ts +21 -0
- package/dist/install-report.d.ts.map +1 -0
- package/dist/install-report.js +76 -0
- package/dist/install-report.js.map +1 -0
- package/dist/mcp.js +160 -8
- package/dist/server-probe.d.ts +35 -1
- package/dist/server-probe.d.ts.map +1 -1
- package/dist/server-probe.js +124 -3
- package/dist/server-probe.js.map +1 -1
- package/dist/sync-client.d.ts.map +1 -1
- package/dist/sync-client.js +47 -2
- package/dist/sync-client.js.map +1 -1
- package/dist/watch.js +250 -103
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** One request the collector made. */
|
|
2
|
+
export interface RecordedRequest {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
/** Parsed JSON body, when there was one. */
|
|
6
|
+
body?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface ConformantServer {
|
|
9
|
+
url: string;
|
|
10
|
+
/** Every request, in order — the record a test asserts against. */
|
|
11
|
+
requests: RecordedRequest[];
|
|
12
|
+
/** The conversations POSTed to /api/sync across every upload. */
|
|
13
|
+
uploaded: () => Array<Record<string, unknown>>;
|
|
14
|
+
close: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function startConformantServer(): Promise<ConformantServer>;
|
|
17
|
+
//# sourceMappingURL=conformant-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformant-server.d.ts","sourceRoot":"","sources":["../src/conformant-server.ts"],"names":[],"mappings":"AAyBA,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAmBD,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA4EvE"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SMALLEST server the collector will successfully sync to.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists, next to hostile-server.ts ────────────────────────────
|
|
5
|
+
* hostile-server.ts proves the CLI describes a BROKEN server correctly. Nothing
|
|
6
|
+
* proved the CLI can complete a sync at all except the compose-integration
|
|
7
|
+
* workflow, which needs Docker and therefore runs on Linux only. So on macOS and
|
|
8
|
+
* Windows the gate said: it installs, it prints a version, it boots the MCP
|
|
9
|
+
* server, and it fails gracefully. Whether `login` then `sync` actually uploads
|
|
10
|
+
* a session on those two platforms was never checked anywhere.
|
|
11
|
+
*
|
|
12
|
+
* Every failure that gap could hide is platform-shaped — path separators, a
|
|
13
|
+
* case-insensitive filesystem, a home directory with a space in it, CRLF in a
|
|
14
|
+
* transcript, a Windows temp path in a project id. This answers it with no
|
|
15
|
+
* Docker and no network: an in-process HTTP server that speaks enough of the API
|
|
16
|
+
* for a real `login --token` and a real `sync` to succeed, and that RECORDS what
|
|
17
|
+
* it was sent so a test can assert the session actually arrived.
|
|
18
|
+
*
|
|
19
|
+
* It is deliberately not a fake of the product. It has no storage, no tenants
|
|
20
|
+
* and no search. It answers the seven endpoints the collector's sync path calls,
|
|
21
|
+
* with the minimum each one needs, and keeps the request bodies.
|
|
22
|
+
*/
|
|
23
|
+
import { createServer } from 'node:http';
|
|
24
|
+
import { gunzipSync } from 'node:zlib';
|
|
25
|
+
/** The API version this stub claims. Must be >= the collector's floor. */
|
|
26
|
+
const API_VERSION = 2;
|
|
27
|
+
async function readBody(req) {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const c of req)
|
|
30
|
+
chunks.push(c);
|
|
31
|
+
if (chunks.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
let raw = Buffer.concat(chunks);
|
|
34
|
+
// The collector gzips a body when the server advertises it. This one does not
|
|
35
|
+
// advertise it, but decode anyway rather than fail confusingly if that ever
|
|
36
|
+
// changes.
|
|
37
|
+
if (req.headers['content-encoding'] === 'gzip') {
|
|
38
|
+
try {
|
|
39
|
+
raw = gunzipSync(raw);
|
|
40
|
+
}
|
|
41
|
+
catch { /* fall through to a parse failure */ }
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(raw.toString('utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return raw.toString('utf8');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function startConformantServer() {
|
|
51
|
+
const requests = [];
|
|
52
|
+
const conversations = [];
|
|
53
|
+
const server = createServer((req, res) => {
|
|
54
|
+
void (async () => {
|
|
55
|
+
const path = (req.url || '').split('?')[0];
|
|
56
|
+
const body = await readBody(req);
|
|
57
|
+
requests.push({ method: req.method || 'GET', path, body });
|
|
58
|
+
const json = (status, payload) => {
|
|
59
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
60
|
+
res.end(JSON.stringify(payload));
|
|
61
|
+
};
|
|
62
|
+
switch (path) {
|
|
63
|
+
case '/api/capabilities':
|
|
64
|
+
// apiVersion is what verifyServerApi requires; `limits` is what it
|
|
65
|
+
// learns from here instead of assuming.
|
|
66
|
+
return json(200, {
|
|
67
|
+
apiVersion: API_VERSION,
|
|
68
|
+
edition: 'test-stub',
|
|
69
|
+
authProvider: 'none',
|
|
70
|
+
oidcIssuer: null,
|
|
71
|
+
limits: { ingestConcurrencyPerTenant: 2, acceptsGzipBody: false },
|
|
72
|
+
});
|
|
73
|
+
case '/api/status':
|
|
74
|
+
// Used by `login` to verify a token, and by `status`.
|
|
75
|
+
return json(200, { ok: true, sessions: 0 });
|
|
76
|
+
case '/api/secrets/rules':
|
|
77
|
+
// No tenant rules; the collector's compiled-in redactors still apply.
|
|
78
|
+
return json(200, { version: 'stub-1', rules: [], pack: { version: 'stub-1', rules: [] } });
|
|
79
|
+
case '/api/teams/security-config':
|
|
80
|
+
return json(200, { verifySecrets: true, telemetry: false });
|
|
81
|
+
case '/api/sync-config/sources':
|
|
82
|
+
// The collector REPORTS its sources here; nothing to answer.
|
|
83
|
+
return json(200, { ok: true });
|
|
84
|
+
case '/api/sync-config':
|
|
85
|
+
// No server-side exclusions. An empty set here is a real answer, and
|
|
86
|
+
// the collector refuses to sync when it cannot get one at all.
|
|
87
|
+
return json(200, { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] });
|
|
88
|
+
case '/api/sync': {
|
|
89
|
+
const b = body;
|
|
90
|
+
if (Array.isArray(b?.conversations))
|
|
91
|
+
conversations.push(...b.conversations);
|
|
92
|
+
// `cli: null` so the collector never tries to self-update from a stub.
|
|
93
|
+
return json(200, { accepted: b?.conversations?.length ?? 0, full_resync_needed: [], cli: null, telemetry: false });
|
|
94
|
+
}
|
|
95
|
+
default:
|
|
96
|
+
// A 404 here is honest: this stub implements the sync path, not the
|
|
97
|
+
// whole API. Anything the collector needs and does not get shows up as
|
|
98
|
+
// a named endpoint in `requests`, which is how this stays minimal.
|
|
99
|
+
return json(404, { error: 'not implemented by the conformant stub', path });
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
});
|
|
103
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
104
|
+
const addr = server.address();
|
|
105
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
106
|
+
return {
|
|
107
|
+
url: `http://127.0.0.1:${port}`,
|
|
108
|
+
requests,
|
|
109
|
+
uploaded: () => conversations,
|
|
110
|
+
close: () => new Promise((resolve) => {
|
|
111
|
+
server.closeAllConnections?.();
|
|
112
|
+
server.close(() => resolve());
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=conformant-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformant-server.js","sourceRoot":"","sources":["../src/conformant-server.ts"],"names":[],"mappings":"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"}
|
package/dist/device-auth.d.ts
CHANGED
|
@@ -7,10 +7,6 @@ export interface DeviceTokens {
|
|
|
7
7
|
refreshToken?: string;
|
|
8
8
|
expiresIn: number;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Run the full device flow. `onPrompt` receives the human-facing instructions
|
|
12
|
-
* (verification URL + code) so the caller controls presentation.
|
|
13
|
-
*/
|
|
14
10
|
export declare function deviceLogin(opts: {
|
|
15
11
|
issuer?: string;
|
|
16
12
|
clientId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"AAyBA;iFACiF;AACjF,eAAO,MAAM,cAAc,QAA4C,CAAC;AACxE,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"AAyBA;iFACiF;AACjF,eAAO,MAAM,cAAc,QAA4C,CAAC;AACxE,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AA8DD,wBAAsB,WAAW,CAC/B,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,QAAQ,EAAE,CAAC,CAAC,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,GAChF,OAAO,CAAC,YAAY,CAAC,CAqEvB;AAED;yEACyE;AACzE,eAAO,MAAM,yBAAyB,oBAAoB,CAAC;AAE3D;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,CAAC,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,GAChF,OAAO,CAAC,YAAY,CAAC,CAiDvB"}
|
package/dist/device-auth.js
CHANGED
|
@@ -30,6 +30,47 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
30
30
|
* Run the full device flow. `onPrompt` receives the human-facing instructions
|
|
31
31
|
* (verification URL + code) so the caller controls presentation.
|
|
32
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* Read a device-code response, or say what answered instead.
|
|
35
|
+
*
|
|
36
|
+
* TWO FAILURES CAME THROUGH THIS ONE LINE (`await startRes.json()`):
|
|
37
|
+
*
|
|
38
|
+
* - a 200 carrying HTML — a login wall or a parked domain — threw
|
|
39
|
+
* "Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON", which names
|
|
40
|
+
* neither the host nor the fix;
|
|
41
|
+
* - a 200 carrying SOMEBODY ELSE'S JSON parsed fine, every field came back
|
|
42
|
+
* undefined, and the CLI printed
|
|
43
|
+
*
|
|
44
|
+
* To log in, open:
|
|
45
|
+
* undefined
|
|
46
|
+
* (if prompted, enter code: undefined at undefined)
|
|
47
|
+
* Waiting for approval…
|
|
48
|
+
*
|
|
49
|
+
* then polled a stranger's endpoint until the (undefined → NaN) deadline
|
|
50
|
+
* expired and reported "the code expired before approval". The user waited
|
|
51
|
+
* for a link that never existed.
|
|
52
|
+
*
|
|
53
|
+
* So: read as text, parse defensively, and require the three fields RFC 8628
|
|
54
|
+
* makes mandatory before telling a human to go and approve something.
|
|
55
|
+
*/
|
|
56
|
+
async function readDeviceCode(res, where) {
|
|
57
|
+
const text = await res.text().catch(() => '');
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(`${where} answered ${res.status} with something that is not a device-code response `
|
|
64
|
+
+ `(${JSON.stringify(text.trim().slice(0, 60))}) — this host is not serving the chat-recall API`);
|
|
65
|
+
}
|
|
66
|
+
const d = parsed;
|
|
67
|
+
if (typeof d?.device_code !== 'string' || typeof d?.user_code !== 'string'
|
|
68
|
+
|| typeof d?.verification_uri !== 'string' || typeof d?.expires_in !== 'number') {
|
|
69
|
+
throw new Error(`${where} answered ${res.status} with JSON that is not a device-code response `
|
|
70
|
+
+ `(${JSON.stringify(text.trim().slice(0, 60))}) — this host is not serving the chat-recall API`);
|
|
71
|
+
}
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
33
74
|
export async function deviceLogin(opts, onPrompt) {
|
|
34
75
|
const issuer = (opts.issuer || DEFAULT_ISSUER).replace(/\/+$/, '');
|
|
35
76
|
if (!issuer) {
|
|
@@ -51,7 +92,7 @@ export async function deviceLogin(opts, onPrompt) {
|
|
|
51
92
|
if (!startRes.ok) {
|
|
52
93
|
throw new Error(`device-auth init failed: HTTP ${startRes.status} ${await startRes.text().catch(() => '')}`);
|
|
53
94
|
}
|
|
54
|
-
const start =
|
|
95
|
+
const start = await readDeviceCode(startRes, issuer);
|
|
55
96
|
onPrompt({
|
|
56
97
|
url: start.verification_uri_complete || start.verification_uri,
|
|
57
98
|
userCode: start.user_code,
|
|
@@ -117,7 +158,7 @@ export async function betterAuthDeviceLogin(serverUrl, onPrompt) {
|
|
|
117
158
|
if (!startRes.ok) {
|
|
118
159
|
throw new Error(`device-auth init failed: HTTP ${startRes.status} ${await startRes.text().catch(() => '')}`);
|
|
119
160
|
}
|
|
120
|
-
const start =
|
|
161
|
+
const start = await readDeviceCode(startRes, base);
|
|
121
162
|
onPrompt({
|
|
122
163
|
url: start.verification_uri_complete || start.verification_uri,
|
|
123
164
|
userCode: start.user_code,
|
package/dist/device-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAE9G;iFACiF;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAiBnD,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEpE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAA4D,EAC5D,QAAiF;IAEjF,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,oEAAoE;cAClE,2DAA2D;cAC3D,mDAAmD;cACnD,4DAA4D,CAC/D,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,sBAAsB,CAAC;IAEnD,yEAAyE;IACzE,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7C,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,sCAAsC,EAAE;QAC5E,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,eAAe,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,EAAE,CAAC;KACxH,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/G,CAAC;IACD,MAAM,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type HostileMode =
|
|
2
|
+
/** A retired route behind Traefik/nginx: plain text, HTTP 404. THE one that
|
|
3
|
+
* produced the JSON parse error, because `404 page not found` parses as the
|
|
4
|
+
* number 404 and then fails at position 4. */
|
|
5
|
+
'text404'
|
|
6
|
+
/** A parked domain or a login wall: HTML with a 200. */
|
|
7
|
+
| 'html200'
|
|
8
|
+
/** An upstream that is up but broken. */
|
|
9
|
+
| 'gateway502'
|
|
10
|
+
/** Authenticated but forbidden — a revoked device token looks like this. */
|
|
11
|
+
| 'forbidden403'
|
|
12
|
+
/** 200, valid JSON, but not our API: a different service on the same host. */
|
|
13
|
+
| 'notOurApi'
|
|
14
|
+
/** Accepts the connection and never answers. A dropped packet looks like this
|
|
15
|
+
* to the client, and it is the failure people describe as "it just hangs". */
|
|
16
|
+
| 'silent';
|
|
17
|
+
export interface HostileServer {
|
|
18
|
+
url: string;
|
|
19
|
+
/** Every path the CLI actually asked for, in order. A test that only checks
|
|
20
|
+
* the printed message cannot tell "the CLI handled a 404 well" from "the CLI
|
|
21
|
+
* never sent a request" — and the first version of this harness spawned the
|
|
22
|
+
* CLI with execFileSync, which blocks the test process's event loop, so this
|
|
23
|
+
* server answered NOTHING and four assertions passed on a timeout. */
|
|
24
|
+
hits: string[];
|
|
25
|
+
close: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** Start one. Always on 127.0.0.1 and an ephemeral port, so tests can run in
|
|
28
|
+
* parallel and nothing is exposed. */
|
|
29
|
+
export declare function startHostileServer(mode: HostileMode): Promise<HostileServer>;
|
|
30
|
+
/**
|
|
31
|
+
* A port nothing listens on — connection refused, immediately. Distinct from
|
|
32
|
+
* 'silent' (accepted then ignored) and from a name that does not resolve.
|
|
33
|
+
*
|
|
34
|
+
* IT HAS TO BE A REAL PORT. This returned `http://127.0.0.1:1`, and port 1 is on
|
|
35
|
+
* the WHATWG "bad port" list, so undici refuses the request before it opens a
|
|
36
|
+
* socket and throws a bare `Error: bad port` with no code. The test asserting
|
|
37
|
+
* "a refused port says refused" was therefore never testing a refused port, and
|
|
38
|
+
* it failed against a CLI whose ECONNREFUSED handling was correct all along.
|
|
39
|
+
*
|
|
40
|
+
* Bind an ephemeral port, read it, close it: nothing else can be listening on it
|
|
41
|
+
* at that instant, and the number is one undici will actually dial.
|
|
42
|
+
*/
|
|
43
|
+
export declare function refusedUrl(): Promise<string>;
|
|
44
|
+
/** A hostname that cannot resolve. `.invalid` is reserved by RFC 2606 exactly so
|
|
45
|
+
* a test can rely on this without depending on somebody's DNS. */
|
|
46
|
+
export declare function unresolvableUrl(): string;
|
|
47
|
+
//# sourceMappingURL=hostile-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostile-server.d.ts","sourceRoot":"","sources":["../src/hostile-server.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,WAAW;AACrB;;+CAE+C;AAC7C,SAAS;AACX,wDAAwD;GACtD,SAAS;AACX,yCAAyC;GACvC,YAAY;AACd,4EAA4E;GAC1E,cAAc;AAChB,8EAA8E;GAC5E,WAAW;AACb;+EAC+E;GAC7E,QAAQ,CAAC;AAEb,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ;;;;2EAIuE;IACvE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED;uCACuC;AACvC,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CA+ClF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAOlD;AAED;mEACmE;AACnE,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Servers that answer BADLY, on demand, so CI can meet them before a user does.
|
|
3
|
+
*
|
|
4
|
+
* Every install bug found on 2026-08-27 was found by a person running a command
|
|
5
|
+
* on a laptop, because the test suite only ever pointed the CLI at a healthy
|
|
6
|
+
* server or at nothing:
|
|
7
|
+
*
|
|
8
|
+
* - a failed capabilities probe reported "No OIDC issuer configured for SSO
|
|
9
|
+
* login" on a service that has never used SSO;
|
|
10
|
+
* - that failure called process.exit(1), so `init` died before configuring MCP;
|
|
11
|
+
* - a retired hostname answered `404 page not found` in text/plain, and the
|
|
12
|
+
* sync client called .json() on it, so the user was shown "Unexpected
|
|
13
|
+
* non-whitespace character after JSON at position 4".
|
|
14
|
+
*
|
|
15
|
+
* None of those need a network to reproduce. They need a server that responds
|
|
16
|
+
* the way real infrastructure does when something is wrong — which is what this
|
|
17
|
+
* is. Each mode is one line of behaviour, deliberately: the point is to be
|
|
18
|
+
* obviously correct, not configurable.
|
|
19
|
+
*/
|
|
20
|
+
import { createServer } from 'node:http';
|
|
21
|
+
/** Start one. Always on 127.0.0.1 and an ephemeral port, so tests can run in
|
|
22
|
+
* parallel and nothing is exposed. */
|
|
23
|
+
export async function startHostileServer(mode) {
|
|
24
|
+
const hits = [];
|
|
25
|
+
const server = createServer((req, res) => {
|
|
26
|
+
hits.push(`${req.method} ${req.url}`);
|
|
27
|
+
switch (mode) {
|
|
28
|
+
case 'text404':
|
|
29
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
30
|
+
res.end('404 page not found');
|
|
31
|
+
return;
|
|
32
|
+
case 'html200':
|
|
33
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
34
|
+
res.end('<!DOCTYPE html><html><body>Nothing here</body></html>');
|
|
35
|
+
return;
|
|
36
|
+
case 'gateway502':
|
|
37
|
+
res.writeHead(502, { 'content-type': 'text/html' });
|
|
38
|
+
res.end('<html><head><title>502 Bad Gateway</title></head></html>');
|
|
39
|
+
return;
|
|
40
|
+
case 'forbidden403':
|
|
41
|
+
res.writeHead(403, { 'content-type': 'application/json' });
|
|
42
|
+
res.end(JSON.stringify({ error: 'forbidden' }));
|
|
43
|
+
return;
|
|
44
|
+
case 'notOurApi':
|
|
45
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
46
|
+
res.end(JSON.stringify({ service: 'something-else', version: 9 }));
|
|
47
|
+
return;
|
|
48
|
+
case 'silent':
|
|
49
|
+
// Deliberately no response and no close: the socket stays open until the
|
|
50
|
+
// client's own timeout fires. `req` is untouched on purpose.
|
|
51
|
+
void req;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
56
|
+
const addr = server.address();
|
|
57
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
58
|
+
return {
|
|
59
|
+
url: `http://127.0.0.1:${port}`,
|
|
60
|
+
hits,
|
|
61
|
+
close: () => new Promise((resolve) => {
|
|
62
|
+
// closeAllConnections, or a 'silent' server keeps the process alive with
|
|
63
|
+
// its open socket and the test run hangs at the end instead of exiting.
|
|
64
|
+
server.closeAllConnections?.();
|
|
65
|
+
server.close(() => resolve());
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A port nothing listens on — connection refused, immediately. Distinct from
|
|
71
|
+
* 'silent' (accepted then ignored) and from a name that does not resolve.
|
|
72
|
+
*
|
|
73
|
+
* IT HAS TO BE A REAL PORT. This returned `http://127.0.0.1:1`, and port 1 is on
|
|
74
|
+
* the WHATWG "bad port" list, so undici refuses the request before it opens a
|
|
75
|
+
* socket and throws a bare `Error: bad port` with no code. The test asserting
|
|
76
|
+
* "a refused port says refused" was therefore never testing a refused port, and
|
|
77
|
+
* it failed against a CLI whose ECONNREFUSED handling was correct all along.
|
|
78
|
+
*
|
|
79
|
+
* Bind an ephemeral port, read it, close it: nothing else can be listening on it
|
|
80
|
+
* at that instant, and the number is one undici will actually dial.
|
|
81
|
+
*/
|
|
82
|
+
export async function refusedUrl() {
|
|
83
|
+
const probe = createServer();
|
|
84
|
+
await new Promise((resolve) => probe.listen(0, '127.0.0.1', resolve));
|
|
85
|
+
const addr = probe.address();
|
|
86
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
87
|
+
await new Promise((resolve) => probe.close(() => resolve()));
|
|
88
|
+
return `http://127.0.0.1:${port}`;
|
|
89
|
+
}
|
|
90
|
+
/** A hostname that cannot resolve. `.invalid` is reserved by RFC 2606 exactly so
|
|
91
|
+
* a test can rely on this without depending on somebody's DNS. */
|
|
92
|
+
export function unresolvableUrl() {
|
|
93
|
+
return 'https://chat-recall-does-not-exist.invalid';
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=hostile-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostile-server.js","sourceRoot":"","sources":["../src/hostile-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The steps that may report. A closed set: an open one turns this into a log. */
|
|
2
|
+
export type InstallStep = 'cli_login_probe' | 'cli_login_device' | 'cli_mcp_config' | 'cli_first_sync' | 'cli_node_version';
|
|
3
|
+
export interface InstallFailure {
|
|
4
|
+
step: InstallStep;
|
|
5
|
+
/** The classification, not the prose: 'dns', 'tls', 'timeout', 'HTTP 502'. */
|
|
6
|
+
reason?: string | null;
|
|
7
|
+
/** One line, already free of paths. Truncated again server-side. */
|
|
8
|
+
message?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Post one failure. Resolves whether or not it was sent.
|
|
12
|
+
*
|
|
13
|
+
* `serverUrl` decides WHERE to report and is never part of the payload. When the
|
|
14
|
+
* user has opted out, or the server has not said telemetry is allowed, this does
|
|
15
|
+
* nothing at all.
|
|
16
|
+
*/
|
|
17
|
+
export declare function reportInstallFailure(serverUrl: string, failure: InstallFailure, opts?: {
|
|
18
|
+
fetchImpl?: typeof fetch;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}): Promise<boolean>;
|
|
21
|
+
//# sourceMappingURL=install-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-report.d.ts","sourceRoot":"","sources":["../src/install-report.ts"],"names":[],"mappings":"AA4BA,kFAAkF;AAClF,MAAM,MAAM,WAAW,GACnB,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAChB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1D,OAAO,CAAC,OAAO,CAAC,CAoClB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tell the server that an install FAILED, when nothing else can.
|
|
3
|
+
*
|
|
4
|
+
* Every other funnel event fires after the user got past login, so a first run
|
|
5
|
+
* that dies before that is invisible: no credential, no tenant, nothing to
|
|
6
|
+
* attribute. The way the last one was found was the founder running the command
|
|
7
|
+
* on his own laptop and pasting the output. This is the mechanism that replaces
|
|
8
|
+
* that.
|
|
9
|
+
*
|
|
10
|
+
* ── What it will not do ───────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* It sends a step name, a classification, a one-line message and three
|
|
13
|
+
* environment strings. Not a path, not an argument, not a stack trace, not a
|
|
14
|
+
* hostname — and `assertNoSensitiveKeys` enforces that structurally rather than
|
|
15
|
+
* by review, so the next person adding "just the server URL for context" gets a
|
|
16
|
+
* failure instead of a shipped leak.
|
|
17
|
+
*
|
|
18
|
+
* The server URL is the ONE thing worth wanting here and it is deliberately
|
|
19
|
+
* absent: a self-hoster's hostname is their infrastructure, and the failures we
|
|
20
|
+
* need to see are the hosted ones, which we already know the address of.
|
|
21
|
+
*
|
|
22
|
+
* ── Fire and forget, and silent ───────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* Never awaited on the user's critical path, never logged, never able to fail a
|
|
25
|
+
* command. A diagnostic that can break an install is worse than no diagnostic.
|
|
26
|
+
*/
|
|
27
|
+
import { mayReport, userConsents, assertNoSensitiveKeys } from './telemetry-consent.js';
|
|
28
|
+
/**
|
|
29
|
+
* Post one failure. Resolves whether or not it was sent.
|
|
30
|
+
*
|
|
31
|
+
* `serverUrl` decides WHERE to report and is never part of the payload. When the
|
|
32
|
+
* user has opted out, or the server has not said telemetry is allowed, this does
|
|
33
|
+
* nothing at all.
|
|
34
|
+
*/
|
|
35
|
+
export async function reportInstallFailure(serverUrl, failure, opts = {}) {
|
|
36
|
+
if (!serverUrl)
|
|
37
|
+
return false;
|
|
38
|
+
// mayReport() needs the server to have confirmed eligibility, which it cannot
|
|
39
|
+
// have done when the very first request failed — so consent alone is the gate
|
|
40
|
+
// here, and the payload carries nothing that needs a server's permission.
|
|
41
|
+
if (!userConsents() && !mayReport(serverUrl))
|
|
42
|
+
return false;
|
|
43
|
+
const payload = {
|
|
44
|
+
step: failure.step,
|
|
45
|
+
reason: trim(failure.reason, 40),
|
|
46
|
+
message: trim(failure.message, 200),
|
|
47
|
+
cliVersion: trim(process.env.CHAT_RECALL_CLI_VERSION, 20),
|
|
48
|
+
node: trim(process.versions.node, 20),
|
|
49
|
+
os: `${process.platform}-${process.arch}`,
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
// Structural check, not a promise in a comment. Throws on a forbidden key.
|
|
53
|
+
assertNoSensitiveKeys(payload);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const f = opts.fetchImpl || fetch;
|
|
59
|
+
try {
|
|
60
|
+
const res = await f(`${serverUrl.replace(/\/+$/, '')}/install/report`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(payload),
|
|
64
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 3000),
|
|
65
|
+
});
|
|
66
|
+
return res.ok || res.status === 204;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// The machine that cannot reach us cannot report. Known and accepted.
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function trim(v, n) {
|
|
74
|
+
return typeof v === 'string' && v.trim() ? v.trim().slice(0, n) : null;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=install-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-report.js","sourceRoot":"","sources":["../src/install-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAkBxF;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAiB,EACjB,OAAuB,EACvB,OAAyD,EAAE;IAE3D,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7B,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3D,MAAM,OAAO,GAAG;QACd,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAChC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;QACnC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,EAAE,CAAC;QACzD,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACrC,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE;KAC1C,CAAC;IAEF,IAAI,CAAC;QACH,2EAA2E;QAC3E,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,iBAAiB,EAAE;YACrE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YAC7B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;SACpD,CAAC,CAAC;QACH,OAAO,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;QACtE,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,IAAI,CAAC,CAAU,EAAE,CAAS;IACjC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzE,CAAC"}
|