@linzumi/cli 1.0.95 → 1.0.97
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/README.md +1 -1
- package/dist/index.js +349 -349
- package/dist/mcp-server.mjs +1 -1
- package/package.json +1 -1
- package/scripts/build-auth-parity-oracle.mjs +708 -0
- package/scripts/build-config-identity-parity-oracle.mjs +520 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 3, auth).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the auth cluster of
|
|
4
|
+
// the ReScript commander port, the sibling of
|
|
5
|
+
// build-protocol-parity-oracle.mjs (cluster 1) and
|
|
6
|
+
// build-config-identity-parity-oracle.mjs (cluster 2). The oracle bundles
|
|
7
|
+
// the REAL TS auth modules (oauth.ts, deviceAuthWait.ts, authResolution.ts,
|
|
8
|
+
// runnerTokenExpiry.ts, runnerTokenRenewer.ts, channelSessionSupport.ts's
|
|
9
|
+
// access-token claim slice, authCache.ts, mcpServer.ts's
|
|
10
|
+
// resolveMcpLocalRunnerToken, runner.ts's writeEphemeralMcpAuthFile, and
|
|
11
|
+
// signupServerClient.ts + signupFlow.ts's signup-auth transforms) behind
|
|
12
|
+
// the same one-shot stdin/stdout batch driver: a JSON array of cases in, a
|
|
13
|
+
// JSON array of results out.
|
|
14
|
+
//
|
|
15
|
+
// FLOW cases carry a `baseUrl`/`kandanUrl` pointing at the ReScript parity
|
|
16
|
+
// suite's scripted auth HTTP server (ScriptedAuthServer.res), so the REAL
|
|
17
|
+
// TS flow functions run their real fetch legs against the same scripted
|
|
18
|
+
// wire the ReScript port runs against - outcome equivalence is asserted on
|
|
19
|
+
// both sides by AuthParityConformance.res.
|
|
20
|
+
//
|
|
21
|
+
// Determinism: every clock is injected per case (pollDeviceCodeForToken's
|
|
22
|
+
// `now` dep, the device wait's deps.now, the renewer's deps.now/setTimer)
|
|
23
|
+
// or scoped through the fixed-Date override for the modules whose
|
|
24
|
+
// timestamps are not injectable (authCache's issuedAt, signupFlow's
|
|
25
|
+
// createdAt). Scratch paths normalize through the "$DIR" placeholder.
|
|
26
|
+
//
|
|
27
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
28
|
+
// tokens; nothing here logs case payloads.
|
|
29
|
+
import { copyFile, mkdir } from 'node:fs/promises';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { build } from 'esbuild';
|
|
33
|
+
|
|
34
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
35
|
+
|
|
36
|
+
const driverSource = `
|
|
37
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
38
|
+
import { tmpdir } from 'node:os';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
import {
|
|
41
|
+
deviceAuthFailureMessage,
|
|
42
|
+
kandanHttpBaseUrl,
|
|
43
|
+
pollDeviceCodeForToken,
|
|
44
|
+
renewLocalRunnerToken,
|
|
45
|
+
startDeviceAuthorization,
|
|
46
|
+
validateLocalRunnerTokenOutcome,
|
|
47
|
+
} from './src/oauth';
|
|
48
|
+
import {
|
|
49
|
+
deviceWaitPromptDecision,
|
|
50
|
+
renderDeviceWaitEvent,
|
|
51
|
+
waitForDeviceAuthorization,
|
|
52
|
+
} from './src/deviceAuthWait';
|
|
53
|
+
import {
|
|
54
|
+
resolveLocalRunnerToken,
|
|
55
|
+
unusableCachedTokenNotice,
|
|
56
|
+
} from './src/authResolution';
|
|
57
|
+
import { resolveRunnerTokenExpiresAt } from './src/runnerTokenExpiry';
|
|
58
|
+
import { createRunnerTokenRenewer } from './src/runnerTokenRenewer';
|
|
59
|
+
import {
|
|
60
|
+
accessTokenExpiresAtIso,
|
|
61
|
+
identityFromAccessToken,
|
|
62
|
+
singleWorkspaceScopeFromAccessToken,
|
|
63
|
+
} from './src/channelSessionSupport';
|
|
64
|
+
import {
|
|
65
|
+
readCachedLocalRunnerToken,
|
|
66
|
+
writeCachedLocalRunnerToken,
|
|
67
|
+
} from './src/authCache';
|
|
68
|
+
import { resolveMcpLocalRunnerToken } from './src/mcpServer';
|
|
69
|
+
import { writeEphemeralMcpAuthFile } from './src/runner';
|
|
70
|
+
import { createSignupServerClient } from './src/signupServerClient';
|
|
71
|
+
import {
|
|
72
|
+
comparableServiceUrl,
|
|
73
|
+
signupAuthFromVerification,
|
|
74
|
+
signupAuthMatchesServiceUrl,
|
|
75
|
+
} from './src/signupFlow';
|
|
76
|
+
|
|
77
|
+
// --- Shared plumbing (the cluster 2 conventions) -----------------------------------
|
|
78
|
+
|
|
79
|
+
const dirPlaceholder = '$DIR';
|
|
80
|
+
|
|
81
|
+
function scratch() {
|
|
82
|
+
return realpathSync(mkdtempSync(join(tmpdir(), 'linzumi-auth-oracle-')));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function substitute(value, from, to) {
|
|
86
|
+
if (typeof value === 'string') {
|
|
87
|
+
return value.split(from).join(to);
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
return value.map((entry) => substitute(entry, from, to));
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'object' && value !== null) {
|
|
93
|
+
return Object.fromEntries(
|
|
94
|
+
Object.entries(value).map(([key, entry]) => [
|
|
95
|
+
substitute(key, from, to),
|
|
96
|
+
substitute(entry, from, to),
|
|
97
|
+
])
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function outcome(run) {
|
|
104
|
+
try {
|
|
105
|
+
const value = run();
|
|
106
|
+
return value === undefined ? { defined: false } : { defined: true, value };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return errorOutcome(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function outcomeAsync(run) {
|
|
113
|
+
try {
|
|
114
|
+
const value = await run();
|
|
115
|
+
return value === undefined ? { defined: false } : { defined: true, value };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return errorOutcome(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function errorOutcome(error) {
|
|
122
|
+
const shaped = {
|
|
123
|
+
threw: true,
|
|
124
|
+
message: error instanceof Error ? error.message : String(error),
|
|
125
|
+
};
|
|
126
|
+
if (error instanceof Error && error.name === 'SignupServerError') {
|
|
127
|
+
shaped.name = error.name;
|
|
128
|
+
shaped.status = error.status;
|
|
129
|
+
shaped.code = error.code;
|
|
130
|
+
}
|
|
131
|
+
return shaped;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const RealDate = Date;
|
|
135
|
+
|
|
136
|
+
function withFixedNow(nowMs, run) {
|
|
137
|
+
if (nowMs === undefined) {
|
|
138
|
+
return run();
|
|
139
|
+
}
|
|
140
|
+
class FixedDate extends RealDate {
|
|
141
|
+
constructor(...args) {
|
|
142
|
+
if (args.length === 0) {
|
|
143
|
+
super(nowMs);
|
|
144
|
+
} else {
|
|
145
|
+
super(...args);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
static now() {
|
|
149
|
+
return nowMs;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
globalThis.Date = FixedDate;
|
|
153
|
+
try {
|
|
154
|
+
return run();
|
|
155
|
+
} finally {
|
|
156
|
+
globalThis.Date = RealDate;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A deterministic clock: starts at startMs and advances by stepMs per
|
|
161
|
+
// read, matching AuthParityConformance's steppedNow.
|
|
162
|
+
function steppedNow(startMs, stepMs) {
|
|
163
|
+
let reads = 0;
|
|
164
|
+
return () => {
|
|
165
|
+
const value = startMs + reads * stepMs;
|
|
166
|
+
reads += 1;
|
|
167
|
+
return value;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function fileMode(path) {
|
|
172
|
+
return statSync(path).mode & 0o777;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Pure transforms -----------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function handleAccessTokenClaims(testCase) {
|
|
178
|
+
return {
|
|
179
|
+
identity: outcome(() => identityFromAccessToken(testCase.token)),
|
|
180
|
+
workspaceScope: outcome(() => singleWorkspaceScopeFromAccessToken(testCase.token)),
|
|
181
|
+
expiresAtIso: outcome(() => accessTokenExpiresAtIso(testCase.token)),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function handleDeviceAuthFailureMessage(testCase) {
|
|
186
|
+
return outcome(() =>
|
|
187
|
+
deviceAuthFailureMessage(testCase.error ?? undefined, testCase.httpStatus ?? undefined)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleDeviceWaitPromptDecision(testCase) {
|
|
192
|
+
return outcome(() =>
|
|
193
|
+
deviceWaitPromptDecision({
|
|
194
|
+
cycle: testCase.cycle,
|
|
195
|
+
nowMs: testCase.nowMs,
|
|
196
|
+
lastLoudPromptAtMs: testCase.lastLoudPromptAtMs ?? undefined,
|
|
197
|
+
isInteractive: testCase.isInteractive,
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleRenderDeviceWaitEvent(testCase) {
|
|
203
|
+
return outcome(() => renderDeviceWaitEvent(testCase.event));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleUnusableCachedTokenNotice(testCase) {
|
|
207
|
+
return outcome(() => unusableCachedTokenNotice(testCase.outcome));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function handleKandanHttpBaseUrl(testCase) {
|
|
211
|
+
return outcome(() => kandanHttpBaseUrl(testCase.kandanUrl));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleComparableServiceUrl(testCase) {
|
|
215
|
+
return outcome(() => comparableServiceUrl(testCase.value));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handleSignupAuthMatch(testCase) {
|
|
219
|
+
return outcome(() => signupAuthMatchesServiceUrl(testCase.auth, testCase.serviceUrl));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function handleSignupAuthFromVerification(testCase) {
|
|
223
|
+
return withFixedNow(testCase.nowMs, () =>
|
|
224
|
+
outcome(() => signupAuthFromVerification(testCase.verifiedAuth, testCase.serviceUrl))
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Token expiry resolution -----------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
function handleRunnerTokenExpiry(testCase) {
|
|
231
|
+
const dir = scratch();
|
|
232
|
+
const authFilePath = join(dir, 'auth.json');
|
|
233
|
+
if (testCase.authFileContents !== undefined) {
|
|
234
|
+
writeFileSync(authFilePath, testCase.authFileContents, 'utf8');
|
|
235
|
+
}
|
|
236
|
+
const result = withFixedNow(testCase.nowMs, () =>
|
|
237
|
+
outcome(() =>
|
|
238
|
+
resolveRunnerTokenExpiresAt({
|
|
239
|
+
kandanUrl: testCase.kandanUrl,
|
|
240
|
+
token: testCase.token,
|
|
241
|
+
explicitToken: testCase.explicitToken ?? undefined,
|
|
242
|
+
authFilePath,
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
return substitute(result, dir, dirPlaceholder);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Token renewer (scripted renewal sequence, recorded timers) -------------------------
|
|
250
|
+
|
|
251
|
+
async function handleTokenRenewer(testCase) {
|
|
252
|
+
const script = [...(testCase.script ?? [])];
|
|
253
|
+
const timers = [];
|
|
254
|
+
const logs = [];
|
|
255
|
+
const persisted = [];
|
|
256
|
+
const now = steppedNow(testCase.nowStartMs, testCase.nowStepMs);
|
|
257
|
+
const renewer = createRunnerTokenRenewer({
|
|
258
|
+
initialToken: testCase.initialToken,
|
|
259
|
+
tokenExpiresAt: testCase.tokenExpiresAt ?? undefined,
|
|
260
|
+
deps: {
|
|
261
|
+
renew: async () => {
|
|
262
|
+
const step = script.shift();
|
|
263
|
+
if (step === undefined) {
|
|
264
|
+
throw new Error('renewal script exhausted');
|
|
265
|
+
}
|
|
266
|
+
if (step.result === 'reject') {
|
|
267
|
+
throw new Error(step.message);
|
|
268
|
+
}
|
|
269
|
+
if (step.result === 'renewed') {
|
|
270
|
+
return {
|
|
271
|
+
kind: 'renewed',
|
|
272
|
+
accessToken: step.accessToken,
|
|
273
|
+
expiresInSeconds: step.expiresInSeconds ?? undefined,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return { kind: step.result };
|
|
277
|
+
},
|
|
278
|
+
persist:
|
|
279
|
+
testCase.persistFails === true
|
|
280
|
+
? () => {
|
|
281
|
+
throw new Error(testCase.persistFailureMessage);
|
|
282
|
+
}
|
|
283
|
+
: (args) => {
|
|
284
|
+
persisted.push({
|
|
285
|
+
accessToken: args.accessToken,
|
|
286
|
+
expiresInSeconds: args.expiresInSeconds ?? null,
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
log: (event, payload) => {
|
|
290
|
+
logs.push({ event, payload });
|
|
291
|
+
},
|
|
292
|
+
now,
|
|
293
|
+
// Timers are RECORDED, never fired: the parity fixture drives
|
|
294
|
+
// renewNow() explicitly so both impls observe the same schedule.
|
|
295
|
+
setTimer: (_callback, delayMs) => {
|
|
296
|
+
timers.push(delayMs);
|
|
297
|
+
return timers.length;
|
|
298
|
+
},
|
|
299
|
+
clearTimer: () => {},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
renewer.start();
|
|
303
|
+
const results = [];
|
|
304
|
+
const renewCalls = testCase.renewCalls ?? (testCase.script ?? []).length;
|
|
305
|
+
for (let call = 0; call < renewCalls; call += 1) {
|
|
306
|
+
results.push(await outcomeAsync(() => renewer.renewNow()));
|
|
307
|
+
}
|
|
308
|
+
const overlap = [];
|
|
309
|
+
if (testCase.probeOverlap === true) {
|
|
310
|
+
// Fire two renewNow calls into the same in-flight renewal: the second
|
|
311
|
+
// must observe in_progress.
|
|
312
|
+
const first = renewer.renewNow();
|
|
313
|
+
const second = await renewer.renewNow();
|
|
314
|
+
overlap.push(await outcomeAsync(() => first), { defined: true, value: second });
|
|
315
|
+
}
|
|
316
|
+
renewer.stop();
|
|
317
|
+
return {
|
|
318
|
+
results,
|
|
319
|
+
overlap,
|
|
320
|
+
currentToken: renewer.currentToken(),
|
|
321
|
+
timers,
|
|
322
|
+
logs,
|
|
323
|
+
persisted,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- HTTP flow legs (against the scripted auth server) ----------------------------------
|
|
328
|
+
|
|
329
|
+
async function handleValidate(testCase) {
|
|
330
|
+
return outcomeAsync(() =>
|
|
331
|
+
validateLocalRunnerTokenOutcome({
|
|
332
|
+
kandanUrl: testCase.kandanUrl,
|
|
333
|
+
accessToken: testCase.accessToken,
|
|
334
|
+
workspaceSlug: testCase.workspaceSlug ?? undefined,
|
|
335
|
+
channelSlug: testCase.channelSlug ?? undefined,
|
|
336
|
+
requiredScopes: testCase.requiredScopes ?? undefined,
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function handleRenew(testCase) {
|
|
342
|
+
return outcomeAsync(() =>
|
|
343
|
+
renewLocalRunnerToken({
|
|
344
|
+
kandanUrl: testCase.kandanUrl,
|
|
345
|
+
accessToken: testCase.accessToken,
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function handleDeviceStart(testCase) {
|
|
351
|
+
return outcomeAsync(() =>
|
|
352
|
+
startDeviceAuthorization({
|
|
353
|
+
httpBaseUrl: testCase.httpBaseUrl,
|
|
354
|
+
workspaceSlug: testCase.workspaceSlug ?? undefined,
|
|
355
|
+
channelSlug: testCase.channelSlug ?? undefined,
|
|
356
|
+
onboarding: testCase.onboarding ?? undefined,
|
|
357
|
+
identity: testCase.identity ?? undefined,
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function handleDevicePoll(testCase) {
|
|
363
|
+
return outcomeAsync(() =>
|
|
364
|
+
pollDeviceCodeForToken({
|
|
365
|
+
httpBaseUrl: testCase.httpBaseUrl,
|
|
366
|
+
deviceCode: testCase.deviceCode,
|
|
367
|
+
expiresInSeconds: testCase.expiresInSeconds ?? undefined,
|
|
368
|
+
now:
|
|
369
|
+
testCase.nowStartMs === undefined
|
|
370
|
+
? undefined
|
|
371
|
+
: steppedNow(testCase.nowStartMs, testCase.nowStepMs),
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function handleDeviceWaitFlow(testCase) {
|
|
377
|
+
const events = [];
|
|
378
|
+
const logs = [];
|
|
379
|
+
const sleeps = [];
|
|
380
|
+
const result = await outcomeAsync(() =>
|
|
381
|
+
waitForDeviceAuthorization({
|
|
382
|
+
kandanUrl: testCase.kandanUrl,
|
|
383
|
+
workspaceSlug: testCase.workspaceSlug ?? undefined,
|
|
384
|
+
channelSlug: testCase.channelSlug ?? undefined,
|
|
385
|
+
onboarding: testCase.onboarding ?? undefined,
|
|
386
|
+
identity: testCase.identity ?? undefined,
|
|
387
|
+
isInteractive: testCase.isInteractive,
|
|
388
|
+
deps: {
|
|
389
|
+
now: steppedNow(testCase.nowStartMs, testCase.nowStepMs),
|
|
390
|
+
sleep: async (ms) => {
|
|
391
|
+
sleeps.push(ms);
|
|
392
|
+
},
|
|
393
|
+
report: (event) => {
|
|
394
|
+
events.push(event);
|
|
395
|
+
},
|
|
396
|
+
log: (event, payload) => {
|
|
397
|
+
logs.push({ event, payload });
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
return { result, events, logs, sleeps };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The device acquisition composition resolveTokenFlow injects: the REAL
|
|
406
|
+
// persistent wait + the REAL cache write, minus the machine-identity read
|
|
407
|
+
// (which would touch the real ~/.linzumi; identity parity is pinned by the
|
|
408
|
+
// cluster 2 fingerprint cases).
|
|
409
|
+
function deviceAcquire(testCase, authFilePath, waitEvents) {
|
|
410
|
+
return async (args) => {
|
|
411
|
+
const token = await waitForDeviceAuthorization({
|
|
412
|
+
kandanUrl: args.kandanUrl,
|
|
413
|
+
workspaceSlug: args.workspaceSlug,
|
|
414
|
+
channelSlug: args.channelSlug,
|
|
415
|
+
onboarding: args.onboarding,
|
|
416
|
+
isInteractive: false,
|
|
417
|
+
deps: {
|
|
418
|
+
now: steppedNow(testCase.nowStartMs, testCase.nowStepMs),
|
|
419
|
+
sleep: async () => {},
|
|
420
|
+
report: (event) => {
|
|
421
|
+
waitEvents.push(event);
|
|
422
|
+
},
|
|
423
|
+
log: () => {},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
withFixedNow(testCase.nowMs, () =>
|
|
427
|
+
writeCachedLocalRunnerToken({
|
|
428
|
+
kandanUrl: args.kandanUrl,
|
|
429
|
+
accessToken: token.accessToken,
|
|
430
|
+
expiresInSeconds: token.expiresInSeconds,
|
|
431
|
+
authFilePath,
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
return token.accessToken;
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function handleResolveTokenFlow(testCase) {
|
|
439
|
+
const dir = scratch();
|
|
440
|
+
const authFilePath = join(dir, 'auth.json');
|
|
441
|
+
if (testCase.authFileContents !== undefined) {
|
|
442
|
+
writeFileSync(authFilePath, testCase.authFileContents, 'utf8');
|
|
443
|
+
}
|
|
444
|
+
const reported = [];
|
|
445
|
+
const waitEvents = [];
|
|
446
|
+
const result = await outcomeAsync(() =>
|
|
447
|
+
resolveLocalRunnerToken(
|
|
448
|
+
{
|
|
449
|
+
kandanUrl: testCase.kandanUrl,
|
|
450
|
+
explicitToken: testCase.explicitToken ?? undefined,
|
|
451
|
+
workspaceSlug: testCase.workspaceSlug ?? undefined,
|
|
452
|
+
channelSlug: testCase.channelSlug ?? undefined,
|
|
453
|
+
requiredScopes: testCase.requiredScopes ?? undefined,
|
|
454
|
+
authFilePath,
|
|
455
|
+
reportRejectedCachedToken: (outcomeName) => {
|
|
456
|
+
reported.push(outcomeName);
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
readCachedToken: (kandanUrl, path) =>
|
|
461
|
+
withFixedNow(testCase.nowMs, () => readCachedLocalRunnerToken(kandanUrl, path)),
|
|
462
|
+
validateToken: validateLocalRunnerTokenOutcome,
|
|
463
|
+
acquireAndCacheToken: deviceAcquire(testCase, authFilePath, waitEvents),
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
let authFileBytes = null;
|
|
468
|
+
try {
|
|
469
|
+
authFileBytes = readFileSync(authFilePath, 'utf8');
|
|
470
|
+
} catch {
|
|
471
|
+
authFileBytes = null;
|
|
472
|
+
}
|
|
473
|
+
return substitute({ result, reported, waitEvents, authFileBytes }, dir, dirPlaceholder);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function handleResolveMcpTokenFlow(testCase) {
|
|
477
|
+
const dir = scratch();
|
|
478
|
+
const authFilePath = join(dir, 'auth.json');
|
|
479
|
+
if (testCase.authFileContents !== undefined) {
|
|
480
|
+
writeFileSync(authFilePath, testCase.authFileContents, 'utf8');
|
|
481
|
+
}
|
|
482
|
+
const waitEvents = [];
|
|
483
|
+
const result = await outcomeAsync(() =>
|
|
484
|
+
resolveMcpLocalRunnerToken(
|
|
485
|
+
{
|
|
486
|
+
kandanUrl: testCase.kandanUrl,
|
|
487
|
+
explicitToken: testCase.explicitToken ?? undefined,
|
|
488
|
+
authFilePath,
|
|
489
|
+
workspaceSlug: testCase.workspaceSlug ?? undefined,
|
|
490
|
+
channelSlug: testCase.channelSlug ?? undefined,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
readCachedToken: (kandanUrl, path) =>
|
|
494
|
+
withFixedNow(testCase.nowMs, () => readCachedLocalRunnerToken(kandanUrl, path)),
|
|
495
|
+
validateTokenOutcome: validateLocalRunnerTokenOutcome,
|
|
496
|
+
resolveToken: (args) =>
|
|
497
|
+
resolveLocalRunnerToken(args, {
|
|
498
|
+
readCachedToken: (kandanUrl, path) =>
|
|
499
|
+
withFixedNow(testCase.nowMs, () => readCachedLocalRunnerToken(kandanUrl, path)),
|
|
500
|
+
validateToken: validateLocalRunnerTokenOutcome,
|
|
501
|
+
acquireAndCacheToken: deviceAcquire(testCase, authFilePath, waitEvents),
|
|
502
|
+
}),
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
let authFileBytes = null;
|
|
507
|
+
try {
|
|
508
|
+
authFileBytes = readFileSync(authFilePath, 'utf8');
|
|
509
|
+
} catch {
|
|
510
|
+
authFileBytes = null;
|
|
511
|
+
}
|
|
512
|
+
return substitute({ result, waitEvents, authFileBytes }, dir, dirPlaceholder);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// --- MCP ephemeral auth file (cross-impl legs) -------------------------------------------
|
|
516
|
+
|
|
517
|
+
function handleMintMcpAuthFile(testCase) {
|
|
518
|
+
const minted = withFixedNow(testCase.nowMs, () =>
|
|
519
|
+
outcome(() =>
|
|
520
|
+
writeEphemeralMcpAuthFile({
|
|
521
|
+
kandanUrl: testCase.kandanUrl,
|
|
522
|
+
token: testCase.token,
|
|
523
|
+
linzumiAgentMcpToken: testCase.linzumiAgentMcpToken ?? undefined,
|
|
524
|
+
})
|
|
525
|
+
)
|
|
526
|
+
);
|
|
527
|
+
if (minted.threw === true) {
|
|
528
|
+
return { minted };
|
|
529
|
+
}
|
|
530
|
+
const { path, cleanup } = minted.value;
|
|
531
|
+
const directory = join(path, '..');
|
|
532
|
+
const shaped = {
|
|
533
|
+
fileBytes: readFileSync(path, 'utf8'),
|
|
534
|
+
dirMode: fileMode(directory),
|
|
535
|
+
fileMode: fileMode(path),
|
|
536
|
+
readBack: withFixedNow(testCase.nowMs, () =>
|
|
537
|
+
outcome(() => readCachedLocalRunnerToken(testCase.kandanUrl, path))
|
|
538
|
+
),
|
|
539
|
+
};
|
|
540
|
+
if (testCase.keep === true) {
|
|
541
|
+
// The ReScript suite consumes this real path cross-impl, then removes it.
|
|
542
|
+
shaped.keptPath = path;
|
|
543
|
+
} else {
|
|
544
|
+
cleanup();
|
|
545
|
+
}
|
|
546
|
+
return { minted: { defined: true, value: 'minted' }, ...shaped };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function handleReadCachedTokenAtPath(testCase) {
|
|
550
|
+
return withFixedNow(testCase.nowMs, () =>
|
|
551
|
+
outcome(() => readCachedLocalRunnerToken(testCase.kandanUrl, testCase.authFilePath))
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function handleWriteCachedTokenAtPath(testCase) {
|
|
556
|
+
return withFixedNow(testCase.nowMs, () =>
|
|
557
|
+
outcome(() =>
|
|
558
|
+
writeCachedLocalRunnerToken({
|
|
559
|
+
kandanUrl: testCase.kandanUrl,
|
|
560
|
+
accessToken: testCase.accessToken,
|
|
561
|
+
expiresInSeconds: testCase.expiresInSeconds ?? undefined,
|
|
562
|
+
authFilePath: testCase.authFilePath,
|
|
563
|
+
})
|
|
564
|
+
)
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function handleRemovePath(testCase) {
|
|
569
|
+
return outcome(() => {
|
|
570
|
+
rmSync(testCase.path, { recursive: true, force: true });
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// --- Signup client (live against the scripted server) --------------------------------------
|
|
575
|
+
|
|
576
|
+
async function handleSignupClientLive(testCase) {
|
|
577
|
+
const client = createSignupServerClient({ serviceUrl: testCase.serviceUrl });
|
|
578
|
+
switch (testCase.leg) {
|
|
579
|
+
case 'startEmailCode':
|
|
580
|
+
return outcomeAsync(() =>
|
|
581
|
+
client.startEmailCode({
|
|
582
|
+
email: testCase.email,
|
|
583
|
+
mode: testCase.mode,
|
|
584
|
+
selectedTasks: testCase.selectedTasks ?? undefined,
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
case 'verifyEmailCode':
|
|
588
|
+
return outcomeAsync(() =>
|
|
589
|
+
client.verifyEmailCode({ pendingId: testCase.pendingId, code: testCase.code })
|
|
590
|
+
);
|
|
591
|
+
case 'validateAuth':
|
|
592
|
+
return outcomeAsync(() => client.validateAuth({ accessToken: testCase.accessToken }));
|
|
593
|
+
default:
|
|
594
|
+
return { threw: true, message: 'unknown signup leg ' + String(testCase.leg) };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// --- Dispatch --------------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
async function handle(testCase) {
|
|
601
|
+
switch (testCase.op) {
|
|
602
|
+
case 'accessTokenClaims':
|
|
603
|
+
return handleAccessTokenClaims(testCase);
|
|
604
|
+
case 'deviceAuthFailureMessage':
|
|
605
|
+
return handleDeviceAuthFailureMessage(testCase);
|
|
606
|
+
case 'deviceWaitPromptDecision':
|
|
607
|
+
return handleDeviceWaitPromptDecision(testCase);
|
|
608
|
+
case 'renderDeviceWaitEvent':
|
|
609
|
+
return handleRenderDeviceWaitEvent(testCase);
|
|
610
|
+
case 'unusableCachedTokenNotice':
|
|
611
|
+
return handleUnusableCachedTokenNotice(testCase);
|
|
612
|
+
case 'kandanHttpBaseUrl':
|
|
613
|
+
return handleKandanHttpBaseUrl(testCase);
|
|
614
|
+
case 'comparableServiceUrl':
|
|
615
|
+
return handleComparableServiceUrl(testCase);
|
|
616
|
+
case 'signupAuthMatch':
|
|
617
|
+
return handleSignupAuthMatch(testCase);
|
|
618
|
+
case 'signupAuthFromVerification':
|
|
619
|
+
return handleSignupAuthFromVerification(testCase);
|
|
620
|
+
case 'runnerTokenExpiry':
|
|
621
|
+
return handleRunnerTokenExpiry(testCase);
|
|
622
|
+
case 'tokenRenewer':
|
|
623
|
+
return handleTokenRenewer(testCase);
|
|
624
|
+
case 'validate':
|
|
625
|
+
return handleValidate(testCase);
|
|
626
|
+
case 'renew':
|
|
627
|
+
return handleRenew(testCase);
|
|
628
|
+
case 'deviceStart':
|
|
629
|
+
return handleDeviceStart(testCase);
|
|
630
|
+
case 'devicePoll':
|
|
631
|
+
return handleDevicePoll(testCase);
|
|
632
|
+
case 'deviceWaitFlow':
|
|
633
|
+
return handleDeviceWaitFlow(testCase);
|
|
634
|
+
case 'resolveTokenFlow':
|
|
635
|
+
return handleResolveTokenFlow(testCase);
|
|
636
|
+
case 'resolveMcpTokenFlow':
|
|
637
|
+
return handleResolveMcpTokenFlow(testCase);
|
|
638
|
+
case 'mintMcpAuthFile':
|
|
639
|
+
return handleMintMcpAuthFile(testCase);
|
|
640
|
+
case 'readCachedTokenAtPath':
|
|
641
|
+
return handleReadCachedTokenAtPath(testCase);
|
|
642
|
+
case 'writeCachedTokenAtPath':
|
|
643
|
+
return handleWriteCachedTokenAtPath(testCase);
|
|
644
|
+
case 'removePath':
|
|
645
|
+
return handleRemovePath(testCase);
|
|
646
|
+
case 'signupClientLive':
|
|
647
|
+
return handleSignupClientLive(testCase);
|
|
648
|
+
default:
|
|
649
|
+
return { threw: true, message: 'unknown op ' + String(testCase.op) };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let stdin = '';
|
|
654
|
+
process.stdin.setEncoding('utf8');
|
|
655
|
+
for await (const chunk of process.stdin) {
|
|
656
|
+
stdin += chunk;
|
|
657
|
+
}
|
|
658
|
+
const cases = JSON.parse(stdin);
|
|
659
|
+
const results = [];
|
|
660
|
+
// Sequential on purpose: the fixed-Date override and the scripted server's
|
|
661
|
+
// per-route response queues both assume one case at a time.
|
|
662
|
+
for (const testCase of cases) {
|
|
663
|
+
results.push(await handle(testCase));
|
|
664
|
+
}
|
|
665
|
+
process.stdout.write(JSON.stringify(results));
|
|
666
|
+
`;
|
|
667
|
+
|
|
668
|
+
await mkdir(join(packageRoot, '.differential'), { recursive: true });
|
|
669
|
+
|
|
670
|
+
// runner.ts's graph resolves assets/ relative to the bundle at import time
|
|
671
|
+
// (same requirement as build-differential-entry.mjs).
|
|
672
|
+
await mkdir(join(packageRoot, '.differential/assets'), { recursive: true });
|
|
673
|
+
await copyFile(
|
|
674
|
+
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|
|
675
|
+
join(packageRoot, '.differential/assets/linzumi-logo.svg')
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
// Renamed banner import (the build-differential-entry.mjs convention):
|
|
679
|
+
// this bundle keeps source-level identifiers and the commander sources
|
|
680
|
+
// import createRequire themselves - a bare banner name would collide. The
|
|
681
|
+
// banner is needed at all because CJS transitive deps (@inquirer's
|
|
682
|
+
// yoctocolors) require() node builtins at module scope.
|
|
683
|
+
const banner = {
|
|
684
|
+
js: "import { createRequire as __authOracleCreateRequire } from 'node:module';\nconst require = __authOracleCreateRequire(import.meta.url);",
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
await build({
|
|
688
|
+
stdin: {
|
|
689
|
+
contents: driverSource,
|
|
690
|
+
resolveDir: packageRoot,
|
|
691
|
+
sourcefile: 'auth-parity-driver.ts',
|
|
692
|
+
loader: 'ts',
|
|
693
|
+
},
|
|
694
|
+
banner,
|
|
695
|
+
bundle: true,
|
|
696
|
+
platform: 'node',
|
|
697
|
+
target: 'node20',
|
|
698
|
+
format: 'esm',
|
|
699
|
+
outfile: join(packageRoot, '.differential/auth-parity-oracle.mjs'),
|
|
700
|
+
minify: false,
|
|
701
|
+
sourcemap: false,
|
|
702
|
+
legalComments: 'none',
|
|
703
|
+
// runner.ts/mcpServer.ts/signupFlow.ts pull the wider commander graph;
|
|
704
|
+
// the same runtime externals as the differential entry build stay
|
|
705
|
+
// external (installed in this package's node_modules; the oracle only
|
|
706
|
+
// exercises the auth exports).
|
|
707
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
708
|
+
});
|