@paradigma-inc/flywheel 0.2.29 → 0.2.31
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/package.json +1 -1
- package/src/cli.mjs +7 -1
- package/src/runtime/vendor/flywheel-cli-dist/generatedProductTelemetryContract.js +10 -2
- package/src/runtime/vendor/flywheel-cli-dist/generatedProductTelemetryContract.js.map +1 -1
- package/src/runtime/vendor/manifest.json +1 -1
- package/src/setup/modes/cli.mjs +4 -0
- package/src/setup/modes/mcp-command.mjs +88 -71
- package/src/setup-auth.mjs +264 -56
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generatedProductTelemetryContract.js","sourceRoot":"","sources":["../src/generatedProductTelemetryContract.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"generatedProductTelemetryContract.js","sourceRoot":"","sources":["../src/generatedProductTelemetryContract.ts"],"names":[],"mappings":"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"}
|
package/src/setup/modes/cli.mjs
CHANGED
|
@@ -199,6 +199,7 @@ export async function runCliSetupMode({
|
|
|
199
199
|
cleanupSkillArtifactsForHosts = cleanupInstalledSkillArtifactsForHosts,
|
|
200
200
|
removeDir = rm,
|
|
201
201
|
resolveSetupApiKeyFn = resolveSetupApiKey,
|
|
202
|
+
onStartRetry,
|
|
202
203
|
writeRuntimeConfigFn = writeRuntimeConfig,
|
|
203
204
|
validateRemoteReadinessFn = validateCandidateRemoteReadiness,
|
|
204
205
|
resolveFlywheelBinaryPath = resolveFlywheelBinaryOnPath,
|
|
@@ -313,6 +314,9 @@ export async function runCliSetupMode({
|
|
|
313
314
|
authMode,
|
|
314
315
|
},
|
|
315
316
|
baseUrl,
|
|
317
|
+
// Presentation-only progress for scheduled start retries; never part
|
|
318
|
+
// of the setup result.
|
|
319
|
+
onStartRetry,
|
|
316
320
|
});
|
|
317
321
|
await validateRemoteReadinessFn({
|
|
318
322
|
baseUrl,
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
readYamlConfig,
|
|
21
21
|
resolveMcpPath,
|
|
22
22
|
} from "../../mcp-writer.mjs";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
formatStartRetryMessage,
|
|
25
|
+
resolveSetupApiKey,
|
|
26
|
+
} from "../../setup-auth.mjs";
|
|
24
27
|
import {
|
|
25
28
|
cleanupInstalledSkillArtifactsForHosts,
|
|
26
29
|
formatSupportedInstallSkillHosts,
|
|
@@ -319,20 +322,31 @@ async function resolveHosts(options, scope, serverName, allowPrompt = true) {
|
|
|
319
322
|
return selected;
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
async function resolveApiKey(
|
|
325
|
+
async function resolveApiKey(
|
|
326
|
+
options,
|
|
327
|
+
baseUrl,
|
|
328
|
+
{
|
|
329
|
+
resolveSetupApiKeyFn = resolveSetupApiKey,
|
|
330
|
+
createAuthSpinner = (text) => ora(text).start(),
|
|
331
|
+
} = {},
|
|
332
|
+
) {
|
|
323
333
|
if (Object.hasOwn(options, "apiKey")) {
|
|
324
|
-
const resolved = await
|
|
334
|
+
const resolved = await resolveSetupApiKeyFn({
|
|
325
335
|
options,
|
|
326
336
|
baseUrl,
|
|
327
337
|
});
|
|
328
338
|
return resolved.apiKey;
|
|
329
339
|
}
|
|
330
340
|
|
|
331
|
-
const spinner =
|
|
341
|
+
const spinner = createAuthSpinner("Configuring authentication...");
|
|
332
342
|
try {
|
|
333
|
-
const resolved = await
|
|
343
|
+
const resolved = await resolveSetupApiKeyFn({
|
|
334
344
|
options,
|
|
335
345
|
baseUrl,
|
|
346
|
+
// Retry progress is presentation-only: update the current spinner text.
|
|
347
|
+
onStartRetry: (progress) => {
|
|
348
|
+
spinner.text = formatStartRetryMessage(progress);
|
|
349
|
+
},
|
|
336
350
|
});
|
|
337
351
|
spinner.succeed(`Authenticated (${resolved.authMode})`);
|
|
338
352
|
return resolved.apiKey;
|
|
@@ -586,6 +600,8 @@ function printSetupResultSummary(setupResult) {
|
|
|
586
600
|
|
|
587
601
|
export async function runMcpModeCommand(options, deps = {}) {
|
|
588
602
|
const projectRoot = process.cwd();
|
|
603
|
+
const detectPriorModeFn = deps.detectPriorModeForSetupFn ?? detectPriorModeForSetup;
|
|
604
|
+
const installSkillForHostsFn = deps.installSkillForHosts ?? installBundledSkillForHosts;
|
|
589
605
|
const skillDecisionMode = normalizeSkillDecisionMode(options);
|
|
590
606
|
const guidedSkillDecisionOverride = getGuidedSkillDecisionOverrideForTests();
|
|
591
607
|
const nonInteractive = process.stdout.isTTY !== true;
|
|
@@ -682,12 +698,12 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
682
698
|
|
|
683
699
|
// Capture prior-mode state before any install/auth side effects so the
|
|
684
700
|
// detector does not classify artifacts created by this same command as
|
|
685
|
-
// pre-existing CLI-mode installs, and so a refuse
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
const priorModeSnapshot = await
|
|
701
|
+
// pre-existing CLI-mode installs, and so a refuse decision happens before we
|
|
702
|
+
// touch disk or start a browser auth flow. Running this inside
|
|
703
|
+
// runMcpSetupMode (the prior placement) meant --force would uninstall the
|
|
704
|
+
// freshly installed bundled skills, and the non-force path would still have
|
|
705
|
+
// run auth before surfacing the conflict.
|
|
706
|
+
const priorModeSnapshot = await detectPriorModeFn({
|
|
691
707
|
targetMode: "mcp",
|
|
692
708
|
selectedHosts: hosts,
|
|
693
709
|
scope,
|
|
@@ -695,11 +711,12 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
695
711
|
serverName,
|
|
696
712
|
serverUrl,
|
|
697
713
|
});
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
priorModeSnapshot.
|
|
702
|
-
|
|
714
|
+
// Conflict detection itself performs no write, so it stays ahead of
|
|
715
|
+
// authentication; the refusal and dependency checks below only throw.
|
|
716
|
+
const hasPriorModeConflict =
|
|
717
|
+
Boolean(priorModeSnapshot.priorMode) &&
|
|
718
|
+
priorModeSnapshot.affectedHosts.length > 0;
|
|
719
|
+
if (hasPriorModeConflict) {
|
|
703
720
|
if (deps.force !== true) {
|
|
704
721
|
throw new SetupModeUsageError(
|
|
705
722
|
formatPriorModeConflictError(priorModeSnapshot),
|
|
@@ -710,37 +727,6 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
710
727
|
"reconcilePriorMode dependency is required when --force is used.",
|
|
711
728
|
);
|
|
712
729
|
}
|
|
713
|
-
const reconcileEntries = Array.isArray(priorModeSnapshot.entries)
|
|
714
|
-
? priorModeSnapshot.entries
|
|
715
|
-
: priorModeSnapshot.affectedHosts.map((hostName) => ({
|
|
716
|
-
hostName,
|
|
717
|
-
serverNames: [],
|
|
718
|
-
}));
|
|
719
|
-
for (const entry of reconcileEntries) {
|
|
720
|
-
try {
|
|
721
|
-
// eslint-disable-next-line no-await-in-loop
|
|
722
|
-
await deps.reconcilePriorMode({
|
|
723
|
-
priorMode: priorModeSnapshot.priorMode,
|
|
724
|
-
targetMode: "mcp",
|
|
725
|
-
hostNames: [entry.hostName],
|
|
726
|
-
serverNames: entry.serverNames,
|
|
727
|
-
scope,
|
|
728
|
-
cwd: projectRoot,
|
|
729
|
-
serverName,
|
|
730
|
-
serverUrl,
|
|
731
|
-
});
|
|
732
|
-
} catch (error) {
|
|
733
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
734
|
-
const reconcileError = new Error(
|
|
735
|
-
`Failed to reconcile prior --mode ${priorModeSnapshot.priorMode} artifacts for host ${entry.hostName}: ${message}`,
|
|
736
|
-
);
|
|
737
|
-
reconcileError.exitCode = 4;
|
|
738
|
-
throw reconcileError;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
reconciled.push(
|
|
742
|
-
`removed prior --mode ${priorModeSnapshot.priorMode} artifacts for host(s): ${priorModeSnapshot.affectedHosts.join(", ")}`,
|
|
743
|
-
);
|
|
744
730
|
}
|
|
745
731
|
|
|
746
732
|
const installDriftHosts = driftHosts;
|
|
@@ -757,8 +743,61 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
757
743
|
);
|
|
758
744
|
}
|
|
759
745
|
|
|
746
|
+
// FLY-1112: authentication must finish before every configuration,
|
|
747
|
+
// reconciliation, and skill-install mutation, so a failed (or cancelled)
|
|
748
|
+
// authorization sequence never partially reconfigures the machine.
|
|
749
|
+
let apiKey = null;
|
|
750
|
+
const explicitApiKeyProvided = Object.hasOwn(options, "apiKey") || options.apiKeyStdin === true;
|
|
751
|
+
const shouldResolveApiKey =
|
|
752
|
+
!oauthMode && (needsSetupHosts.length > 0 || explicitApiKeyProvided);
|
|
753
|
+
if (shouldResolveApiKey) {
|
|
754
|
+
apiKey = authResult?.apiKey ?? await resolveApiKey(options, baseUrl, {
|
|
755
|
+
resolveSetupApiKeyFn: deps.resolveSetupApiKeyFn,
|
|
756
|
+
createAuthSpinner: deps.createAuthSpinner,
|
|
757
|
+
});
|
|
758
|
+
if (!apiKey) {
|
|
759
|
+
log.warn("Setup cancelled");
|
|
760
|
+
return { exitCode: 0 };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const reconciled = [];
|
|
760
765
|
let cleanupState = null;
|
|
761
766
|
try {
|
|
767
|
+
if (hasPriorModeConflict) {
|
|
768
|
+
const reconcileEntries = Array.isArray(priorModeSnapshot.entries)
|
|
769
|
+
? priorModeSnapshot.entries
|
|
770
|
+
: priorModeSnapshot.affectedHosts.map((hostName) => ({
|
|
771
|
+
hostName,
|
|
772
|
+
serverNames: [],
|
|
773
|
+
}));
|
|
774
|
+
for (const entry of reconcileEntries) {
|
|
775
|
+
try {
|
|
776
|
+
// eslint-disable-next-line no-await-in-loop
|
|
777
|
+
await deps.reconcilePriorMode({
|
|
778
|
+
priorMode: priorModeSnapshot.priorMode,
|
|
779
|
+
targetMode: "mcp",
|
|
780
|
+
hostNames: [entry.hostName],
|
|
781
|
+
serverNames: entry.serverNames,
|
|
782
|
+
scope,
|
|
783
|
+
cwd: projectRoot,
|
|
784
|
+
serverName,
|
|
785
|
+
serverUrl,
|
|
786
|
+
});
|
|
787
|
+
} catch (error) {
|
|
788
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
789
|
+
const reconcileError = new Error(
|
|
790
|
+
`Failed to reconcile prior --mode ${priorModeSnapshot.priorMode} artifacts for host ${entry.hostName}: ${message}`,
|
|
791
|
+
);
|
|
792
|
+
reconcileError.exitCode = 4;
|
|
793
|
+
throw reconcileError;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
reconciled.push(
|
|
797
|
+
`removed prior --mode ${priorModeSnapshot.priorMode} artifacts for host(s): ${priorModeSnapshot.affectedHosts.join(", ")}`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
762
801
|
if (skillDecisionMode === "install") {
|
|
763
802
|
logBundledSkillInstallPlan({
|
|
764
803
|
bundledSkillNames,
|
|
@@ -772,7 +811,7 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
772
811
|
scope,
|
|
773
812
|
skillNames: bundledSkillNames,
|
|
774
813
|
});
|
|
775
|
-
await
|
|
814
|
+
await installSkillForHostsFn({
|
|
776
815
|
projectRoot,
|
|
777
816
|
hostNames: supportedHosts,
|
|
778
817
|
scope,
|
|
@@ -787,28 +826,6 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
787
826
|
}
|
|
788
827
|
}
|
|
789
828
|
|
|
790
|
-
let apiKey = null;
|
|
791
|
-
const explicitApiKeyProvided = Object.hasOwn(options, "apiKey") || options.apiKeyStdin === true;
|
|
792
|
-
const shouldResolveApiKey =
|
|
793
|
-
!oauthMode && (needsSetupHosts.length > 0 || explicitApiKeyProvided);
|
|
794
|
-
if (shouldResolveApiKey) {
|
|
795
|
-
apiKey = authResult?.apiKey ?? await resolveApiKey(options, baseUrl);
|
|
796
|
-
if (!apiKey) {
|
|
797
|
-
if (cleanupState) {
|
|
798
|
-
await cleanupInstalledSkillArtifactsForHosts({
|
|
799
|
-
projectRoot,
|
|
800
|
-
hostNames: supportedHosts,
|
|
801
|
-
priorState: cleanupState,
|
|
802
|
-
scope,
|
|
803
|
-
skillNames: bundledSkillNames,
|
|
804
|
-
});
|
|
805
|
-
cleanupState = null;
|
|
806
|
-
}
|
|
807
|
-
log.warn("Setup cancelled");
|
|
808
|
-
return { exitCode: 0 };
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
829
|
const setupResult = await runMcpSetupMode({
|
|
813
830
|
selectedHosts: hosts,
|
|
814
831
|
scope,
|
|
@@ -927,7 +944,7 @@ export async function runMcpModeCommand(options, deps = {}) {
|
|
|
927
944
|
scope,
|
|
928
945
|
skillNames: bundledSkillNames,
|
|
929
946
|
});
|
|
930
|
-
await
|
|
947
|
+
await installSkillForHostsFn({
|
|
931
948
|
projectRoot,
|
|
932
949
|
hostNames: supportedHosts,
|
|
933
950
|
scope,
|
package/src/setup-auth.mjs
CHANGED
|
@@ -7,11 +7,25 @@ import { isatty } from "node:tty";
|
|
|
7
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
8
|
|
|
9
9
|
import { SetupInputUsageError } from "./setup/mode.mjs";
|
|
10
|
+
import {
|
|
11
|
+
isRetryableNetworkError,
|
|
12
|
+
isRetryableStatus,
|
|
13
|
+
parseRetryAfterMs,
|
|
14
|
+
withRetry,
|
|
15
|
+
} from "./runtime/vendor/flywheel-cli-dist/http/retry.js";
|
|
10
16
|
|
|
11
17
|
const DEFAULT_AUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
12
18
|
const MIN_POLL_DELAY_MS = 1000;
|
|
13
19
|
const MAX_POLL_DELAY_MS = 10_000;
|
|
14
20
|
const SETUP_DEVICE_API_PREFIX = "/api/v1/auth/mcp-api-keys/setup-device";
|
|
21
|
+
// FLY-1112: the setup-device start request is retried through the bundled
|
|
22
|
+
// helper. One logical request (body, idempotency key, request ID) is reused
|
|
23
|
+
// across at most three attempts; each attempt gets a fresh signal of at most
|
|
24
|
+
// 15s, and the whole recovery sequence is bounded by 49s so a temporary
|
|
25
|
+
// routing failure fails fast instead of pinning the spinner for two minutes.
|
|
26
|
+
const START_ATTEMPT_TIMEOUT_MS = 15_000;
|
|
27
|
+
const START_RECOVERY_BUDGET_MS = 49_000;
|
|
28
|
+
const START_MAX_ATTEMPTS = 3;
|
|
15
29
|
const readDescriptor = promisify(readFd);
|
|
16
30
|
|
|
17
31
|
async function readStdin(fd, buffer, offset, length, position) {
|
|
@@ -211,77 +225,246 @@ export function normalizeSetupAuthMode(value) {
|
|
|
211
225
|
// return { apiKey: key };
|
|
212
226
|
// }
|
|
213
227
|
|
|
228
|
+
export function formatStartRetryMessage({ nextAttempt, maxAttempts }) {
|
|
229
|
+
return `Flywheel is taking longer than expected; retrying (${nextAttempt} of ${maxAttempts})...`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// A retryable-class start failure: local attempt timeout, recognized
|
|
233
|
+
// temporary network error, or accepted HTTP status. Carries the structured
|
|
234
|
+
// summary used for progress callbacks and the exhausted-attempts error, plus
|
|
235
|
+
// the original error so non-retried failures retain their existing detail.
|
|
236
|
+
class StartAttemptFailure extends Error {
|
|
237
|
+
constructor(failure, cause, { retryAfter = null, returnAsIs = false } = {}) {
|
|
238
|
+
super(cause.message);
|
|
239
|
+
this.failure = failure;
|
|
240
|
+
this.cause = cause;
|
|
241
|
+
this.retryAfter = retryAfter;
|
|
242
|
+
this.returnAsIs = returnAsIs;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Internal sentinels: the sleep gate raises these when the next attempt can
|
|
247
|
+
// no longer fit inside a deadline; the outer catch maps them to the exact
|
|
248
|
+
// public error messages.
|
|
249
|
+
class StartRecoveryExhausted extends Error {
|
|
250
|
+
constructor(failure) {
|
|
251
|
+
super("start recovery budget exhausted");
|
|
252
|
+
this.failure = failure;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
class StartAuthorizationDeadline extends Error {}
|
|
256
|
+
|
|
257
|
+
function startFailureDescription(failure) {
|
|
258
|
+
return failure.kind === "http" ? `HTTP ${failure.status}` : failure.kind;
|
|
259
|
+
}
|
|
260
|
+
|
|
214
261
|
async function startSetupDeviceSession({
|
|
215
262
|
baseUrl,
|
|
216
263
|
keyName,
|
|
217
264
|
verificationBaseUrl,
|
|
218
|
-
|
|
265
|
+
authorizationDeadlineMs,
|
|
266
|
+
now = Date.now,
|
|
267
|
+
fetchFn = fetch,
|
|
268
|
+
sleep = defaultSleep,
|
|
269
|
+
random = Math.random,
|
|
270
|
+
createTimeoutSignal = AbortSignal.timeout,
|
|
271
|
+
onStartRetry,
|
|
219
272
|
}) {
|
|
220
273
|
const startUrl = new URL(`${SETUP_DEVICE_API_PREFIX}/start`, baseUrl);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
274
|
+
// One logical request: serialized body, idempotency key, and request ID are
|
|
275
|
+
// created once before attempt 1 and reused unchanged on every attempt so the
|
|
276
|
+
// server can replay the first unexpired session instead of duplicating it.
|
|
277
|
+
const body = JSON.stringify({
|
|
278
|
+
name: keyName,
|
|
279
|
+
verification_base_url: verificationBaseUrl,
|
|
280
|
+
});
|
|
281
|
+
const idempotencyKey = `setup-device-start:${crypto.randomUUID()}`;
|
|
282
|
+
const requestId = crypto.randomUUID();
|
|
283
|
+
const recoveryDeadlineMs = Math.min(
|
|
284
|
+
authorizationDeadlineMs,
|
|
285
|
+
now() + START_RECOVERY_BUDGET_MS,
|
|
286
|
+
);
|
|
287
|
+
// Tracks the most recently classified retryable failure so a budget
|
|
288
|
+
// guard reached at the top of a later attempt (see attemptStart below)
|
|
289
|
+
// can still attribute exhaustion to the retry sequence instead of the
|
|
290
|
+
// generic authorization timeout, matching gatedSleep's own attribution.
|
|
291
|
+
let lastFailure = null;
|
|
292
|
+
|
|
293
|
+
const attemptStart = async () => {
|
|
294
|
+
const signalBudgetMs = Math.min(
|
|
295
|
+
START_ATTEMPT_TIMEOUT_MS,
|
|
296
|
+
authorizationDeadlineMs - now(),
|
|
297
|
+
recoveryDeadlineMs - now(),
|
|
298
|
+
);
|
|
299
|
+
if (signalBudgetMs <= 0) {
|
|
300
|
+
// Ordinarily unreachable: the caller guards the authorization deadline
|
|
301
|
+
// before the first attempt, and gatedSleep already re-checks this same
|
|
302
|
+
// budget before scheduling any later attempt's sleep. Kept as a
|
|
303
|
+
// defensive fallback for real-world sleep/timer overrun (the actual
|
|
304
|
+
// wait exceeding the requested delay) between gatedSleep's check and
|
|
305
|
+
// this attempt actually starting.
|
|
306
|
+
if (recoveryDeadlineMs < authorizationDeadlineMs && lastFailure) {
|
|
307
|
+
throw new StartRecoveryExhausted(lastFailure);
|
|
308
|
+
}
|
|
309
|
+
throw new StartAuthorizationDeadline();
|
|
310
|
+
}
|
|
311
|
+
const signal = createTimeoutSignal(signalBudgetMs);
|
|
312
|
+
let response;
|
|
313
|
+
let payload;
|
|
314
|
+
try {
|
|
315
|
+
response = await fetchFn(startUrl, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: {
|
|
318
|
+
"content-type": "application/json",
|
|
319
|
+
"Idempotency-Key": idempotencyKey,
|
|
320
|
+
"X-Flywheel-Surface": "cli",
|
|
321
|
+
"X-Flywheel-Request-Id": requestId,
|
|
322
|
+
"X-Request-ID": requestId,
|
|
323
|
+
},
|
|
324
|
+
body,
|
|
325
|
+
signal,
|
|
326
|
+
});
|
|
327
|
+
payload = await parseJsonResponse(response, { signal });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// Only an abort of this attempt's own signal is a local timeout.
|
|
330
|
+
if (signal.aborted) {
|
|
331
|
+
throw new StartAttemptFailure({ kind: "timeout" }, error);
|
|
332
|
+
}
|
|
333
|
+
if (isRetryableNetworkError(error)) {
|
|
334
|
+
throw new StartAttemptFailure({ kind: "network" }, error);
|
|
335
|
+
}
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
const detail = resolveErrorMessage(payload, "Failed to start setup device flow.");
|
|
340
|
+
throw new StartAttemptFailure(
|
|
341
|
+
{ kind: "http", status: response.status },
|
|
342
|
+
new Error(detail),
|
|
343
|
+
{ retryAfter: response.headers?.get?.("retry-after") ?? null },
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const deviceCode = nonEmptyString(payload.device_code);
|
|
348
|
+
const userCode = nonEmptyString(payload.user_code);
|
|
349
|
+
const verificationUri = nonEmptyString(payload.verification_uri);
|
|
350
|
+
const verificationUriComplete = nonEmptyString(payload.verification_uri_complete);
|
|
351
|
+
const pollIntervalSeconds = Number(payload.poll_interval_seconds);
|
|
352
|
+
const expiresInSeconds = Number(payload.expires_in_seconds);
|
|
353
|
+
|
|
354
|
+
if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
|
|
355
|
+
throw new Error("Setup device session response missing required fields.");
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
!Number.isFinite(pollIntervalSeconds) ||
|
|
359
|
+
pollIntervalSeconds <= 0 ||
|
|
360
|
+
!Number.isFinite(expiresInSeconds) ||
|
|
361
|
+
expiresInSeconds <= 0
|
|
362
|
+
) {
|
|
363
|
+
throw new Error("Setup device session response contained invalid timing values.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
deviceCode,
|
|
368
|
+
userCode,
|
|
369
|
+
verificationUri,
|
|
370
|
+
verificationUriComplete,
|
|
371
|
+
pollIntervalSeconds: Math.floor(pollIntervalSeconds),
|
|
372
|
+
expiresInSeconds: Math.floor(expiresInSeconds),
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
let pendingRetry = null;
|
|
377
|
+
// A retry is scheduled only when the delay plus a full 15s next attempt still
|
|
378
|
+
// fits inside the recovery budget. The gate runs before the progress
|
|
379
|
+
// callback so "retrying" is announced only when another attempt will happen.
|
|
380
|
+
const gatedSleep = async (delayMs) => {
|
|
381
|
+
const { nextAttempt, failure } = pendingRetry;
|
|
382
|
+
pendingRetry = null;
|
|
383
|
+
if (now() + delayMs + START_ATTEMPT_TIMEOUT_MS > recoveryDeadlineMs) {
|
|
384
|
+
if (recoveryDeadlineMs === authorizationDeadlineMs) {
|
|
385
|
+
throw new StartAuthorizationDeadline();
|
|
386
|
+
}
|
|
387
|
+
throw new StartRecoveryExhausted(failure);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
onStartRetry?.({
|
|
391
|
+
nextAttempt,
|
|
392
|
+
maxAttempts: START_MAX_ATTEMPTS,
|
|
393
|
+
delayMs,
|
|
394
|
+
requestId,
|
|
395
|
+
failure,
|
|
396
|
+
});
|
|
397
|
+
} catch {
|
|
398
|
+
// Presentation is best-effort and must not change retry behavior.
|
|
399
|
+
}
|
|
400
|
+
await sleep(delayMs);
|
|
401
|
+
};
|
|
402
|
+
|
|
224
403
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
404
|
+
return await withRetry(attemptStart, {
|
|
405
|
+
maxAttempts: START_MAX_ATTEMPTS,
|
|
406
|
+
sleep: gatedSleep,
|
|
407
|
+
random,
|
|
408
|
+
shouldRetry: (error, attempt) => {
|
|
409
|
+
if (!(error instanceof StartAttemptFailure) || error.returnAsIs) {
|
|
410
|
+
return { retry: false };
|
|
411
|
+
}
|
|
412
|
+
const { failure } = error;
|
|
413
|
+
if (failure.kind === "http") {
|
|
414
|
+
if (!isRetryableStatus(failure.status)) {
|
|
415
|
+
return { retry: false };
|
|
416
|
+
}
|
|
417
|
+
const retryAfterMs = parseRetryAfterMs(error.retryAfter);
|
|
418
|
+
if (retryAfterMs !== null) {
|
|
419
|
+
// A valid Retry-After replaces the helper's delay only when it
|
|
420
|
+
// leaves room for the remaining 15s attempts; otherwise the
|
|
421
|
+
// current HTTP failure is returned unchanged.
|
|
422
|
+
if (now() + retryAfterMs + START_ATTEMPT_TIMEOUT_MS <= recoveryDeadlineMs) {
|
|
423
|
+
pendingRetry = { nextAttempt: attempt + 1, failure };
|
|
424
|
+
lastFailure = failure;
|
|
425
|
+
return { retry: true, delayMs: retryAfterMs };
|
|
426
|
+
}
|
|
427
|
+
error.returnAsIs = true;
|
|
428
|
+
return { retry: false };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
pendingRetry = { nextAttempt: attempt + 1, failure };
|
|
432
|
+
lastFailure = failure;
|
|
433
|
+
return { retry: true };
|
|
230
434
|
},
|
|
231
|
-
body: JSON.stringify({
|
|
232
|
-
name: keyName,
|
|
233
|
-
verification_base_url: verificationBaseUrl,
|
|
234
|
-
}),
|
|
235
|
-
signal: startTimeoutSignal,
|
|
236
435
|
});
|
|
237
|
-
payload = await parseJsonResponse(response, { signal: startTimeoutSignal });
|
|
238
436
|
} catch (error) {
|
|
239
|
-
if (
|
|
437
|
+
if (error instanceof StartAuthorizationDeadline) {
|
|
240
438
|
throw new Error("Timed out waiting for device authorization.");
|
|
241
439
|
}
|
|
440
|
+
if (error instanceof StartRecoveryExhausted) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Failed to start device authorization after ${START_MAX_ATTEMPTS} attempts (last failure: ${startFailureDescription(error.failure)}; request ID: ${requestId}). Run the same setup command again.`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (error instanceof StartAttemptFailure) {
|
|
446
|
+
const retryableClass =
|
|
447
|
+
error.failure.kind !== "http" || isRetryableStatus(error.failure.status);
|
|
448
|
+
if (retryableClass && !error.returnAsIs) {
|
|
449
|
+
// A retryable failure escaping withRetry means the final attempt
|
|
450
|
+
// failed; report the exhausted sequence with request correlation.
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Failed to start device authorization after ${START_MAX_ATTEMPTS} attempts (last failure: ${startFailureDescription(error.failure)}; request ID: ${requestId}). Run the same setup command again.`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
throw error.cause;
|
|
456
|
+
}
|
|
242
457
|
throw error;
|
|
243
458
|
}
|
|
244
|
-
if (!response.ok) {
|
|
245
|
-
const detail = resolveErrorMessage(payload, "Failed to start setup device flow.");
|
|
246
|
-
throw new Error(detail);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const deviceCode = nonEmptyString(payload.device_code);
|
|
250
|
-
const userCode = nonEmptyString(payload.user_code);
|
|
251
|
-
const verificationUri = nonEmptyString(payload.verification_uri);
|
|
252
|
-
const verificationUriComplete = nonEmptyString(payload.verification_uri_complete);
|
|
253
|
-
const pollIntervalSeconds = Number(payload.poll_interval_seconds);
|
|
254
|
-
const expiresInSeconds = Number(payload.expires_in_seconds);
|
|
255
|
-
|
|
256
|
-
if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
|
|
257
|
-
throw new Error("Setup device session response missing required fields.");
|
|
258
|
-
}
|
|
259
|
-
if (
|
|
260
|
-
!Number.isFinite(pollIntervalSeconds) ||
|
|
261
|
-
pollIntervalSeconds <= 0 ||
|
|
262
|
-
!Number.isFinite(expiresInSeconds) ||
|
|
263
|
-
expiresInSeconds <= 0
|
|
264
|
-
) {
|
|
265
|
-
throw new Error("Setup device session response contained invalid timing values.");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
deviceCode,
|
|
270
|
-
userCode,
|
|
271
|
-
verificationUri,
|
|
272
|
-
verificationUriComplete,
|
|
273
|
-
pollIntervalSeconds: Math.floor(pollIntervalSeconds),
|
|
274
|
-
expiresInSeconds: Math.floor(expiresInSeconds),
|
|
275
|
-
};
|
|
276
459
|
}
|
|
277
460
|
|
|
278
|
-
async function pollSetupDeviceSession({ baseUrl, userCode, deviceCode }) {
|
|
461
|
+
async function pollSetupDeviceSession({ baseUrl, userCode, deviceCode, fetchFn = fetch, now = Date.now }) {
|
|
279
462
|
const pollUrl = new URL(`${SETUP_DEVICE_API_PREFIX}/poll`, baseUrl);
|
|
280
|
-
const response = await
|
|
463
|
+
const response = await fetchFn(pollUrl, {
|
|
281
464
|
method: "POST",
|
|
282
465
|
headers: {
|
|
283
466
|
"content-type": "application/json",
|
|
284
|
-
"Idempotency-Key": `setup-device-poll:${deviceCode}:${
|
|
467
|
+
"Idempotency-Key": `setup-device-poll:${deviceCode}:${now()}`,
|
|
285
468
|
},
|
|
286
469
|
body: JSON.stringify({
|
|
287
470
|
user_code: userCode,
|
|
@@ -377,6 +560,12 @@ export async function resolveSetupApiKey({
|
|
|
377
560
|
acquireDeviceFlow = acquireApiKeyViaDeviceFlow,
|
|
378
561
|
acquireBrowserBridge = acquireApiKeyViaBrowserBridge,
|
|
379
562
|
keyNameFactory = () => `flywheel-setup-${crypto.randomBytes(3).toString("hex")}`,
|
|
563
|
+
fetchFn,
|
|
564
|
+
now,
|
|
565
|
+
sleep,
|
|
566
|
+
random,
|
|
567
|
+
createTimeoutSignal,
|
|
568
|
+
onStartRetry,
|
|
380
569
|
} = {}) {
|
|
381
570
|
// Check the value, not just property presence: callers like runCliSetupMode
|
|
382
571
|
// always forward `{apiKey, authMode}` to share one helper shape, so the
|
|
@@ -410,6 +599,12 @@ export async function resolveSetupApiKey({
|
|
|
410
599
|
baseUrl,
|
|
411
600
|
keyName,
|
|
412
601
|
verificationBaseUrl: baseUrl,
|
|
602
|
+
fetchFn,
|
|
603
|
+
now,
|
|
604
|
+
sleep,
|
|
605
|
+
random,
|
|
606
|
+
createTimeoutSignal,
|
|
607
|
+
onStartRetry,
|
|
413
608
|
})
|
|
414
609
|
: await acquireBrowserBridge({
|
|
415
610
|
baseUrl,
|
|
@@ -594,17 +789,28 @@ export async function acquireApiKeyViaDeviceFlow({
|
|
|
594
789
|
timeoutMs = DEFAULT_AUTH_TIMEOUT_MS,
|
|
595
790
|
openUrl = openUrlInBrowser,
|
|
596
791
|
sleep = defaultSleep,
|
|
792
|
+
fetchFn = fetch,
|
|
793
|
+
now = Date.now,
|
|
794
|
+
random = Math.random,
|
|
795
|
+
createTimeoutSignal = AbortSignal.timeout,
|
|
796
|
+
onStartRetry,
|
|
597
797
|
}) {
|
|
598
|
-
const startedAt =
|
|
599
|
-
const
|
|
600
|
-
if (
|
|
798
|
+
const startedAt = now();
|
|
799
|
+
const authorizationDeadlineMs = startedAt + timeoutMs;
|
|
800
|
+
if (authorizationDeadlineMs - now() <= 0) {
|
|
601
801
|
throw new Error("Timed out waiting for device authorization.");
|
|
602
802
|
}
|
|
603
803
|
const session = await startSetupDeviceSession({
|
|
604
804
|
baseUrl,
|
|
605
805
|
keyName,
|
|
606
806
|
verificationBaseUrl,
|
|
607
|
-
|
|
807
|
+
authorizationDeadlineMs,
|
|
808
|
+
now,
|
|
809
|
+
fetchFn,
|
|
810
|
+
sleep,
|
|
811
|
+
random,
|
|
812
|
+
createTimeoutSignal,
|
|
813
|
+
onStartRetry,
|
|
608
814
|
});
|
|
609
815
|
|
|
610
816
|
console.log("Opening browser for Flywheel device authorization...");
|
|
@@ -617,12 +823,14 @@ export async function acquireApiKeyViaDeviceFlow({
|
|
|
617
823
|
launchBrowser(openUrl, session.verificationUriComplete);
|
|
618
824
|
|
|
619
825
|
let attempt = 0;
|
|
620
|
-
while (
|
|
826
|
+
while (now() - startedAt < timeoutMs) {
|
|
621
827
|
// eslint-disable-next-line no-await-in-loop
|
|
622
828
|
const pollResult = await pollSetupDeviceSession({
|
|
623
829
|
baseUrl,
|
|
624
830
|
userCode: session.userCode,
|
|
625
831
|
deviceCode: session.deviceCode,
|
|
832
|
+
fetchFn,
|
|
833
|
+
now,
|
|
626
834
|
});
|
|
627
835
|
if (pollResult.status === "approved") {
|
|
628
836
|
return { apiKey: pollResult.apiKey };
|