@bridge_gpt/mcp-server 0.2.36 → 0.2.37
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 +44 -6
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-user-config-doctor.js +317 -0
- package/build/commands.generated.js +2 -1
- package/build/conductor/bridge-api-client.js +178 -4
- package/build/conductor-bin.js +1 -1
- package/build/conductor-bundle-artifacts.js +7 -6
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +239 -80
- package/build/executor/cli.js +51 -1
- package/build/executor/credentials.js +1 -7
- package/build/executor/deps.js +18 -1
- package/build/executor/env.js +51 -25
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +422 -22
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +104 -8
- package/build/executor/preflight.js +32 -0
- package/build/executor/runner.js +8 -0
- package/build/executor/test-clock.js +67 -3
- package/build/executor/types.js +4 -1
- package/build/executor/worker-command.js +11 -3
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +614 -244
- package/build/init.js +363 -73
- package/build/install-bridge.js +568 -80
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +30 -7
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +22 -9
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -0
- package/build/setup-epic.js +662 -27
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +417 -101
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +23 -8
- package/package.json +3 -3
- package/pipelines/implement-ticket.json +15 -5
package/build/install-bridge.js
CHANGED
|
@@ -156,7 +156,7 @@ import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-r
|
|
|
156
156
|
import { runSetupEpicCli } from "./setup-epic.js";
|
|
157
157
|
import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
|
|
158
158
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
159
|
-
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
159
|
+
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
160
160
|
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
161
161
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
162
162
|
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
@@ -384,29 +384,34 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
384
384
|
"Self-serve onboarding (no account, no API key, no pre-issued invite):",
|
|
385
385
|
" --email <addr> Create a brand-new Bridge workspace from just an email —",
|
|
386
386
|
" the primary path for a first-time user with nothing yet.",
|
|
387
|
-
" It requests a fresh workspace for that email, then creates",
|
|
388
|
-
" the project and mints your own admin API key in one command.",
|
|
389
387
|
" Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
|
|
390
388
|
" interactive prompt — which is also what answering 2 on a",
|
|
391
|
-
" bare run reaches. The email is NOT a",
|
|
392
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
389
|
+
" bare run reaches. The email is NOT a secret (it is shown as",
|
|
390
|
+
" you type), but it is never printed to a log. Mutually",
|
|
391
|
+
" exclusive with --api-key and --invite.",
|
|
392
|
+
"",
|
|
393
|
+
" VERIFIED: Bridge emails a 6-digit code to that address and",
|
|
394
|
+
" this command prompts you for it. Nothing is created until",
|
|
395
|
+
" the code is entered — no workspace, no project, no API key.",
|
|
396
|
+
" Delivery is asynchronous, so the message arrives shortly",
|
|
397
|
+
" after you are prompted rather than instantly. The code",
|
|
398
|
+
" expires 60 minutes after it is requested, and you can ask",
|
|
399
|
+
" for a new one if it does not arrive; earlier codes keep",
|
|
400
|
+
" working until one is accepted.",
|
|
401
|
+
"",
|
|
402
|
+
" REQUIRES A TERMINAL: there is no flag or environment",
|
|
403
|
+
" variable for the code — either would put a live credential",
|
|
404
|
+
" into your shell history and process list. For unattended",
|
|
405
|
+
" setup use --api-key <key> or --invite <token> instead.",
|
|
402
406
|
"",
|
|
403
|
-
" RESUMABLE: a self-serve run that
|
|
407
|
+
" RESUMABLE: a self-serve run that is interrupted saves its",
|
|
404
408
|
" signup state under bootstrap-pending:<repo> in the credential",
|
|
405
|
-
" store. Re-running
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"
|
|
409
|
+
" store. Re-running RESUMES that attempt — at the code prompt",
|
|
410
|
+
" if the code has not been entered yet, or at the exchange if",
|
|
411
|
+
" it has — so a retry never sends a second code and never",
|
|
412
|
+
" creates a second workspace. The code itself is never saved.",
|
|
413
|
+
" Never copy, display, or hand-remove that record; if the saved",
|
|
414
|
+
" invite has genuinely expired the CLI asks before discarding it.",
|
|
410
415
|
"",
|
|
411
416
|
"Bootstrap-invite onboarding (no web UI, no pre-existing key):",
|
|
412
417
|
" --invite [token] Redeem a bootstrap invite you were already given: creates",
|
|
@@ -956,6 +961,8 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
956
961
|
repointBootstrapPending: repointBootstrapPendingCredential,
|
|
957
962
|
promoteBootstrapPending: promoteBootstrapPendingCredential,
|
|
958
963
|
lookupSelfServeBootstrapPending: lookupSelfServeBootstrapPendingCredential,
|
|
964
|
+
prepareSelfServeBootstrapChallenge: prepareSelfServeBootstrapChallengeCredential,
|
|
965
|
+
now: () => Date.now(),
|
|
959
966
|
discardBootstrapPending: discardBootstrapPendingCredential,
|
|
960
967
|
buildShellCommand: buildGenericAgentShellCommand,
|
|
961
968
|
spawnTerminalTab: getDefaultSpawnTerminalTabForPlatform(process.platform),
|
|
@@ -1123,6 +1130,16 @@ export async function resolveInviteToken(options, deps) {
|
|
|
1123
1130
|
* back in an error message.
|
|
1124
1131
|
*/
|
|
1125
1132
|
export async function resolveSignupEmail(options, deps) {
|
|
1133
|
+
// BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
|
|
1134
|
+
// and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
|
|
1135
|
+
// so an unattended run fails before any request is made, no verification email
|
|
1136
|
+
// is sent, and no challenge row is created. Supplying the address was never the
|
|
1137
|
+
// hard part; entering a code that only arrives by email is, and there is
|
|
1138
|
+
// deliberately no `--code` flag and no environment variable to supply it (a live
|
|
1139
|
+
// code must never reach shell history or a process list).
|
|
1140
|
+
if (!deps.isTTY || !deps.promptLine) {
|
|
1141
|
+
return { ok: false, error: INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY };
|
|
1142
|
+
}
|
|
1126
1143
|
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
1127
1144
|
return { ok: true, value: options.email.trim() };
|
|
1128
1145
|
}
|
|
@@ -1130,19 +1147,11 @@ export async function resolveSignupEmail(options, deps) {
|
|
|
1130
1147
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
1131
1148
|
return { ok: true, value: fromEnv.trim() };
|
|
1132
1149
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
return { ok: true, value: entered };
|
|
1137
|
-
}
|
|
1138
|
-
return { ok: false, error: "No email entered." };
|
|
1150
|
+
const entered = (await deps.promptLine("Email for Bridge workspace setup: ")).trim();
|
|
1151
|
+
if (entered.length > 0) {
|
|
1152
|
+
return { ok: true, value: entered };
|
|
1139
1153
|
}
|
|
1140
|
-
return {
|
|
1141
|
-
ok: false,
|
|
1142
|
-
error: "An email is required to create a Bridge workspace. Pass --email <addr> or set the " +
|
|
1143
|
-
"BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt " +
|
|
1144
|
-
"for it).",
|
|
1145
|
-
};
|
|
1154
|
+
return { ok: false, error: "No email entered." };
|
|
1146
1155
|
}
|
|
1147
1156
|
/**
|
|
1148
1157
|
* Select the onboarding branch as pure logic (deterministic, no I/O, no prompt):
|
|
@@ -2556,23 +2565,234 @@ export async function requestCredentialRouteSwitchConfirmation(deps, promptText)
|
|
|
2556
2565
|
export function buildSelfServeMintUrl(baseUrl) {
|
|
2557
2566
|
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve`;
|
|
2558
2567
|
}
|
|
2568
|
+
// ===================================================================
|
|
2569
|
+
// BAPI-757 — the verification checkpoint's user-facing copy.
|
|
2570
|
+
//
|
|
2571
|
+
// Every string below is a module constant rather than an inline literal, for two
|
|
2572
|
+
// reasons: the static-contract suite pins several of them, and a uniform outcome
|
|
2573
|
+
// stated in one place cannot drift into four subtly different sentences that
|
|
2574
|
+
// together disclose which failure occurred.
|
|
2575
|
+
// ===================================================================
|
|
2576
|
+
/** Digits in a verification code. Mirrors the server's protocol constant. */
|
|
2577
|
+
export const SIGNUP_VERIFICATION_CODE_LENGTH = 6;
|
|
2578
|
+
/** The one prompt label. The E2E harness and the static contract both pin it. */
|
|
2579
|
+
export const INSTALL_BRIDGE_CODE_PROMPT = "Email verification code: ";
|
|
2580
|
+
/** Shown when a stored, unexpired challenge is picked up on a later run. */
|
|
2581
|
+
export const INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING = "Resume email verification";
|
|
2582
|
+
/**
|
|
2583
|
+
* The non-interactive refusal (AC-9 / R-3).
|
|
2584
|
+
*
|
|
2585
|
+
* Names BOTH unattended alternatives, because the whole point of the message is
|
|
2586
|
+
* that the user is running in a context where no prompt can be answered — telling
|
|
2587
|
+
* them only what does not work would leave them stuck.
|
|
2588
|
+
*/
|
|
2589
|
+
export const INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY = [
|
|
2590
|
+
"Creating a Bridge workspace requires verifying your email address, and that needs an",
|
|
2591
|
+
"interactive terminal to enter the emailed code. There is deliberately no flag or",
|
|
2592
|
+
"environment variable for the code — either would put a live credential into your shell",
|
|
2593
|
+
"history and process list.",
|
|
2594
|
+
"",
|
|
2595
|
+
"Two onboarding paths still work unattended:",
|
|
2596
|
+
" • An existing API key: install --api-key <key> (or set BAPI_API_KEY)",
|
|
2597
|
+
" • A pre-issued invite: install --invite <token> (or set BAPI_INVITE)",
|
|
2598
|
+
"",
|
|
2599
|
+
"To sign up from scratch, re-run this command in an interactive terminal.",
|
|
2600
|
+
].join("\n");
|
|
2601
|
+
/**
|
|
2602
|
+
* The UNIFORM recovery message for a rejected verification.
|
|
2603
|
+
*
|
|
2604
|
+
* The server answers a wrong code, an expired code, an unknown handle, and an
|
|
2605
|
+
* exhausted challenge with one identical response — and this message is the client
|
|
2606
|
+
* half of that property. It must never be specialized per case, and it must never
|
|
2607
|
+
* speculate ("maybe it expired?"): a message that narrowed the possibilities would
|
|
2608
|
+
* hand back exactly the signal the uniform response withholds.
|
|
2609
|
+
*/
|
|
2610
|
+
export const INSTALL_BRIDGE_VERIFICATION_REJECTED = [
|
|
2611
|
+
"That code was not accepted.",
|
|
2612
|
+
"",
|
|
2613
|
+
"Check the most recent verification email and try again, or request a new code. If the",
|
|
2614
|
+
"signup attempt is too old, re-run this command to start a fresh one.",
|
|
2615
|
+
].join("\n");
|
|
2616
|
+
/** Resend success. States plainly that earlier codes still work — see R-5. */
|
|
2617
|
+
export const INSTALL_BRIDGE_RESEND_SENT = "A new code is on its way. Any code from an earlier email for this signup still works too.";
|
|
2618
|
+
/** Cooldown. Non-accusatory, and silent about which limiter applied. */
|
|
2619
|
+
export const INSTALL_BRIDGE_RESEND_COOLDOWN = "A verification email was sent moments ago — give it a minute to arrive before requesting another.";
|
|
2620
|
+
/** Send ceiling. Names no bucket: not the address, not the source, not the global cap. */
|
|
2621
|
+
export const INSTALL_BRIDGE_RESEND_THROTTLED = "Bridge is not sending another verification email for this signup right now. Try again later, or use the code from an email you already received.";
|
|
2622
|
+
/**
|
|
2623
|
+
* A throttle that arrives from the VERIFY call, i.e. after a code was submitted.
|
|
2624
|
+
*
|
|
2625
|
+
* Deliberately NOT {@link INSTALL_BRIDGE_RESEND_THROTTLED}. That message tells the
|
|
2626
|
+
* user to fall back on a code they already have — which is backwards here: the
|
|
2627
|
+
* server throttles verification at the invite-mint ceiling, so their code may have
|
|
2628
|
+
* been perfectly correct and there is nothing to fall back to. Re-running is the
|
|
2629
|
+
* remedy, and the code stays valid until it expires.
|
|
2630
|
+
*/
|
|
2631
|
+
export const INSTALL_BRIDGE_VERIFY_THROTTLED = "Bridge is at capacity and could not finish creating your workspace just now. Nothing was created — wait a few minutes and re-run this command; the code already emailed to you stays valid until it expires.";
|
|
2632
|
+
/**
|
|
2633
|
+
* A transient server failure during verification.
|
|
2634
|
+
*
|
|
2635
|
+
* Separate from {@link INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED} because a 5xx means
|
|
2636
|
+
* the server understood this CLI perfectly and simply failed. Telling the user to
|
|
2637
|
+
* upgrade would send them to the one recovery step that cannot help, and would
|
|
2638
|
+
* hide a real outage behind a version-skew story.
|
|
2639
|
+
*/
|
|
2640
|
+
export const INSTALL_BRIDGE_VERIFY_SERVER_ERROR = "Error: Bridge could not complete verification right now. Nothing was created — wait a few minutes and re-run this command; the code already emailed to you stays valid until it expires.";
|
|
2641
|
+
/**
|
|
2642
|
+
* Local cap on rejected verification attempts before the CLI stops re-prompting.
|
|
2643
|
+
*
|
|
2644
|
+
* AC-4 makes "wrong code" and "this challenge is dead" byte-identical on purpose,
|
|
2645
|
+
* so the CLI genuinely cannot tell them apart. Without a cap, a user who exhausts
|
|
2646
|
+
* the server's attempt budget is re-prompted forever against a challenge that can
|
|
2647
|
+
* never succeed, with Ctrl-C as the only exit.
|
|
2648
|
+
*
|
|
2649
|
+
* Set ABOVE the server's default attempt budget (5) so a legitimately mistyping
|
|
2650
|
+
* user is never cut off before the server itself would refuse — the cap exists to
|
|
2651
|
+
* end a hopeless loop, not to add a second, tighter budget the user cannot see.
|
|
2652
|
+
*/
|
|
2653
|
+
export const INSTALL_BRIDGE_MAX_CODE_REJECTIONS = 6;
|
|
2654
|
+
/** Printed when the rejection cap is reached. Still state-neutral (AC-4). */
|
|
2655
|
+
export const INSTALL_BRIDGE_VERIFICATION_GIVING_UP = [
|
|
2656
|
+
"Error: that signup could not be verified.",
|
|
2657
|
+
"",
|
|
2658
|
+
"Nothing has been created. Re-run this command to start a fresh signup — it will email",
|
|
2659
|
+
"a new code to the same address.",
|
|
2660
|
+
].join("\n");
|
|
2661
|
+
/** The fail-closed enqueue failure (R-4). No suggestion that verification is skippable. */
|
|
2662
|
+
export const INSTALL_BRIDGE_SIGNUP_UNAVAILABLE = "Error: Bridge could not send a verification email, so no workspace was created. Nothing has been set up — try again in a few minutes.";
|
|
2559
2663
|
/**
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
2664
|
+
* R-2's hard break, rendered for a server that no longer speaks this client's
|
|
2665
|
+
* protocol — or a client that no longer speaks the server's.
|
|
2562
2666
|
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
2565
|
-
*
|
|
2566
|
-
* the status alone decides the category — because an untrusted upstream body could
|
|
2567
|
-
* echo the email or carry internals. A caught fetch exception is likewise never
|
|
2568
|
-
* inspected or interpolated; some fetch impls embed the request (and thus the
|
|
2569
|
-
* email) in the message.
|
|
2667
|
+
* Deliberately NOT the old generic "Check connectivity and retry": connectivity is
|
|
2668
|
+
* fine, and sending a user to debug their network for a protocol change is the
|
|
2669
|
+
* failure mode this constant exists to prevent.
|
|
2570
2670
|
*/
|
|
2571
|
-
export
|
|
2572
|
-
|
|
2671
|
+
export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
|
|
2672
|
+
"Error: this Bridge server's signup flow does not match this CLI.",
|
|
2673
|
+
"",
|
|
2674
|
+
"Signup now verifies your email address with a code before creating a workspace. Upgrade",
|
|
2675
|
+
"and re-run:",
|
|
2676
|
+
"",
|
|
2677
|
+
" npx -y @bridge_gpt/mcp-server@latest install --email <addr>",
|
|
2678
|
+
].join("\n");
|
|
2679
|
+
/**
|
|
2680
|
+
* Mask an address for display: first character, then the domain.
|
|
2681
|
+
*
|
|
2682
|
+
* `ada@example.com` renders as `a***@example.com`. Enough for the user to confirm
|
|
2683
|
+
* they typed the right mailbox, without putting a full address into a terminal
|
|
2684
|
+
* transcript that may be pasted into an issue or captured by CI. Anything that
|
|
2685
|
+
* does not look like a mailbox is reduced to a fixed placeholder rather than
|
|
2686
|
+
* echoed — an unparseable value is exactly the one most likely to be a typo worth
|
|
2687
|
+
* not repeating.
|
|
2688
|
+
*/
|
|
2689
|
+
export function maskSignupEmail(email) {
|
|
2690
|
+
const trimmed = (email ?? "").trim();
|
|
2691
|
+
const at = trimmed.lastIndexOf("@");
|
|
2692
|
+
if (at <= 0 || at === trimmed.length - 1)
|
|
2693
|
+
return "your email address";
|
|
2694
|
+
return `${trimmed[0]}***${trimmed.slice(at)}`;
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Render a challenge's remaining lifetime in calm, approximate language.
|
|
2698
|
+
*
|
|
2699
|
+
* Whole minutes, rounded down, and never a countdown: the exact number does not
|
|
2700
|
+
* change what the user does, and a precise timer turns a routine wait into a
|
|
2701
|
+
* deadline. A challenge already past its expiry says so plainly rather than
|
|
2702
|
+
* rendering a negative number.
|
|
2703
|
+
*/
|
|
2704
|
+
export function formatChallengeRemaining(expiresAtIso, nowMs) {
|
|
2705
|
+
const expiresMs = Date.parse(expiresAtIso);
|
|
2706
|
+
if (Number.isNaN(expiresMs))
|
|
2707
|
+
return "";
|
|
2708
|
+
const minutes = Math.floor((expiresMs - nowMs) / 60_000);
|
|
2709
|
+
if (minutes <= 0)
|
|
2710
|
+
return "This code has expired.";
|
|
2711
|
+
if (minutes === 1)
|
|
2712
|
+
return "This code is valid for about another minute.";
|
|
2713
|
+
return `This code is valid for about another ${minutes} minutes.`;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Prompt for the verification code, validating its shape LOCALLY before any
|
|
2717
|
+
* network call.
|
|
2718
|
+
*
|
|
2719
|
+
* Local validation is a courtesy, not a security control — the server re-validates
|
|
2720
|
+
* everything — but it keeps a mistyped code from consuming one of the five attempts
|
|
2721
|
+
* that permanently kill a challenge. The user gets the prompt back instead.
|
|
2722
|
+
*
|
|
2723
|
+
* Pasted input is trimmed: copying a code out of a mail client routinely brings
|
|
2724
|
+
* whitespace with it, and refusing that would be an avoidable dead end.
|
|
2725
|
+
*
|
|
2726
|
+
* The entered value is returned and never echoed, never logged, and never written
|
|
2727
|
+
* to disk. `null` means the user gave up (an empty line, or an unreadable prompt).
|
|
2728
|
+
*/
|
|
2729
|
+
export async function promptForVerificationCode(deps, log, maxAttempts = 3) {
|
|
2730
|
+
if (!deps.promptLine)
|
|
2731
|
+
return null;
|
|
2732
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2733
|
+
let entered;
|
|
2734
|
+
try {
|
|
2735
|
+
entered = (await deps.promptLine(INSTALL_BRIDGE_CODE_PROMPT)).trim();
|
|
2736
|
+
}
|
|
2737
|
+
catch {
|
|
2738
|
+
// Secret-free by construction: the caught value is never surfaced or
|
|
2739
|
+
// inspected, because a readline failure can carry the partial input.
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
if (entered.length === 0)
|
|
2743
|
+
return null;
|
|
2744
|
+
if (new RegExp(`^[0-9]{${SIGNUP_VERIFICATION_CODE_LENGTH}}$`).test(entered)) {
|
|
2745
|
+
return entered;
|
|
2746
|
+
}
|
|
2747
|
+
// Describes the SHAPE, never the value the user typed.
|
|
2748
|
+
log(` That does not look like a code — it is ${SIGNUP_VERIFICATION_CODE_LENGTH} digits.`);
|
|
2749
|
+
}
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
/*
|
|
2753
|
+
* `mintSelfServeInvite` and `SelfServeMintResult` were REMOVED in BAPI-757.
|
|
2754
|
+
*
|
|
2755
|
+
* They read `token` from the request phase's success body. Under R-2 that field
|
|
2756
|
+
* no longer exists for any client version, and a helper that still looked for it
|
|
2757
|
+
* would be a live path back to an unverified signup — so it is deleted rather than
|
|
2758
|
+
* deprecated. The replacement is the three-phase client immediately below.
|
|
2759
|
+
*/
|
|
2760
|
+
// ===================================================================
|
|
2761
|
+
// BAPI-757 — the three-phase self-serve signup client.
|
|
2762
|
+
//
|
|
2763
|
+
// The server no longer returns a token from the request phase, for ANY client
|
|
2764
|
+
// version (R-2). These three helpers replace the single-shot mint above:
|
|
2765
|
+
//
|
|
2766
|
+
// requestSelfServeChallenge → POST /setup/bootstrap/self-serve
|
|
2767
|
+
// verifySelfServeChallenge → POST /setup/bootstrap/self-serve/verify
|
|
2768
|
+
// resendSelfServeChallenge → POST /setup/bootstrap/self-serve/resend
|
|
2769
|
+
//
|
|
2770
|
+
// All three keep the discipline the deleted single-shot mint established, and that
|
|
2771
|
+
// `exchangeBootstrapInvite` still follows: the same 10s timeout, a COARSE result
|
|
2772
|
+
// category on failure, and a failure body that is never read — an untrusted
|
|
2773
|
+
// upstream body could echo the email or the code, and some fetch implementations
|
|
2774
|
+
// embed the request (and therefore the code) in a thrown error's message, so caught
|
|
2775
|
+
// exceptions are never inspected either. The one exception is the resend 429, which
|
|
2776
|
+
// reads a FIXED server-controlled detail string carrying no request value.
|
|
2777
|
+
// ===================================================================
|
|
2778
|
+
/** The challenge REQUEST endpoint: `<base>/setup/bootstrap/self-serve`. */
|
|
2779
|
+
export function buildSelfServeVerifyUrl(baseUrl) {
|
|
2780
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve/verify`;
|
|
2781
|
+
}
|
|
2782
|
+
/** The challenge RESEND endpoint: `<base>/setup/bootstrap/self-serve/resend`. */
|
|
2783
|
+
export function buildSelfServeResendUrl(baseUrl) {
|
|
2784
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve/resend`;
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* POST the challenge REQUEST: an email in, a handle out. Never a token.
|
|
2788
|
+
*
|
|
2789
|
+
* The email travels ONLY in the JSON body — never a query string, never a header,
|
|
2790
|
+
* never a log line.
|
|
2791
|
+
*/
|
|
2792
|
+
export async function requestSelfServeChallenge(deps, baseUrl, email) {
|
|
2573
2793
|
let resp;
|
|
2574
2794
|
try {
|
|
2575
|
-
resp = await deps.fetch(
|
|
2795
|
+
resp = await deps.fetch(buildSelfServeMintUrl(baseUrl), {
|
|
2576
2796
|
method: "POST",
|
|
2577
2797
|
headers: { "Content-Type": "application/json" },
|
|
2578
2798
|
body: JSON.stringify({ invitee_email: email }),
|
|
@@ -2580,8 +2800,57 @@ export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
|
2580
2800
|
});
|
|
2581
2801
|
}
|
|
2582
2802
|
catch (err) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2803
|
+
void err;
|
|
2804
|
+
return { ok: false, category: "failed" };
|
|
2805
|
+
}
|
|
2806
|
+
if (resp.ok) {
|
|
2807
|
+
let handle;
|
|
2808
|
+
let expiresAt;
|
|
2809
|
+
try {
|
|
2810
|
+
const body = (await resp.json());
|
|
2811
|
+
handle = body?.handle;
|
|
2812
|
+
expiresAt = body?.expires_at;
|
|
2813
|
+
}
|
|
2814
|
+
catch {
|
|
2815
|
+
return { ok: false, category: "failed" };
|
|
2816
|
+
}
|
|
2817
|
+
if (typeof handle !== "string" ||
|
|
2818
|
+
handle.trim().length === 0 ||
|
|
2819
|
+
typeof expiresAt !== "string" ||
|
|
2820
|
+
expiresAt.trim().length === 0) {
|
|
2821
|
+
// A success body without challenge metadata is not something this client can
|
|
2822
|
+
// act on. It is also what an OLD server (pre-BAPI-757, still returning a
|
|
2823
|
+
// token) looks like — and continuing from one would mean skipping
|
|
2824
|
+
// verification entirely, so it is refused rather than tolerated.
|
|
2825
|
+
return { ok: false, category: "failed" };
|
|
2826
|
+
}
|
|
2827
|
+
return { ok: true, handle: handle.trim(), expiresAt: expiresAt.trim() };
|
|
2828
|
+
}
|
|
2829
|
+
if (resp.status === 429)
|
|
2830
|
+
return { ok: false, category: "rate-limited" };
|
|
2831
|
+
if (resp.status === 503)
|
|
2832
|
+
return { ok: false, category: "unavailable" };
|
|
2833
|
+
if (resp.status === 400 || resp.status === 422)
|
|
2834
|
+
return { ok: false, category: "invalid" };
|
|
2835
|
+
return { ok: false, category: "failed" };
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* POST the VERIFY phase: a handle and a code in, the invite token out.
|
|
2839
|
+
*
|
|
2840
|
+
* The code is sent once, in the body, and is never echoed back into terminal
|
|
2841
|
+
* output or into any error this returns.
|
|
2842
|
+
*/
|
|
2843
|
+
export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
|
|
2844
|
+
let resp;
|
|
2845
|
+
try {
|
|
2846
|
+
resp = await deps.fetch(buildSelfServeVerifyUrl(baseUrl), {
|
|
2847
|
+
method: "POST",
|
|
2848
|
+
headers: { "Content-Type": "application/json" },
|
|
2849
|
+
body: JSON.stringify({ handle, code }),
|
|
2850
|
+
signal: AbortSignal.timeout(10_000),
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
catch (err) {
|
|
2585
2854
|
void err;
|
|
2586
2855
|
return { ok: false, category: "failed" };
|
|
2587
2856
|
}
|
|
@@ -2592,7 +2861,6 @@ export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
|
2592
2861
|
token = body?.token;
|
|
2593
2862
|
}
|
|
2594
2863
|
catch {
|
|
2595
|
-
// An unreadable success body is not actionable and must not be exposed.
|
|
2596
2864
|
return { ok: false, category: "failed" };
|
|
2597
2865
|
}
|
|
2598
2866
|
if (typeof token !== "string" ||
|
|
@@ -2602,14 +2870,83 @@ export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
|
2602
2870
|
}
|
|
2603
2871
|
return { ok: true, token };
|
|
2604
2872
|
}
|
|
2605
|
-
//
|
|
2606
|
-
//
|
|
2607
|
-
|
|
2873
|
+
// 401 is the server's uniform invalid-challenge outcome. It covers a wrong code,
|
|
2874
|
+
// an expired code, an unknown handle, and an exhausted challenge, and the client
|
|
2875
|
+
// deliberately cannot tell them apart — so neither can a caller watching the
|
|
2876
|
+
// client's output.
|
|
2877
|
+
if (resp.status === 401)
|
|
2878
|
+
return { ok: false, category: "rejected" };
|
|
2879
|
+
if (resp.status === 429)
|
|
2608
2880
|
return { ok: false, category: "rate-limited" };
|
|
2881
|
+
// A 5xx is the server failing, not the protocol disagreeing. Kept distinct so the
|
|
2882
|
+
// caller can say "try again" instead of "upgrade the CLI".
|
|
2883
|
+
if (resp.status >= 500)
|
|
2884
|
+
return { ok: false, category: "server-error" };
|
|
2885
|
+
return { ok: false, category: "failed" };
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* POST the RESEND phase: the handle alone, timing metadata out.
|
|
2889
|
+
*
|
|
2890
|
+
* No email field — accepting one would let a caller request mail for an address
|
|
2891
|
+
* they hold no challenge for, which is the abuse the per-email budget bounds.
|
|
2892
|
+
*/
|
|
2893
|
+
export async function resendSelfServeChallenge(deps, baseUrl, handle) {
|
|
2894
|
+
let resp;
|
|
2895
|
+
try {
|
|
2896
|
+
resp = await deps.fetch(buildSelfServeResendUrl(baseUrl), {
|
|
2897
|
+
method: "POST",
|
|
2898
|
+
headers: { "Content-Type": "application/json" },
|
|
2899
|
+
body: JSON.stringify({ handle }),
|
|
2900
|
+
signal: AbortSignal.timeout(10_000),
|
|
2901
|
+
});
|
|
2609
2902
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2903
|
+
catch (err) {
|
|
2904
|
+
void err;
|
|
2905
|
+
return { ok: false, category: "failed" };
|
|
2906
|
+
}
|
|
2907
|
+
if (resp.ok) {
|
|
2908
|
+
let expiresAt;
|
|
2909
|
+
let resendsRemaining;
|
|
2910
|
+
try {
|
|
2911
|
+
const body = (await resp.json());
|
|
2912
|
+
expiresAt = body?.expires_at;
|
|
2913
|
+
resendsRemaining = body?.resends_remaining;
|
|
2914
|
+
}
|
|
2915
|
+
catch {
|
|
2916
|
+
return { ok: false, category: "failed" };
|
|
2917
|
+
}
|
|
2918
|
+
if (typeof expiresAt !== "string" || expiresAt.trim().length === 0) {
|
|
2919
|
+
return { ok: false, category: "failed" };
|
|
2920
|
+
}
|
|
2921
|
+
return {
|
|
2922
|
+
ok: true,
|
|
2923
|
+
expiresAt: expiresAt.trim(),
|
|
2924
|
+
resendsRemaining: typeof resendsRemaining === "number" && Number.isFinite(resendsRemaining)
|
|
2925
|
+
? resendsRemaining
|
|
2926
|
+
: 0,
|
|
2927
|
+
};
|
|
2612
2928
|
}
|
|
2929
|
+
// Cooldown and the send ceilings are BOTH 429 and are distinguished only by the
|
|
2930
|
+
// fixed detail string. Reading it is safe here precisely because it is fixed and
|
|
2931
|
+
// server-controlled — it carries no request value — and the distinction lets the
|
|
2932
|
+
// CLI say "wait a moment" instead of "try again later". Any parse problem falls
|
|
2933
|
+
// through to the more conservative `rate-limited`.
|
|
2934
|
+
if (resp.status === 429) {
|
|
2935
|
+
let detail = "";
|
|
2936
|
+
try {
|
|
2937
|
+
const body = (await resp.json());
|
|
2938
|
+
detail = typeof body?.detail === "string" ? body.detail : "";
|
|
2939
|
+
}
|
|
2940
|
+
catch {
|
|
2941
|
+
detail = "";
|
|
2942
|
+
}
|
|
2943
|
+
return {
|
|
2944
|
+
ok: false,
|
|
2945
|
+
category: detail.toLowerCase().includes("very recently") ? "cooldown" : "rate-limited",
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
if (resp.status === 401)
|
|
2949
|
+
return { ok: false, category: "rejected" };
|
|
2613
2950
|
return { ok: false, category: "failed" };
|
|
2614
2951
|
}
|
|
2615
2952
|
/**
|
|
@@ -3905,7 +4242,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3905
4242
|
// ---- Step 1 — scaffold (secret-free placeholders only) ----
|
|
3906
4243
|
currentStep = INSTALL_BRIDGE_STEP_LABELS.scaffold;
|
|
3907
4244
|
log(currentStep);
|
|
3908
|
-
|
|
4245
|
+
// BAPI-728: hand init the identity this run already resolved and validated.
|
|
4246
|
+
// `credentialAvailable: true` is a statement about THIS flow, not a lookup:
|
|
4247
|
+
// Step 2 mints/verifies the key and Step 4 persists it, so a placeholder
|
|
4248
|
+
// warning here would be a false alarm about a config the installer is
|
|
4249
|
+
// actively configuring. The key value itself never crosses this boundary.
|
|
4250
|
+
await deps.runInit(deps.cwd, {
|
|
4251
|
+
creationContext: { repoName, baseUrl, credentialAvailable: true },
|
|
4252
|
+
});
|
|
3909
4253
|
// ---- Step 2 — mint (invite mode) or verify (normal mode) the credential ----
|
|
3910
4254
|
let inviteFingerprint = "";
|
|
3911
4255
|
if (bootstrapInviteMode) {
|
|
@@ -3923,13 +4267,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3923
4267
|
let resumedSelfServeReplay = false;
|
|
3924
4268
|
let keySecret = "";
|
|
3925
4269
|
let reusedPendingSecret = false;
|
|
4270
|
+
// BAPI-757: a LIVE challenge found by the lookup below. When set, the run
|
|
4271
|
+
// resumes at the code prompt and MUST NOT request or resend — the emailed
|
|
4272
|
+
// code is still valid, and asking again would spend one of the user's own
|
|
4273
|
+
// per-address sends for nothing.
|
|
4274
|
+
let liveChallenge = null;
|
|
3926
4275
|
if (selfServeSignupMode) {
|
|
3927
|
-
const lookup = await deps.lookupSelfServeBootstrapPending({ repoName }, credentialWriteDeps);
|
|
4276
|
+
const lookup = await deps.lookupSelfServeBootstrapPending({ repoName, now: deps.now() }, credentialWriteDeps);
|
|
3928
4277
|
if (!lookup.ok) {
|
|
3929
|
-
// Fail BEFORE
|
|
3930
|
-
// untouched — an ordinary-invite record, a malformed record,
|
|
3931
|
-
// unreadable store are all states where
|
|
3932
|
-
// problem rather than recover from it.
|
|
4278
|
+
// Fail BEFORE any endpoint is called and leave the stored record
|
|
4279
|
+
// untouched — an ordinary-invite record, a malformed record, a partial
|
|
4280
|
+
// challenge record, and an unreadable store are all states where
|
|
4281
|
+
// starting over would compound the problem rather than recover from it.
|
|
3933
4282
|
return fatal(`Error: ${lookup.error}`);
|
|
3934
4283
|
}
|
|
3935
4284
|
if (lookup.state === "resumable") {
|
|
@@ -3943,37 +4292,176 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3943
4292
|
reusedPendingSecret = true;
|
|
3944
4293
|
log(` resuming your previous signup attempt for ${repoName}`);
|
|
3945
4294
|
}
|
|
4295
|
+
else if (lookup.state === "challenge") {
|
|
4296
|
+
liveChallenge = {
|
|
4297
|
+
handle: lookup.challengeHandle,
|
|
4298
|
+
expiresAt: lookup.challengeExpiresAt,
|
|
4299
|
+
email: lookup.signupEmail,
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
// `challenge-expired` and `none` both fall through to a fresh request.
|
|
3946
4303
|
}
|
|
3947
4304
|
/**
|
|
3948
|
-
*
|
|
4305
|
+
* Run the self-serve signup through request → verify, then durably store the
|
|
4306
|
+
* resulting invite token WITH a fresh `key_secret` (BAPI-757).
|
|
3949
4307
|
*
|
|
3950
|
-
*
|
|
3951
|
-
*
|
|
3952
|
-
*
|
|
3953
|
-
*
|
|
3954
|
-
*
|
|
4308
|
+
* Three phases, in this order, and the order is the protocol:
|
|
4309
|
+
*
|
|
4310
|
+
* 1. **Request** (skipped when resuming a live challenge). The server
|
|
4311
|
+
* creates a challenge and queues an email. No tenant, no invitation, and
|
|
4312
|
+
* nothing billable exists yet.
|
|
4313
|
+
* 2. **Persist the challenge, THEN prompt.** The email has already been
|
|
4314
|
+
* sent and the user's per-address send budget already charged by this
|
|
4315
|
+
* point, so the handle reaches disk before anything can interrupt the
|
|
4316
|
+
* run. A terminal closed at the prompt resumes the SAME challenge with
|
|
4317
|
+
* the SAME code.
|
|
4318
|
+
* 3. **Verify**, which is what actually mints — and hands back the very
|
|
4319
|
+
* `inviteToken` the untouched persist-before-exchange region below
|
|
4320
|
+
* already expected.
|
|
4321
|
+
*
|
|
4322
|
+
* The verification code is prompted for, submitted once, and never written to
|
|
4323
|
+
* disk, never logged, and never echoed.
|
|
4324
|
+
*
|
|
4325
|
+
* Logs its own (secret-free) failure and returns false; the caller exits
|
|
4326
|
+
* non-zero.
|
|
3955
4327
|
*/
|
|
3956
4328
|
const mintAndPrepareSelfServe = async () => {
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
4329
|
+
let challengeHandle;
|
|
4330
|
+
let challengeExpiresAt;
|
|
4331
|
+
let challengeEmail;
|
|
4332
|
+
if (liveChallenge) {
|
|
4333
|
+
// Phase 1 SKIPPED. Resuming an unexpired challenge: no request call, no
|
|
4334
|
+
// resend call, and no second email.
|
|
4335
|
+
challengeHandle = liveChallenge.handle;
|
|
4336
|
+
challengeExpiresAt = liveChallenge.expiresAt;
|
|
4337
|
+
challengeEmail = liveChallenge.email;
|
|
4338
|
+
log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`);
|
|
4339
|
+
log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
4340
|
+
}
|
|
4341
|
+
else {
|
|
4342
|
+
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
4343
|
+
if (!requested.ok) {
|
|
4344
|
+
if (requested.category === "rate-limited") {
|
|
4345
|
+
// Deliberately does NOT name a limiter bucket. The server refuses to
|
|
4346
|
+
// say which ceiling was reached, and repeating a guess here would
|
|
4347
|
+
// undo that.
|
|
4348
|
+
reportFatal(`Error: ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
4349
|
+
}
|
|
4350
|
+
else if (requested.category === "unavailable") {
|
|
4351
|
+
// R-4's fail-closed branch: nothing was created, and there is no
|
|
4352
|
+
// suggestion anywhere that verification can be skipped.
|
|
4353
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
|
|
4354
|
+
}
|
|
4355
|
+
else if (requested.category === "invalid") {
|
|
4356
|
+
reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again.");
|
|
4357
|
+
}
|
|
4358
|
+
else {
|
|
4359
|
+
// R-2: a malformed or tokenless-unexpected response is a PROTOCOL
|
|
4360
|
+
// mismatch, not a network problem. Saying "check connectivity" here
|
|
4361
|
+
// was the old behavior and is exactly what this ticket replaces.
|
|
4362
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
|
|
4363
|
+
}
|
|
4364
|
+
return false;
|
|
3966
4365
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
4366
|
+
challengeHandle = requested.handle;
|
|
4367
|
+
challengeExpiresAt = requested.expiresAt;
|
|
4368
|
+
challengeEmail = signupEmail;
|
|
4369
|
+
// Phase 2, first half — durable BEFORE the prompt. A failure here stops
|
|
4370
|
+
// the run: continuing would prompt for a code whose challenge could not
|
|
4371
|
+
// be resumed if anything interrupted the terminal.
|
|
4372
|
+
const recorded = await deps.prepareSelfServeBootstrapChallenge({
|
|
4373
|
+
repoName,
|
|
4374
|
+
signupEmail: challengeEmail,
|
|
4375
|
+
challengeHandle,
|
|
4376
|
+
challengeExpiresAt,
|
|
4377
|
+
}, credentialWriteDeps);
|
|
4378
|
+
if (!recorded.ok) {
|
|
4379
|
+
reportFatal(`Error: could not record the signup attempt (${recorded.kind}). ${recorded.error} ` +
|
|
4380
|
+
"No workspace has been set up — fix the problem and re-run.");
|
|
4381
|
+
return false;
|
|
3969
4382
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
4383
|
+
log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
4384
|
+
}
|
|
4385
|
+
log(` Enter the ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to finish setting up your workspace.`);
|
|
4386
|
+
const remaining = formatChallengeRemaining(challengeExpiresAt, deps.now());
|
|
4387
|
+
if (remaining)
|
|
4388
|
+
log(` ${remaining}`);
|
|
4389
|
+
// Phase 2, second half, and phase 3. One resend is offered on the recovery
|
|
4390
|
+
// path only — never proactively — so the budget is spent on the user's
|
|
4391
|
+
// explicit request rather than by the CLI's own retry logic.
|
|
4392
|
+
let resendOffered = false;
|
|
4393
|
+
// BOUNDED, not `for (;;)`. AC-4 makes a wrong code and a dead challenge
|
|
4394
|
+
// byte-identical, so the CLI cannot detect that a challenge has been killed
|
|
4395
|
+
// — an unbounded loop would re-prompt forever against something that can
|
|
4396
|
+
// never succeed, with Ctrl-C as the only way out.
|
|
4397
|
+
let rejections = 0;
|
|
4398
|
+
for (;;) {
|
|
4399
|
+
const code = await promptForVerificationCode(deps, log);
|
|
4400
|
+
if (code === null) {
|
|
4401
|
+
reportFatal("Error: No verification code entered — nothing has been set up. Re-run this command " +
|
|
4402
|
+
"to continue; the code already emailed to you stays valid until it expires.");
|
|
4403
|
+
return false;
|
|
4404
|
+
}
|
|
4405
|
+
const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
|
|
4406
|
+
if (verified.ok) {
|
|
4407
|
+
inviteToken = verified.token;
|
|
4408
|
+
break;
|
|
4409
|
+
}
|
|
4410
|
+
if (verified.category === "rate-limited") {
|
|
4411
|
+
// NOT the resend wording: a throttle here comes from the invite-mint
|
|
4412
|
+
// ceiling and can fire AFTER a perfectly correct code, so telling the
|
|
4413
|
+
// user to fall back on an email they already have is backwards.
|
|
4414
|
+
reportFatal(`Error: ${INSTALL_BRIDGE_VERIFY_THROTTLED}`);
|
|
4415
|
+
return false;
|
|
4416
|
+
}
|
|
4417
|
+
if (verified.category === "server-error") {
|
|
4418
|
+
reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
|
|
4419
|
+
return false;
|
|
4420
|
+
}
|
|
4421
|
+
if (verified.category === "failed") {
|
|
4422
|
+
// Only a genuine protocol mismatch reaches here now: a malformed body,
|
|
4423
|
+
// a missing token, a network error, or a 4xx this client does not
|
|
4424
|
+
// expect. R-2's upgrade instruction is the right answer for those.
|
|
4425
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
|
|
4426
|
+
return false;
|
|
4427
|
+
}
|
|
4428
|
+
// `rejected` — the server's UNIFORM outcome. One message, no speculation
|
|
4429
|
+
// about which of the four states it was.
|
|
4430
|
+
rejections += 1;
|
|
4431
|
+
if (rejections >= INSTALL_BRIDGE_MAX_CODE_REJECTIONS) {
|
|
4432
|
+
// Still state-neutral: this says the attempt is over, never WHY.
|
|
4433
|
+
reportFatal(INSTALL_BRIDGE_VERIFICATION_GIVING_UP);
|
|
4434
|
+
return false;
|
|
4435
|
+
}
|
|
4436
|
+
errorLog(INSTALL_BRIDGE_VERIFICATION_REJECTED);
|
|
4437
|
+
if (!resendOffered && deps.isTTY && deps.promptLine) {
|
|
4438
|
+
resendOffered = true;
|
|
4439
|
+
const answer = await requestCredentialRouteSwitchConfirmation(deps, "Send a new code? [y/N]: ");
|
|
4440
|
+
if (answer) {
|
|
4441
|
+
const resent = await resendSelfServeChallenge(deps, baseUrl, challengeHandle);
|
|
4442
|
+
if (resent.ok) {
|
|
4443
|
+
log(` ${INSTALL_BRIDGE_RESEND_SENT}`);
|
|
4444
|
+
}
|
|
4445
|
+
else if (resent.category === "cooldown") {
|
|
4446
|
+
log(` ${INSTALL_BRIDGE_RESEND_COOLDOWN}`);
|
|
4447
|
+
}
|
|
4448
|
+
else if (resent.category === "rate-limited") {
|
|
4449
|
+
log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
4450
|
+
}
|
|
4451
|
+
else if (resent.category === "rejected") {
|
|
4452
|
+
// The challenge is no longer eligible — the same uniform outcome
|
|
4453
|
+
// verification reports. Stop rather than loop on a dead challenge.
|
|
4454
|
+
reportFatal(`Error: ${INSTALL_BRIDGE_VERIFICATION_REJECTED}`);
|
|
4455
|
+
return false;
|
|
4456
|
+
}
|
|
4457
|
+
else {
|
|
4458
|
+
log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
3972
4461
|
}
|
|
3973
|
-
return false;
|
|
3974
4462
|
}
|
|
3975
|
-
//
|
|
3976
|
-
|
|
4463
|
+
// From here on the token is treated identically to a manually supplied
|
|
4464
|
+
// invite — the load-bearing region below is completely unchanged.
|
|
3977
4465
|
inviteFingerprint = fingerprintBootstrapInvite(inviteToken);
|
|
3978
4466
|
const prepareSelfServePending = (allowOverwrite) => deps.prepareBootstrapPending({
|
|
3979
4467
|
repoName,
|