@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* {@link resolveCredentialBundle}, and the durable source of truth is the
|
|
38
38
|
* user-scoped `~/.config/bridge/credentials.json` store written here.
|
|
39
39
|
*/
|
|
40
|
+
import { validateBootstrapDisplayName, validateBootstrapRepoName } from "./bootstrap-project-identity.js";
|
|
40
41
|
import path from "path";
|
|
41
42
|
import { diagnosticComponent } from "./epic-launch-diagnostic.js";
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
@@ -336,37 +337,30 @@ function defaultTempSuffix() {
|
|
|
336
337
|
// ---------------------------------------------------------------------------
|
|
337
338
|
/** Poll interval while waiting for a contended lock. */
|
|
338
339
|
const LOCK_POLL_INTERVAL_MS = 50;
|
|
339
|
-
/** Bounded wait
|
|
340
|
+
/** Bounded acquisition wait; elapsed time never establishes abandonment. */
|
|
340
341
|
const LOCK_TIMEOUT_MS = 5_000;
|
|
341
342
|
/** Lock path, adjacent to the primary store. Never carries secret material. */
|
|
342
343
|
export function getCredentialStoreLockPath(deps) {
|
|
343
344
|
return `${getPrimaryCredentialStorePath(deps)}.lock`;
|
|
344
345
|
}
|
|
345
346
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* When `deps.open` is absent there is no lock primitive available, so the caller
|
|
350
|
-
* runs unlocked — exactly the pre-BAPI-606 behavior, preserved so existing
|
|
351
|
-
* builders and in-memory test doubles keep working.
|
|
352
|
-
*
|
|
353
|
-
* Stale-lock recovery: a lock still held after {@link LOCK_TIMEOUT_MS} (orders of
|
|
354
|
-
* magnitude longer than a legitimate sub-millisecond read-modify-write) is
|
|
355
|
-
* treated as abandoned by a crashed process and stolen exactly once. The
|
|
356
|
-
* alternative — refusing forever — wedges every future install behind a lock file
|
|
357
|
-
* no user knows to delete.
|
|
347
|
+
* Exclusive creation with bounded fail-closed contention. No writer steals or
|
|
348
|
+
* replaces a held lock; only the successful owner can release it. Abandonment
|
|
349
|
+
* cannot be established by this portable primitive, so recovery requires support.
|
|
358
350
|
*/
|
|
359
351
|
async function acquireCredentialStoreLock(deps) {
|
|
360
352
|
const open = deps.open;
|
|
361
|
-
if (!open)
|
|
362
|
-
return { ok:
|
|
353
|
+
if (!open || !deps.unlink)
|
|
354
|
+
return { ok: false, error: "Exclusive credential storage is unavailable. Resume from the original folder with installation support." };
|
|
363
355
|
const lockPath = getCredentialStoreLockPath(deps);
|
|
364
356
|
const isPosix = deps.platform !== "win32";
|
|
365
357
|
const now = deps.now ?? (() => Date.now());
|
|
366
358
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
359
|
+
let released = false;
|
|
367
360
|
const release = async () => {
|
|
368
|
-
if (!deps.unlink)
|
|
361
|
+
if (released || !deps.unlink)
|
|
369
362
|
return;
|
|
363
|
+
released = true;
|
|
370
364
|
try {
|
|
371
365
|
await deps.unlink(lockPath);
|
|
372
366
|
}
|
|
@@ -403,14 +397,9 @@ async function acquireCredentialStoreLock(deps) {
|
|
|
403
397
|
break;
|
|
404
398
|
await sleep(LOCK_POLL_INTERVAL_MS);
|
|
405
399
|
}
|
|
406
|
-
// Bounded wait exhausted — steal the abandoned lock, once.
|
|
407
|
-
await release();
|
|
408
|
-
const stolen = await tryAcquire();
|
|
409
|
-
if (stolen.ok)
|
|
410
|
-
return { ok: true, release };
|
|
411
400
|
return {
|
|
412
401
|
ok: false,
|
|
413
|
-
error: `Timed out waiting for
|
|
402
|
+
error: `Timed out waiting for exclusive credential storage at ${lockPath}. Another installer may still be running. Resume from the original folder with installation support; preserve recovery proof.`,
|
|
414
403
|
};
|
|
415
404
|
}
|
|
416
405
|
/**
|
|
@@ -418,7 +407,9 @@ async function acquireCredentialStoreLock(deps) {
|
|
|
418
407
|
* lock is always released, and `onLockError` maps an acquisition failure into the
|
|
419
408
|
* caller's own (secret-free) result shape.
|
|
420
409
|
*/
|
|
421
|
-
export async function withCredentialStoreLock(deps, fn, onLockError) {
|
|
410
|
+
export async function withCredentialStoreLock(deps, fn, onLockError, requireExclusive = true) {
|
|
411
|
+
if (!requireExclusive && !deps.open)
|
|
412
|
+
return fn();
|
|
422
413
|
const lock = await acquireCredentialStoreLock(deps);
|
|
423
414
|
if (!lock.ok)
|
|
424
415
|
return onLockError(lock.error);
|
|
@@ -457,7 +448,7 @@ export async function durablyReplaceCredentialStoreJson(primaryPath, value, deps
|
|
|
457
448
|
let handle;
|
|
458
449
|
try {
|
|
459
450
|
await deps.mkdir(dir, { recursive: true });
|
|
460
|
-
handle = await open(tempPath, "
|
|
451
|
+
handle = await open(tempPath, "wx", isPosix ? 0o600 : undefined);
|
|
461
452
|
await handle.writeFile(json, { encoding: "utf-8" });
|
|
462
453
|
await handle.sync();
|
|
463
454
|
await handle.close();
|
|
@@ -490,19 +481,22 @@ export async function durablyReplaceCredentialStoreJson(primaryPath, value, deps
|
|
|
490
481
|
error: `Failed to durably write the credentials file at ${primaryPath}.`,
|
|
491
482
|
};
|
|
492
483
|
}
|
|
493
|
-
|
|
494
|
-
|
|
484
|
+
// Windows cannot open directory handles through Node's portable fs API.
|
|
485
|
+
// File sync plus atomic rename still apply; directory durability is limited.
|
|
486
|
+
if (deps.platform !== "win32") {
|
|
495
487
|
try {
|
|
496
|
-
await
|
|
488
|
+
const dirHandle = await open(dir, "r");
|
|
489
|
+
try {
|
|
490
|
+
await dirHandle.sync();
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
await dirHandle.close();
|
|
494
|
+
}
|
|
497
495
|
}
|
|
498
|
-
|
|
499
|
-
|
|
496
|
+
catch {
|
|
497
|
+
return { ok: false, kind: "post-rename-uncertain", error: "Credential replacement completed but durability could not be confirmed. Preserve recovery proof and rerun from the original folder." };
|
|
500
498
|
}
|
|
501
499
|
}
|
|
502
|
-
catch {
|
|
503
|
-
/* directory fsync is unsupported on some platforms; the file fsync above is
|
|
504
|
-
the load-bearing one and already succeeded. */
|
|
505
|
-
}
|
|
506
500
|
return { ok: true };
|
|
507
501
|
}
|
|
508
502
|
// ---------------------------------------------------------------------------
|
|
@@ -597,8 +591,10 @@ async function loadStoreForMutation(deps) {
|
|
|
597
591
|
if (primary.state === "present") {
|
|
598
592
|
return { ok: true, base: { ...primary.value } };
|
|
599
593
|
}
|
|
600
|
-
const
|
|
601
|
-
|
|
594
|
+
const fallback = await readCredentialStoreJsonIfPresent(getFallbackCredentialStorePath(deps), deps);
|
|
595
|
+
if (fallback.state === "error")
|
|
596
|
+
return { ok: false, kind: fallback.kind, error: "Recovery storage could not be read. Preserve it and contact installation support." };
|
|
597
|
+
return { ok: true, base: fallback.state === "present" ? { ...fallback.value } : {} };
|
|
602
598
|
}
|
|
603
599
|
/** Does a real (non-empty) `BAPI_API_KEY` already exist for `bapi:<repo>`? */
|
|
604
600
|
function hasExistingBapiKey(store, repoName) {
|
|
@@ -704,17 +700,8 @@ function storedPendingIsSelfServe(store, repoName) {
|
|
|
704
700
|
* (an ordinary, fingerprint-only record) keeps the original fail-closed guidance,
|
|
705
701
|
* because there the CLI genuinely cannot replay it.
|
|
706
702
|
*/
|
|
707
|
-
function pendingConflictError(
|
|
708
|
-
|
|
709
|
-
return (`A pending self-serve signup for a DIFFERENT invite already exists at ${target} in ` +
|
|
710
|
-
`${primaryPath}. It is the only proof that can replay that signup, so it will not be ` +
|
|
711
|
-
"overwritten. Re-run install-bridge and choose the email option — it will resume that " +
|
|
712
|
-
"signup automatically. Do NOT remove the entry by hand.");
|
|
713
|
-
}
|
|
714
|
-
return (`A pending bootstrap-invite credential for a DIFFERENT invite already exists at ${target} ` +
|
|
715
|
-
`in ${primaryPath}. It is the only proof that can replay that redemption, so it will not be ` +
|
|
716
|
-
"overwritten. Complete that redemption first, or — only if you are certain its invite was " +
|
|
717
|
-
"never exchanged — remove the entry from the store by hand.");
|
|
703
|
+
function pendingConflictError(_target, _primaryPath, _storedIsSelfServe) {
|
|
704
|
+
return "An earlier installation has saved recovery proof. Resume from its original folder with its original invite or signup flow, or contact installation support. Preserve the saved proof.";
|
|
718
705
|
}
|
|
719
706
|
/**
|
|
720
707
|
* Generate (or REUSE) the bootstrap `key_secret` and durably persist it as a
|
|
@@ -1413,7 +1400,7 @@ export async function upsertBapiCredential(repoName, apiKey, deps) {
|
|
|
1413
1400
|
};
|
|
1414
1401
|
}
|
|
1415
1402
|
return { ok: true, path: primaryPath, target, action, migratedFallback };
|
|
1416
|
-
}, (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error }));
|
|
1403
|
+
}, (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error }), false);
|
|
1417
1404
|
}
|
|
1418
1405
|
// ---------------------------------------------------------------------------
|
|
1419
1406
|
// Epic-launch credential binding (BAPI-1260)
|
|
@@ -1603,3 +1590,176 @@ export async function bindEpicLaunchCredential(repoName, targetBaseUrl, deps) {
|
|
|
1603
1590
|
return { ok: true, target, action: previous === null ? "bound" : "rebound", path: primaryPath };
|
|
1604
1591
|
}, (error) => ({ ok: false, target, kind: "lock-error", error }));
|
|
1605
1592
|
}
|
|
1593
|
+
const modernRecoveryError = "Installation recovery is inconsistent or unavailable. Preserve proof and resume with the original folder and invite, or contact installation support.";
|
|
1594
|
+
function parseModernBootstrapRecord(entry) {
|
|
1595
|
+
if (!entry)
|
|
1596
|
+
return null;
|
|
1597
|
+
const raw = JSON.parse(entry.RECORD);
|
|
1598
|
+
if (raw.version !== 1 || raw.mode !== "server_assigned" || typeof raw.baseUrl !== "string" || typeof raw.folder !== "string" || typeof raw.label !== "string" || typeof raw.keySecret !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(raw.keySecret))
|
|
1599
|
+
throw new Error(modernRecoveryError);
|
|
1600
|
+
validateBootstrapDisplayName(raw.label);
|
|
1601
|
+
if (raw.fingerprint !== undefined && !/^[a-f0-9]{64}$/.test(raw.fingerprint))
|
|
1602
|
+
throw new Error(modernRecoveryError);
|
|
1603
|
+
if (raw.canonical !== undefined) {
|
|
1604
|
+
validateBootstrapRepoName(raw.canonical);
|
|
1605
|
+
validateBootstrapDisplayName(raw.serverLabel);
|
|
1606
|
+
}
|
|
1607
|
+
return raw;
|
|
1608
|
+
}
|
|
1609
|
+
export async function mutateModernBootstrapRecord(scope, deps, change, previousLocator) {
|
|
1610
|
+
const primaryPath = getPrimaryCredentialStorePath(deps);
|
|
1611
|
+
return withCredentialStoreLock(deps, async () => {
|
|
1612
|
+
try {
|
|
1613
|
+
const loaded = await loadStoreForMutation(deps);
|
|
1614
|
+
if (!loaded.ok)
|
|
1615
|
+
return { ok: false, error: modernRecoveryError };
|
|
1616
|
+
const store = loaded.base;
|
|
1617
|
+
const reference = store[scope.folderReference];
|
|
1618
|
+
const actualLocator = reference?.LOCATOR ?? scope.locator;
|
|
1619
|
+
if (reference && (reference.FOLDER !== scope.folder || reference.BASE_URL !== scope.baseUrl || (actualLocator !== scope.locator && actualLocator !== previousLocator)))
|
|
1620
|
+
throw new Error(modernRecoveryError);
|
|
1621
|
+
const record = parseModernBootstrapRecord(store[actualLocator]);
|
|
1622
|
+
if (reference && !record)
|
|
1623
|
+
throw new Error(modernRecoveryError);
|
|
1624
|
+
if (record && (!reference || record.folder !== scope.folder || record.baseUrl !== scope.baseUrl))
|
|
1625
|
+
throw new Error(modernRecoveryError);
|
|
1626
|
+
const next = change(record);
|
|
1627
|
+
if (next.folder !== scope.folder || next.baseUrl !== scope.baseUrl || (record && (record.keySecret !== next.keySecret || record.label !== next.label)))
|
|
1628
|
+
throw new Error(modernRecoveryError);
|
|
1629
|
+
const destination = store[scope.locator];
|
|
1630
|
+
if (actualLocator !== scope.locator && destination && JSON.stringify(parseModernBootstrapRecord(destination)) !== JSON.stringify(next))
|
|
1631
|
+
throw new Error(modernRecoveryError);
|
|
1632
|
+
const updated = { ...store, [scope.locator]: { RECORD: JSON.stringify(next) }, [scope.folderReference]: { LOCATOR: scope.locator, FOLDER: scope.folder, BASE_URL: scope.baseUrl } };
|
|
1633
|
+
if (actualLocator !== scope.locator)
|
|
1634
|
+
delete updated[actualLocator];
|
|
1635
|
+
const written = await durablyReplaceCredentialStoreJson(primaryPath, updated, deps);
|
|
1636
|
+
return written.ok ? { ok: true, record: next, path: primaryPath } : { ok: false, error: written.error };
|
|
1637
|
+
}
|
|
1638
|
+
catch {
|
|
1639
|
+
return { ok: false, error: modernRecoveryError };
|
|
1640
|
+
}
|
|
1641
|
+
}, () => ({ ok: false, error: modernRecoveryError }));
|
|
1642
|
+
}
|
|
1643
|
+
export async function lookupModernBootstrapRecord(scope, deps) {
|
|
1644
|
+
return withCredentialStoreLock(deps, async () => {
|
|
1645
|
+
try {
|
|
1646
|
+
const loaded = await loadStoreForMutation(deps);
|
|
1647
|
+
if (!loaded.ok)
|
|
1648
|
+
throw new Error(modernRecoveryError);
|
|
1649
|
+
const reference = loaded.base[scope.folderReference];
|
|
1650
|
+
if (!reference) {
|
|
1651
|
+
// A stray modern or legacy record could be a redeemed proof; never replace it.
|
|
1652
|
+
if (loaded.base[scope.locator])
|
|
1653
|
+
throw new Error(modernRecoveryError);
|
|
1654
|
+
return { ok: true, record: null, path: getPrimaryCredentialStorePath(deps) };
|
|
1655
|
+
}
|
|
1656
|
+
if (reference.FOLDER !== scope.folder || reference.BASE_URL !== scope.baseUrl)
|
|
1657
|
+
throw new Error(modernRecoveryError);
|
|
1658
|
+
const record = parseModernBootstrapRecord(loaded.base[reference.LOCATOR]);
|
|
1659
|
+
if (!record || record.folder !== scope.folder || record.baseUrl !== scope.baseUrl)
|
|
1660
|
+
throw new Error(modernRecoveryError);
|
|
1661
|
+
if (scope.locator.startsWith("bootstrap-pending:invite:") && reference.LOCATOR !== scope.locator)
|
|
1662
|
+
throw new Error(modernRecoveryError);
|
|
1663
|
+
return { ok: true, record, path: getPrimaryCredentialStorePath(deps) };
|
|
1664
|
+
}
|
|
1665
|
+
catch {
|
|
1666
|
+
return { ok: false, error: modernRecoveryError };
|
|
1667
|
+
}
|
|
1668
|
+
}, () => ({ ok: false, error: modernRecoveryError }));
|
|
1669
|
+
}
|
|
1670
|
+
export async function prepareModernBootstrapInvite(scope, fingerprint, label, generateKeySecret, deps) {
|
|
1671
|
+
return mutateModernBootstrapRecord(scope, deps, record => {
|
|
1672
|
+
if (record) {
|
|
1673
|
+
if (record.fingerprint !== fingerprint)
|
|
1674
|
+
throw new Error(modernRecoveryError);
|
|
1675
|
+
return record;
|
|
1676
|
+
}
|
|
1677
|
+
return { version: 1, mode: "server_assigned", baseUrl: scope.baseUrl, folder: scope.folder,
|
|
1678
|
+
label: validateBootstrapDisplayName(label), keySecret: generateKeySecret(), fingerprint };
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
export async function lookupLegacyBootstrapInvite(fingerprint, deps) {
|
|
1682
|
+
return withCredentialStoreLock(deps, async () => {
|
|
1683
|
+
try {
|
|
1684
|
+
const loaded = await loadStoreForMutation(deps);
|
|
1685
|
+
if (!loaded.ok)
|
|
1686
|
+
throw new Error(modernRecoveryError);
|
|
1687
|
+
const candidates = Object.entries(loaded.base).filter(([target, entry]) => target.startsWith(BOOTSTRAP_PENDING_TARGET_PREFIX) && entry.BOOTSTRAP_INVITE_FINGERPRINT === fingerprint);
|
|
1688
|
+
if (candidates.length > 1)
|
|
1689
|
+
throw new Error(modernRecoveryError);
|
|
1690
|
+
if (!candidates.length)
|
|
1691
|
+
return { ok: true };
|
|
1692
|
+
const [target, entry] = candidates[0];
|
|
1693
|
+
const repoName = validateBootstrapRepoName(target.slice(BOOTSTRAP_PENDING_TARGET_PREFIX.length));
|
|
1694
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(entry.BAPI_API_KEY))
|
|
1695
|
+
throw new Error(modernRecoveryError);
|
|
1696
|
+
return { ok: true, repoName };
|
|
1697
|
+
}
|
|
1698
|
+
catch {
|
|
1699
|
+
return { ok: false, error: modernRecoveryError };
|
|
1700
|
+
}
|
|
1701
|
+
}, () => ({ ok: false, error: modernRecoveryError }));
|
|
1702
|
+
}
|
|
1703
|
+
export async function prepareModernBootstrapSignup(scope, label, email, generateKeySecret, deps) {
|
|
1704
|
+
return mutateModernBootstrapRecord(scope, deps, record => {
|
|
1705
|
+
if (record)
|
|
1706
|
+
return record;
|
|
1707
|
+
return { version: 1, mode: "server_assigned", baseUrl: scope.baseUrl, folder: scope.folder,
|
|
1708
|
+
label: validateBootstrapDisplayName(label), keySecret: generateKeySecret(), signupEmail: email };
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
export async function transitionModernBootstrapSignup(signupScope, inviteScope, fingerprint, replayToken, challengeHandle, deps) {
|
|
1712
|
+
return mutateModernBootstrapRecord(inviteScope, deps, record => {
|
|
1713
|
+
if (!record || record.challengeHandle !== challengeHandle)
|
|
1714
|
+
throw new Error(modernRecoveryError);
|
|
1715
|
+
if (record.fingerprint && (record.fingerprint !== fingerprint || record.replayToken !== replayToken))
|
|
1716
|
+
throw new Error(modernRecoveryError);
|
|
1717
|
+
return { ...record, fingerprint, replayToken };
|
|
1718
|
+
}, signupScope.locator);
|
|
1719
|
+
}
|
|
1720
|
+
export async function checkpointModernBootstrapIdentity(scope, proof, canonical, serverLabel, deps) {
|
|
1721
|
+
return mutateModernBootstrapRecord(scope, deps, record => {
|
|
1722
|
+
if (!record || record.keySecret !== proof.keySecret || record.fingerprint !== proof.fingerprint ||
|
|
1723
|
+
(record.canonical && record.canonical !== canonical))
|
|
1724
|
+
throw new Error(modernRecoveryError);
|
|
1725
|
+
return { ...record, canonical: validateBootstrapRepoName(canonical), serverLabel: validateBootstrapDisplayName(serverLabel) };
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
export async function promoteModernBootstrapCredential(scope, proof, deps, allowOverwrite = false, consentedKey) {
|
|
1729
|
+
return withCredentialStoreLock(deps, async () => {
|
|
1730
|
+
try {
|
|
1731
|
+
if (!proof.canonical || !proof.fingerprint)
|
|
1732
|
+
throw new Error(modernRecoveryError);
|
|
1733
|
+
const loaded = await loadStoreForMutation(deps);
|
|
1734
|
+
if (!loaded.ok)
|
|
1735
|
+
throw new Error(modernRecoveryError);
|
|
1736
|
+
const store = loaded.base;
|
|
1737
|
+
const target = getBapiTarget(validateBootstrapRepoName(proof.canonical));
|
|
1738
|
+
const existingKey = store[target]?.BAPI_API_KEY;
|
|
1739
|
+
const record = parseModernBootstrapRecord(store[scope.locator]);
|
|
1740
|
+
const reference = store[scope.folderReference];
|
|
1741
|
+
// A completed uncertain/concurrent promotion is recognized by exact proof.
|
|
1742
|
+
if (!record && !reference && existingKey === proof.keySecret)
|
|
1743
|
+
return { ok: true, path: getPrimaryCredentialStorePath(deps) };
|
|
1744
|
+
if (!record || record.keySecret !== proof.keySecret || record.canonical !== proof.canonical || record.fingerprint !== proof.fingerprint || record.folder !== scope.folder || record.baseUrl !== scope.baseUrl || reference?.LOCATOR !== scope.locator || reference.FOLDER !== scope.folder || reference.BASE_URL !== scope.baseUrl)
|
|
1745
|
+
throw new Error(modernRecoveryError);
|
|
1746
|
+
if (existingKey && existingKey !== proof.keySecret) {
|
|
1747
|
+
if (!allowOverwrite)
|
|
1748
|
+
return { ok: false, error: "The canonical credential already exists; overwrite consent is required.", needsConsent: true, observedKey: existingKey };
|
|
1749
|
+
if (consentedKey !== undefined && existingKey !== consentedKey)
|
|
1750
|
+
return { ok: false, error: "Destination credential changed after consent. Resume and confirm again." };
|
|
1751
|
+
}
|
|
1752
|
+
const entry = { ...store[target], BAPI_API_KEY: proof.keySecret };
|
|
1753
|
+
if (existingKey !== proof.keySecret)
|
|
1754
|
+
delete entry.BAPI_EPIC_LAUNCH_BOUND_BASE_URL;
|
|
1755
|
+
const updated = { ...store, [target]: entry };
|
|
1756
|
+
delete updated[scope.locator];
|
|
1757
|
+
delete updated[scope.folderReference];
|
|
1758
|
+
const written = await durablyReplaceCredentialStoreJson(getPrimaryCredentialStorePath(deps), updated, deps);
|
|
1759
|
+
return written.ok ? { ok: true, path: getPrimaryCredentialStorePath(deps) } : { ok: false, error: written.error };
|
|
1760
|
+
}
|
|
1761
|
+
catch {
|
|
1762
|
+
return { ok: false, error: modernRecoveryError };
|
|
1763
|
+
}
|
|
1764
|
+
}, () => ({ ok: false, error: modernRecoveryError }));
|
|
1765
|
+
}
|