@ricsam/r5d-worker 0.0.75 → 0.0.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mjs/main.mjs CHANGED
@@ -5,6 +5,7 @@ import { createHash } from "node:crypto";
5
5
  import os, { hostname } from "node:os";
6
6
  import { spawn as spawnChildProcess } from "node:child_process";
7
7
  import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
8
+ import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
8
9
  import {
9
10
  grantWorkerHeartbeatBusyGrace,
10
11
  hasWorkerHeartbeatTimedOutWithBusyGrace,
@@ -26,6 +27,7 @@ import {
26
27
  } from "./supervisor.mjs";
27
28
  import { managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
28
29
  import {
30
+ cleanupStaleProjectWorktreeSnapshots,
29
31
  createLinkedProjectBranch,
30
32
  deleteLinkedProjectBranch,
31
33
  deleteProjectMirrorBranch,
@@ -635,29 +637,16 @@ const NON_RECURSIVE_GIT_CONFIG_ARGS = [
635
637
  "-c",
636
638
  "push.recurseSubmodules=false"
637
639
  ];
638
- function gitExtraHeaderConfigKey(extraHeaderUrl) {
639
- return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
640
- }
641
- function gitAuthArgs(auth) {
642
- if (!auth) {
643
- return [];
644
- }
645
- const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
646
- return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
647
- }
648
640
  const workerGitCommand = {
649
- commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args]
641
+ commandArgs: (args) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...args]
650
642
  };
651
643
  function runGit(args, options = {}) {
652
- const command = workerGitCommand.commandArgs(args, options.auth);
644
+ const command = workerGitCommand.commandArgs(args);
653
645
  const result = Bun.spawnSync(command, {
654
646
  cwd: options.cwd,
655
647
  stdout: "pipe",
656
648
  stderr: "pipe",
657
- env: {
658
- ...process.env,
659
- GIT_TERMINAL_PROMPT: "0"
660
- }
649
+ env: workerGitProcessEnvironment()
661
650
  });
662
651
  if (result.exitCode !== 0) {
663
652
  throw new Error(
@@ -790,28 +779,92 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
790
779
  function canonicalProjectRemoteUrl(baseUrl, projectId) {
791
780
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
792
781
  }
793
- function gitExtraHeaderUrlForRemote(remoteUrl) {
794
- try {
795
- return new URL(remoteUrl).origin;
796
- } catch {
797
- return remoteUrl;
798
- }
799
- }
800
- function gitAuthForRemote(remoteUrl, authHeader) {
801
- if (!authHeader) {
802
- return void 0;
803
- }
804
- return {
805
- extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
806
- header: authHeader
807
- };
808
- }
809
782
  function shellQuote(value) {
810
783
  return `'${value.replace(/'/g, "'\\''")}'`;
811
784
  }
812
- function githubCredentialsPath() {
785
+ function readOnlyCredentialStoreHelper(storePath) {
786
+ return `!f() { if [ "$1" = get ]; then git credential-store --file=${shellQuote(storePath)} get; else return 0; fi; }; f`;
787
+ }
788
+ function credentialStoreDirectory() {
789
+ return path.join(os.homedir(), ".r5d", "git-credentials");
790
+ }
791
+ function legacySharedCredentialStorePath() {
813
792
  return path.join(os.homedir(), ".r5d", "github-credentials");
814
793
  }
794
+ function credentialStorePathForProject(projectId) {
795
+ validateProjectId(projectId);
796
+ const digest = createHash("sha256").update(`project\0${projectId}`).digest("hex");
797
+ return path.join(credentialStoreDirectory(), `project-${digest}.store`);
798
+ }
799
+ function credentialStorePathForWorkspace(remoteUrl) {
800
+ const digest = createHash("sha256").update(`workspace\0${remoteUrl}`).digest("hex");
801
+ return path.join(credentialStoreDirectory(), `workspace-${digest}.store`);
802
+ }
803
+ function lstatIfExists(filePath) {
804
+ try {
805
+ return fs.lstatSync(filePath);
806
+ } catch (error) {
807
+ if (error.code === "ENOENT") return null;
808
+ throw error;
809
+ }
810
+ }
811
+ function ensurePrivateCredentialStoreDirectory(directoryPath) {
812
+ const stat = lstatIfExists(directoryPath);
813
+ if (stat) {
814
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Git credential-store path must be a private directory");
815
+ } else {
816
+ fs.mkdirSync(directoryPath, { recursive: true, mode: 448 });
817
+ }
818
+ fs.chmodSync(directoryPath, 448);
819
+ }
820
+ function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
821
+ fs.rmSync(storePath, { force: true });
822
+ }
823
+ let credentialStoreDirectoryInitialized = false;
824
+ function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
825
+ const stat = lstatIfExists(directoryPath);
826
+ if (stat) {
827
+ if (stat.isSymbolicLink() || !stat.isDirectory()) fs.unlinkSync(directoryPath);
828
+ else fs.rmSync(directoryPath, { recursive: true, force: true });
829
+ }
830
+ ensurePrivateCredentialStoreDirectory(directoryPath);
831
+ }
832
+ function initializeCredentialStoreDirectoryOnce() {
833
+ if (credentialStoreDirectoryInitialized) return;
834
+ removeLegacySharedCredentialStore();
835
+ resetCredentialStoreDirectory();
836
+ credentialStoreDirectoryInitialized = true;
837
+ }
838
+ const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
839
+ function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
840
+ ensurePrivateCredentialStoreDirectory(directoryPath);
841
+ const resolvedDirectory = path.resolve(directoryPath);
842
+ const requiredNames = /* @__PURE__ */ new Set();
843
+ for (const storePath of requiredStorePaths) {
844
+ const resolvedStorePath = path.resolve(storePath);
845
+ if (path.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(path.basename(resolvedStorePath))) {
846
+ throw new Error("Invalid app-owned Git credential-store path");
847
+ }
848
+ requiredNames.add(path.basename(resolvedStorePath));
849
+ }
850
+ const removed = [];
851
+ for (const name of fs.readdirSync(directoryPath)) {
852
+ if (!APP_CREDENTIAL_STORE_FILE.test(name) || requiredNames.has(name)) continue;
853
+ const storePath = path.join(directoryPath, name);
854
+ const stat = fs.lstatSync(storePath);
855
+ if (!stat.isFile() && !stat.isSymbolicLink()) continue;
856
+ fs.unlinkSync(storePath);
857
+ removed.push(storePath);
858
+ }
859
+ return removed;
860
+ }
861
+ function removeProjectCredentialStore(projectId) {
862
+ const storePath = credentialStorePathForProject(projectId);
863
+ const stat = lstatIfExists(storePath);
864
+ if (!stat) return;
865
+ if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
866
+ fs.unlinkSync(storePath);
867
+ }
815
868
  function decodeGitAuthHeader(authHeader) {
816
869
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
817
870
  if (match) {
@@ -823,45 +876,84 @@ function decodeGitAuthHeader(authHeader) {
823
876
  const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
824
877
  return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
825
878
  }
826
- function writeCredentialStoreEntry(remoteUrl, authHeader) {
879
+ function credentialUsernameForAuthHeader(authHeader) {
827
880
  const credentials = decodeGitAuthHeader(authHeader);
828
- if (!credentials) {
829
- return null;
830
- }
831
- let remote;
881
+ if (!credentials) throw new Error("Unsupported Git authorization header");
882
+ if (/[\0\r\n]/.test(credentials.username)) throw new Error("Invalid Git credential username");
883
+ return credentials.username;
884
+ }
885
+ function replaceCredentialStoreFile(storePath, content) {
886
+ const temporaryPath = path.join(path.dirname(storePath), `.${path.basename(storePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
832
887
  try {
833
- remote = new URL(remoteUrl);
834
- } catch {
835
- return null;
836
- }
837
- if (remote.protocol !== "https:") {
838
- return null;
888
+ fs.writeFileSync(temporaryPath, content, { flag: "wx", mode: 384 });
889
+ fs.chmodSync(temporaryPath, 384);
890
+ fs.renameSync(temporaryPath, storePath);
891
+ } finally {
892
+ fs.rmSync(temporaryPath, { force: true });
839
893
  }
840
- const storePath = githubCredentialsPath();
841
- fs.mkdirSync(path.dirname(storePath), { recursive: true });
842
- const hostKey = `${remote.protocol}//${remote.host}`;
843
- const existing = fs.existsSync(storePath) ? fs.readFileSync(storePath, "utf8").split(/\r?\n/).filter(Boolean) : [];
844
- const next = existing.filter((line) => {
894
+ }
895
+ function writeCredentialStoreEntries(remotes, storePath) {
896
+ const authoritativeEntries = /* @__PURE__ */ new Map();
897
+ for (const input of remotes) {
898
+ let remote;
845
899
  try {
846
- const parsed = new URL(line);
847
- return `${parsed.protocol}//${parsed.host}` !== hostKey;
900
+ remote = new URL(input.remoteUrl);
848
901
  } catch {
849
- return true;
902
+ if (input.authHeader != null) throw new Error("Invalid authenticated Git remote URL");
903
+ continue;
850
904
  }
851
- });
852
- const credentialUrl = new URL(hostKey);
853
- credentialUrl.username = credentials.username;
854
- credentialUrl.password = credentials.password;
855
- next.push(credentialUrl.toString());
856
- fs.writeFileSync(storePath, `${next.join("\n")}
857
- `, { mode: 384 });
858
- fs.chmodSync(storePath, 384);
859
- return storePath;
860
- }
861
- function credentialHelperForRemote(remoteUrl, authHeader) {
862
- const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
863
- return credentialStore ? `store --file=${shellQuote(credentialStore)}` : null;
864
- }
905
+ if ((remote.protocol === "https:" || remote.protocol === "http:") && (remote.username || remote.password)) {
906
+ throw new Error("Git credentials must not be embedded in a remote URL");
907
+ }
908
+ if (remote.protocol !== "https:" && remote.protocol !== "http:") {
909
+ if (input.authHeader != null) throw new Error("Unsupported authenticated Git remote protocol");
910
+ continue;
911
+ }
912
+ const hostKey = `${remote.protocol}//${remote.host}`;
913
+ let replacement = null;
914
+ if (input.authHeader != null) {
915
+ const credentials = decodeGitAuthHeader(input.authHeader);
916
+ if (!credentials) throw new Error("Unsupported Git authorization header");
917
+ const credentialUrl = new URL(hostKey);
918
+ credentialUrl.username = credentials.username;
919
+ credentialUrl.password = credentials.password;
920
+ replacement = credentialUrl.toString();
921
+ }
922
+ if (authoritativeEntries.has(hostKey) && authoritativeEntries.get(hostKey) !== replacement) {
923
+ throw new Error("Conflicting Git credentials for one transport host");
924
+ }
925
+ authoritativeEntries.set(hostKey, replacement);
926
+ }
927
+ const storeStat = lstatIfExists(storePath);
928
+ if (storeStat) {
929
+ if (storeStat.isSymbolicLink() || !storeStat.isFile()) throw new Error("Git credential store must be a regular file");
930
+ }
931
+ const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
932
+ if (replacements.length === 0) {
933
+ if (storeStat) fs.unlinkSync(storePath);
934
+ return null;
935
+ }
936
+ ensurePrivateCredentialStoreDirectory(path.dirname(storePath));
937
+ replaceCredentialStoreFile(storePath, `${replacements.join("\n")}
938
+ `);
939
+ return { hasCredentials: true, storePath };
940
+ }
941
+ function credentialHelperForRemotes(remotes, storePath) {
942
+ const credentialStore = writeCredentialStoreEntries(remotes, storePath);
943
+ return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
944
+ }
945
+ const workerGitSecurityTestHarness = {
946
+ commandArgs: workerGitCommand.commandArgs,
947
+ credentialHelperForRemotes,
948
+ credentialStorePathForProject,
949
+ credentialStorePathForWorkspace,
950
+ legacySharedCredentialStorePath,
951
+ removeLegacySharedCredentialStore,
952
+ pruneCredentialStoreFiles,
953
+ readOnlyCredentialStoreHelper,
954
+ resetCredentialStoreDirectory,
955
+ replaceCredentialStoreFile
956
+ };
865
957
  function dockerConfigPath() {
866
958
  return path.join(os.homedir(), ".docker", "config.json");
867
959
  }
@@ -2247,6 +2339,7 @@ async function startWorker(options) {
2247
2339
  });
2248
2340
  const config = readConfig(options.configPath);
2249
2341
  const { baseUrl, token } = resolveCredentials(options, config);
2342
+ initializeCredentialStoreDirectoryOnce();
2250
2343
  const label = requireValue(options.label, "Missing required --label <label>");
2251
2344
  validateLabel(label);
2252
2345
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
@@ -2266,6 +2359,15 @@ async function startWorker(options) {
2266
2359
  `);
2267
2360
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
2268
2361
  `);
2362
+ const snapshotCleanup = cleanupStaleProjectWorktreeSnapshots();
2363
+ if (snapshotCleanup.removed.length > 0) {
2364
+ process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
2365
+ `);
2366
+ }
2367
+ for (const failure of snapshotCleanup.failed) {
2368
+ process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
2369
+ `);
2370
+ }
2269
2371
  runGit(["--version"]);
2270
2372
  await verifyR5dctlAuth(baseUrl, token);
2271
2373
  fs.mkdirSync(projectsRoot, { recursive: true });
@@ -2284,6 +2386,8 @@ async function startWorker(options) {
2284
2386
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
2285
2387
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
2286
2388
  let workspaceRemoteUrl = null;
2389
+ let workspaceCredentialHelper = null;
2390
+ let workspaceCredentialUsername = null;
2287
2391
  let workspaceGitIdentity = null;
2288
2392
  let workspaceConfigured = false;
2289
2393
  let activeWorkspaceIncidentId = null;
@@ -2298,6 +2402,7 @@ async function startWorker(options) {
2298
2402
  let reloadAfterClose = false;
2299
2403
  let shutdownAfterClose = false;
2300
2404
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
2405
+ const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
2301
2406
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2302
2407
  const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
2303
2408
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
@@ -2305,12 +2410,32 @@ async function startWorker(options) {
2305
2410
  const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
2306
2411
  const projectConnection = (project) => {
2307
2412
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2308
- const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
2309
2413
  const originUrl = project.repoHttpUrl ?? mirrorUrl;
2310
2414
  const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
2311
- const originAuth = gitAuthForRemote(originUrl, originHeader);
2312
- const credentialHelper = originHeader ? credentialHelperForRemote(originUrl, originHeader) : null;
2313
- return { mirrorUrl, mirrorAuth, originUrl, originAuth, credentialHelper };
2415
+ const credentialHelper = credentialHelperForRemotes(
2416
+ [
2417
+ { remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
2418
+ { remoteUrl: originUrl, authHeader: originHeader }
2419
+ ],
2420
+ credentialStorePathForProject(project.projectId)
2421
+ );
2422
+ if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2423
+ return {
2424
+ mirrorUrl,
2425
+ mirrorCredentialUsername: workerCredentialUsername,
2426
+ originUrl,
2427
+ originCredentialUsername: originHeader ? credentialUsernameForAuthHeader(originHeader) : null,
2428
+ credentialHelper
2429
+ };
2430
+ };
2431
+ const projectMirrorConnection = (projectId) => {
2432
+ const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
2433
+ const credentialHelper = credentialHelperForRemotes(
2434
+ [{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
2435
+ credentialStorePathForProject(projectId)
2436
+ );
2437
+ if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2438
+ return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
2314
2439
  };
2315
2440
  const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
2316
2441
  (tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
@@ -2334,7 +2459,8 @@ async function startWorker(options) {
2334
2459
  branchName,
2335
2460
  gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
2336
2461
  mirrorUrl: connection.mirrorUrl,
2337
- mirrorAuth: connection.mirrorAuth,
2462
+ credentialHelper: connection.credentialHelper,
2463
+ credentialUsername: connection.mirrorCredentialUsername,
2338
2464
  sourcePath,
2339
2465
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
2340
2466
  planSourcePath,
@@ -2345,7 +2471,7 @@ async function startWorker(options) {
2345
2471
  };
2346
2472
  const stageProjectDeletion = (project) => {
2347
2473
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2348
- const connection = projectConnection(project);
2474
+ const connection = projectMirrorConnection(project.projectId);
2349
2475
  const branches = project.branches.map(({ branchName }) => ({
2350
2476
  branchName,
2351
2477
  sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
@@ -2366,7 +2492,8 @@ async function startWorker(options) {
2366
2492
  // hidden ref only after that publication succeeds.
2367
2493
  gitDirectory: workspaceShadowRoot,
2368
2494
  mirrorUrl: connection.mirrorUrl,
2369
- mirrorAuth: connection.mirrorAuth,
2495
+ credentialHelper: connection.credentialHelper,
2496
+ credentialUsername: connection.mirrorCredentialUsername,
2370
2497
  sourcePath: branch.sourcePath,
2371
2498
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2372
2499
  planSourcePath: branch.planSourcePath,
@@ -2424,14 +2551,17 @@ async function startWorker(options) {
2424
2551
  for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
2425
2552
  const key = projectBranchKey(deletion.projectId, deletion.branchName);
2426
2553
  const planSourcePath = path.join(planRoot, deletion.projectId, ...deletion.branchName.split("/"));
2554
+ const liveProject = projectConfigById.get(deletion.projectId);
2555
+ const connection = liveProject ? projectConnection(liveProject) : projectMirrorConnection(deletion.projectId);
2427
2556
  fs.rmSync(planSourcePath, { recursive: true, force: true });
2428
2557
  pendingMirrorDeletes.set(key, {
2429
2558
  id: projectMountId(deletion.projectId, deletion.branchName),
2430
2559
  projectId: deletion.projectId,
2431
2560
  branchName: deletion.branchName,
2432
2561
  gitDirectory: workspaceShadowRoot,
2433
- mirrorUrl: canonicalProjectRemoteUrl(baseUrl, deletion.projectId),
2434
- mirrorAuth: gitAuthForRemote(canonicalProjectRemoteUrl(baseUrl, deletion.projectId), bearerAuthHeader),
2562
+ mirrorUrl: connection.mirrorUrl,
2563
+ credentialHelper: connection.credentialHelper,
2564
+ credentialUsername: connection.mirrorCredentialUsername,
2435
2565
  sourcePath: deletion.managedPath,
2436
2566
  workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
2437
2567
  planSourcePath,
@@ -2579,9 +2709,9 @@ async function startWorker(options) {
2579
2709
  primaryBranchName: primaryProjectBranch(project),
2580
2710
  branches: project.branches,
2581
2711
  originUrl: connection.originUrl,
2582
- originAuth: connection.originAuth,
2712
+ originCredentialUsername: connection.originCredentialUsername,
2583
2713
  mirrorUrl: connection.mirrorUrl,
2584
- mirrorAuth: connection.mirrorAuth,
2714
+ mirrorCredentialUsername: connection.mirrorCredentialUsername,
2585
2715
  credentialHelper: connection.credentialHelper,
2586
2716
  gitIdentity: workspaceGitIdentity
2587
2717
  });
@@ -2620,10 +2750,17 @@ async function startWorker(options) {
2620
2750
  if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2621
2751
  const connection = projectConnection(project);
2622
2752
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2623
- if (!tryGit(["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"], {
2624
- cwd: primaryPath,
2625
- auth: connection.originAuth
2626
- })) {
2753
+ if (!tryGit(
2754
+ [
2755
+ ...gitTransportSecurityArgs(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
2756
+ "fetch",
2757
+ "--no-recurse-submodules",
2758
+ "--prune",
2759
+ "origin",
2760
+ "+refs/heads/*:refs/remotes/origin/*"
2761
+ ],
2762
+ { cwd: primaryPath }
2763
+ )) {
2627
2764
  process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
2628
2765
  `);
2629
2766
  }
@@ -2673,7 +2810,8 @@ async function startWorker(options) {
2673
2810
  projectRoot: configuredProjectRoot(projectsRoot, project),
2674
2811
  branchNames: project.branches.map(({ branchName }) => branchName),
2675
2812
  mirrorUrl: connection.mirrorUrl,
2676
- mirrorAuth: connection.mirrorAuth,
2813
+ credentialHelper: connection.credentialHelper,
2814
+ credentialUsername: connection.mirrorCredentialUsername,
2677
2815
  onlyBranches: changed
2678
2816
  });
2679
2817
  for (const result of results) {
@@ -2700,7 +2838,8 @@ async function startWorker(options) {
2700
2838
  gitDirectory: deletion.gitDirectory,
2701
2839
  branchName: deletion.branchName,
2702
2840
  mirrorUrl: deletion.mirrorUrl,
2703
- mirrorAuth: deletion.mirrorAuth
2841
+ credentialHelper: deletion.credentialHelper,
2842
+ credentialUsername: deletion.credentialUsername
2704
2843
  });
2705
2844
  }
2706
2845
  if (deletion.tombstoneId && !deletion.projectDeleted) {
@@ -2710,6 +2849,9 @@ async function startWorker(options) {
2710
2849
  });
2711
2850
  }
2712
2851
  pendingMirrorDeletes.delete(key);
2852
+ if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
2853
+ removeProjectCredentialStore(deletion.projectId);
2854
+ }
2713
2855
  lastObservedProjectHeads.delete(key);
2714
2856
  }
2715
2857
  };
@@ -2723,7 +2865,8 @@ async function startWorker(options) {
2723
2865
  primaryBranchName: primaryProjectBranch(project),
2724
2866
  branchNames: project.branches.map(({ branchName }) => branchName),
2725
2867
  mirrorUrl: connection.mirrorUrl,
2726
- mirrorAuth: connection.mirrorAuth
2868
+ credentialHelper: connection.credentialHelper,
2869
+ credentialUsername: connection.mirrorCredentialUsername
2727
2870
  });
2728
2871
  for (const state of refreshed) {
2729
2872
  lastObservedProjectHeads.set(
@@ -2748,9 +2891,9 @@ async function startWorker(options) {
2748
2891
  primaryBranchName: primaryProjectBranch(project),
2749
2892
  branches: project.branches,
2750
2893
  originUrl: connection.originUrl,
2751
- originAuth: connection.originAuth,
2894
+ originCredentialUsername: connection.originCredentialUsername,
2752
2895
  mirrorUrl: connection.mirrorUrl,
2753
- mirrorAuth: connection.mirrorAuth,
2896
+ mirrorCredentialUsername: connection.mirrorCredentialUsername,
2754
2897
  credentialHelper: connection.credentialHelper,
2755
2898
  gitIdentity: workspaceGitIdentity
2756
2899
  });
@@ -2797,16 +2940,16 @@ async function startWorker(options) {
2797
2940
  error: error instanceof Error ? error.message : String(error)
2798
2941
  });
2799
2942
  const performWorkspaceSync = async (input) => {
2800
- if (!workspaceRemoteUrl || !workspaceGitIdentity) throw new Error("Worker workspace configuration has not been received");
2943
+ if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
2944
+ throw new Error("Worker workspace configuration has not been received");
2945
+ }
2801
2946
  const mounts = buildWorkspaceMounts();
2802
- const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
2803
- const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
2804
2947
  if (input.resetToCanonical) {
2805
2948
  const reset = resetWorkspaceGit({
2806
2949
  workspacePath: workspaceShadowRoot,
2807
2950
  remoteUrl: workspaceRemoteUrl,
2808
- remoteAuth,
2809
- credentialHelper,
2951
+ credentialHelper: workspaceCredentialHelper,
2952
+ credentialUsername: workspaceCredentialUsername,
2810
2953
  gitIdentity: workspaceGitIdentity,
2811
2954
  mounts
2812
2955
  });
@@ -2835,8 +2978,8 @@ async function startWorker(options) {
2835
2978
  workerLabel: label,
2836
2979
  workspacePath: workspaceShadowRoot,
2837
2980
  remoteUrl: workspaceRemoteUrl,
2838
- remoteAuth,
2839
- credentialHelper,
2981
+ credentialHelper: workspaceCredentialHelper,
2982
+ credentialUsername: workspaceCredentialUsername,
2840
2983
  gitIdentity: workspaceGitIdentity,
2841
2984
  mounts,
2842
2985
  commitDetail: input.confirmationReason ?? input.trigger.detail,
@@ -2949,13 +3092,47 @@ async function startWorker(options) {
2949
3092
  try {
2950
3093
  return await workspaceSyncSingleFlight.runExclusive(async () => {
2951
3094
  workspaceConfigured = false;
3095
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
3096
+ desiredProjects: message.projects
3097
+ });
3098
+ const requiredCredentialStorePaths = /* @__PURE__ */ new Set();
3099
+ const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
3100
+ requiredCredentialStorePaths.add(workspaceStorePath);
3101
+ const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
3102
+ [{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
3103
+ workspaceStorePath
3104
+ );
3105
+ if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
3106
+ const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
3107
+ for (const project of message.projects) {
3108
+ requiredCredentialStorePaths.add(credentialStorePathForProject(project.projectId));
3109
+ projectConnection(project);
3110
+ }
3111
+ const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
3112
+ ...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
3113
+ ...projectWorkspaceState.pendingTreeDeletions.map(({ projectId }) => projectId),
3114
+ ...projectWorkspaceState.pendingMirrorRefDeletions.map(({ projectId }) => projectId)
3115
+ ]);
3116
+ for (const projectId of pendingCredentialProjectIds) {
3117
+ const desiredProject = desiredProjectConfigById.get(projectId);
3118
+ if (desiredProject) projectConnection(desiredProject);
3119
+ else projectMirrorConnection(projectId);
3120
+ requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
3121
+ }
3122
+ for (const deletion of pendingMirrorDeletes.values()) {
3123
+ const desiredProject = desiredProjectConfigById.get(deletion.projectId);
3124
+ const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
3125
+ deletion.mirrorUrl = connection.mirrorUrl;
3126
+ deletion.credentialHelper = connection.credentialHelper;
3127
+ deletion.credentialUsername = connection.mirrorCredentialUsername;
3128
+ }
3129
+ pruneCredentialStoreFiles(requiredCredentialStorePaths);
2952
3130
  configureGitHubAuth(message.githubCredential);
2953
3131
  visibleGitIdentity = message.gitIdentity;
2954
3132
  workspaceGitIdentity = message.gitIdentity;
2955
3133
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2956
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
2957
- desiredProjects: message.projects
2958
- });
3134
+ workspaceCredentialHelper = nextWorkspaceCredentialHelper;
3135
+ workspaceCredentialUsername = workerCredentialUsername;
2959
3136
  pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, message.projects);
2960
3137
  const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
2961
3138
  ...project,
@@ -2986,13 +3163,11 @@ async function startWorker(options) {
2986
3163
  for (const projectId of [...readyProjectIds]) {
2987
3164
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
2988
3165
  }
2989
- const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
2990
- const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
2991
3166
  ensureWorkspaceGitClone({
2992
3167
  workspacePath: workspaceShadowRoot,
2993
3168
  remoteUrl: message.workspaceRemoteUrl,
2994
- remoteAuth,
2995
- credentialHelper,
3169
+ credentialHelper: nextWorkspaceCredentialHelper,
3170
+ credentialUsername: workerCredentialUsername,
2996
3171
  gitIdentity: message.gitIdentity
2997
3172
  });
2998
3173
  pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
@@ -3791,5 +3966,6 @@ export {
3791
3966
  resolveWorkerFilePath,
3792
3967
  resolveWorkerSessionTarget,
3793
3968
  syncSessionArtifacts,
3969
+ workerGitSecurityTestHarness,
3794
3970
  writeWorkerTextFile
3795
3971
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
4
4
  "type": "module"
5
5
  }