@ricsam/r5d-worker 0.0.76 → 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,
@@ -636,29 +637,16 @@ const NON_RECURSIVE_GIT_CONFIG_ARGS = [
636
637
  "-c",
637
638
  "push.recurseSubmodules=false"
638
639
  ];
639
- function gitExtraHeaderConfigKey(extraHeaderUrl) {
640
- return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
641
- }
642
- function gitAuthArgs(auth) {
643
- if (!auth) {
644
- return [];
645
- }
646
- const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
647
- return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
648
- }
649
640
  const workerGitCommand = {
650
- commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args]
641
+ commandArgs: (args) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...args]
651
642
  };
652
643
  function runGit(args, options = {}) {
653
- const command = workerGitCommand.commandArgs(args, options.auth);
644
+ const command = workerGitCommand.commandArgs(args);
654
645
  const result = Bun.spawnSync(command, {
655
646
  cwd: options.cwd,
656
647
  stdout: "pipe",
657
648
  stderr: "pipe",
658
- env: {
659
- ...process.env,
660
- GIT_TERMINAL_PROMPT: "0"
661
- }
649
+ env: workerGitProcessEnvironment()
662
650
  });
663
651
  if (result.exitCode !== 0) {
664
652
  throw new Error(
@@ -791,28 +779,92 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
791
779
  function canonicalProjectRemoteUrl(baseUrl, projectId) {
792
780
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
793
781
  }
794
- function gitExtraHeaderUrlForRemote(remoteUrl) {
795
- try {
796
- return new URL(remoteUrl).origin;
797
- } catch {
798
- return remoteUrl;
799
- }
800
- }
801
- function gitAuthForRemote(remoteUrl, authHeader) {
802
- if (!authHeader) {
803
- return void 0;
804
- }
805
- return {
806
- extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
807
- header: authHeader
808
- };
809
- }
810
782
  function shellQuote(value) {
811
783
  return `'${value.replace(/'/g, "'\\''")}'`;
812
784
  }
813
- 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() {
814
792
  return path.join(os.homedir(), ".r5d", "github-credentials");
815
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
+ }
816
868
  function decodeGitAuthHeader(authHeader) {
817
869
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
818
870
  if (match) {
@@ -824,45 +876,84 @@ function decodeGitAuthHeader(authHeader) {
824
876
  const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
825
877
  return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
826
878
  }
827
- function writeCredentialStoreEntry(remoteUrl, authHeader) {
879
+ function credentialUsernameForAuthHeader(authHeader) {
828
880
  const credentials = decodeGitAuthHeader(authHeader);
829
- if (!credentials) {
830
- return null;
831
- }
832
- 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`);
833
887
  try {
834
- remote = new URL(remoteUrl);
835
- } catch {
836
- return null;
837
- }
838
- if (remote.protocol !== "https:") {
839
- 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 });
840
893
  }
841
- const storePath = githubCredentialsPath();
842
- fs.mkdirSync(path.dirname(storePath), { recursive: true });
843
- const hostKey = `${remote.protocol}//${remote.host}`;
844
- const existing = fs.existsSync(storePath) ? fs.readFileSync(storePath, "utf8").split(/\r?\n/).filter(Boolean) : [];
845
- 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;
846
899
  try {
847
- const parsed = new URL(line);
848
- return `${parsed.protocol}//${parsed.host}` !== hostKey;
900
+ remote = new URL(input.remoteUrl);
849
901
  } catch {
850
- return true;
902
+ if (input.authHeader != null) throw new Error("Invalid authenticated Git remote URL");
903
+ continue;
851
904
  }
852
- });
853
- const credentialUrl = new URL(hostKey);
854
- credentialUrl.username = credentials.username;
855
- credentialUrl.password = credentials.password;
856
- next.push(credentialUrl.toString());
857
- fs.writeFileSync(storePath, `${next.join("\n")}
858
- `, { mode: 384 });
859
- fs.chmodSync(storePath, 384);
860
- return storePath;
861
- }
862
- function credentialHelperForRemote(remoteUrl, authHeader) {
863
- const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
864
- return credentialStore ? `store --file=${shellQuote(credentialStore)}` : null;
865
- }
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
+ };
866
957
  function dockerConfigPath() {
867
958
  return path.join(os.homedir(), ".docker", "config.json");
868
959
  }
@@ -2248,6 +2339,7 @@ async function startWorker(options) {
2248
2339
  });
2249
2340
  const config = readConfig(options.configPath);
2250
2341
  const { baseUrl, token } = resolveCredentials(options, config);
2342
+ initializeCredentialStoreDirectoryOnce();
2251
2343
  const label = requireValue(options.label, "Missing required --label <label>");
2252
2344
  validateLabel(label);
2253
2345
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
@@ -2294,6 +2386,8 @@ async function startWorker(options) {
2294
2386
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
2295
2387
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
2296
2388
  let workspaceRemoteUrl = null;
2389
+ let workspaceCredentialHelper = null;
2390
+ let workspaceCredentialUsername = null;
2297
2391
  let workspaceGitIdentity = null;
2298
2392
  let workspaceConfigured = false;
2299
2393
  let activeWorkspaceIncidentId = null;
@@ -2308,6 +2402,7 @@ async function startWorker(options) {
2308
2402
  let reloadAfterClose = false;
2309
2403
  let shutdownAfterClose = false;
2310
2404
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
2405
+ const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
2311
2406
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2312
2407
  const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
2313
2408
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
@@ -2315,12 +2410,32 @@ async function startWorker(options) {
2315
2410
  const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
2316
2411
  const projectConnection = (project) => {
2317
2412
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2318
- const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
2319
2413
  const originUrl = project.repoHttpUrl ?? mirrorUrl;
2320
2414
  const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
2321
- const originAuth = gitAuthForRemote(originUrl, originHeader);
2322
- const credentialHelper = originHeader ? credentialHelperForRemote(originUrl, originHeader) : null;
2323
- 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 };
2324
2439
  };
2325
2440
  const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
2326
2441
  (tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
@@ -2344,7 +2459,8 @@ async function startWorker(options) {
2344
2459
  branchName,
2345
2460
  gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
2346
2461
  mirrorUrl: connection.mirrorUrl,
2347
- mirrorAuth: connection.mirrorAuth,
2462
+ credentialHelper: connection.credentialHelper,
2463
+ credentialUsername: connection.mirrorCredentialUsername,
2348
2464
  sourcePath,
2349
2465
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
2350
2466
  planSourcePath,
@@ -2355,7 +2471,7 @@ async function startWorker(options) {
2355
2471
  };
2356
2472
  const stageProjectDeletion = (project) => {
2357
2473
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2358
- const connection = projectConnection(project);
2474
+ const connection = projectMirrorConnection(project.projectId);
2359
2475
  const branches = project.branches.map(({ branchName }) => ({
2360
2476
  branchName,
2361
2477
  sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
@@ -2376,7 +2492,8 @@ async function startWorker(options) {
2376
2492
  // hidden ref only after that publication succeeds.
2377
2493
  gitDirectory: workspaceShadowRoot,
2378
2494
  mirrorUrl: connection.mirrorUrl,
2379
- mirrorAuth: connection.mirrorAuth,
2495
+ credentialHelper: connection.credentialHelper,
2496
+ credentialUsername: connection.mirrorCredentialUsername,
2380
2497
  sourcePath: branch.sourcePath,
2381
2498
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2382
2499
  planSourcePath: branch.planSourcePath,
@@ -2434,14 +2551,17 @@ async function startWorker(options) {
2434
2551
  for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
2435
2552
  const key = projectBranchKey(deletion.projectId, deletion.branchName);
2436
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);
2437
2556
  fs.rmSync(planSourcePath, { recursive: true, force: true });
2438
2557
  pendingMirrorDeletes.set(key, {
2439
2558
  id: projectMountId(deletion.projectId, deletion.branchName),
2440
2559
  projectId: deletion.projectId,
2441
2560
  branchName: deletion.branchName,
2442
2561
  gitDirectory: workspaceShadowRoot,
2443
- mirrorUrl: canonicalProjectRemoteUrl(baseUrl, deletion.projectId),
2444
- mirrorAuth: gitAuthForRemote(canonicalProjectRemoteUrl(baseUrl, deletion.projectId), bearerAuthHeader),
2562
+ mirrorUrl: connection.mirrorUrl,
2563
+ credentialHelper: connection.credentialHelper,
2564
+ credentialUsername: connection.mirrorCredentialUsername,
2445
2565
  sourcePath: deletion.managedPath,
2446
2566
  workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
2447
2567
  planSourcePath,
@@ -2589,9 +2709,9 @@ async function startWorker(options) {
2589
2709
  primaryBranchName: primaryProjectBranch(project),
2590
2710
  branches: project.branches,
2591
2711
  originUrl: connection.originUrl,
2592
- originAuth: connection.originAuth,
2712
+ originCredentialUsername: connection.originCredentialUsername,
2593
2713
  mirrorUrl: connection.mirrorUrl,
2594
- mirrorAuth: connection.mirrorAuth,
2714
+ mirrorCredentialUsername: connection.mirrorCredentialUsername,
2595
2715
  credentialHelper: connection.credentialHelper,
2596
2716
  gitIdentity: workspaceGitIdentity
2597
2717
  });
@@ -2630,10 +2750,17 @@ async function startWorker(options) {
2630
2750
  if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2631
2751
  const connection = projectConnection(project);
2632
2752
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2633
- if (!tryGit(["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"], {
2634
- cwd: primaryPath,
2635
- auth: connection.originAuth
2636
- })) {
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
+ )) {
2637
2764
  process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
2638
2765
  `);
2639
2766
  }
@@ -2683,7 +2810,8 @@ async function startWorker(options) {
2683
2810
  projectRoot: configuredProjectRoot(projectsRoot, project),
2684
2811
  branchNames: project.branches.map(({ branchName }) => branchName),
2685
2812
  mirrorUrl: connection.mirrorUrl,
2686
- mirrorAuth: connection.mirrorAuth,
2813
+ credentialHelper: connection.credentialHelper,
2814
+ credentialUsername: connection.mirrorCredentialUsername,
2687
2815
  onlyBranches: changed
2688
2816
  });
2689
2817
  for (const result of results) {
@@ -2710,7 +2838,8 @@ async function startWorker(options) {
2710
2838
  gitDirectory: deletion.gitDirectory,
2711
2839
  branchName: deletion.branchName,
2712
2840
  mirrorUrl: deletion.mirrorUrl,
2713
- mirrorAuth: deletion.mirrorAuth
2841
+ credentialHelper: deletion.credentialHelper,
2842
+ credentialUsername: deletion.credentialUsername
2714
2843
  });
2715
2844
  }
2716
2845
  if (deletion.tombstoneId && !deletion.projectDeleted) {
@@ -2720,6 +2849,9 @@ async function startWorker(options) {
2720
2849
  });
2721
2850
  }
2722
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
+ }
2723
2855
  lastObservedProjectHeads.delete(key);
2724
2856
  }
2725
2857
  };
@@ -2733,7 +2865,8 @@ async function startWorker(options) {
2733
2865
  primaryBranchName: primaryProjectBranch(project),
2734
2866
  branchNames: project.branches.map(({ branchName }) => branchName),
2735
2867
  mirrorUrl: connection.mirrorUrl,
2736
- mirrorAuth: connection.mirrorAuth
2868
+ credentialHelper: connection.credentialHelper,
2869
+ credentialUsername: connection.mirrorCredentialUsername
2737
2870
  });
2738
2871
  for (const state of refreshed) {
2739
2872
  lastObservedProjectHeads.set(
@@ -2758,9 +2891,9 @@ async function startWorker(options) {
2758
2891
  primaryBranchName: primaryProjectBranch(project),
2759
2892
  branches: project.branches,
2760
2893
  originUrl: connection.originUrl,
2761
- originAuth: connection.originAuth,
2894
+ originCredentialUsername: connection.originCredentialUsername,
2762
2895
  mirrorUrl: connection.mirrorUrl,
2763
- mirrorAuth: connection.mirrorAuth,
2896
+ mirrorCredentialUsername: connection.mirrorCredentialUsername,
2764
2897
  credentialHelper: connection.credentialHelper,
2765
2898
  gitIdentity: workspaceGitIdentity
2766
2899
  });
@@ -2807,16 +2940,16 @@ async function startWorker(options) {
2807
2940
  error: error instanceof Error ? error.message : String(error)
2808
2941
  });
2809
2942
  const performWorkspaceSync = async (input) => {
2810
- 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
+ }
2811
2946
  const mounts = buildWorkspaceMounts();
2812
- const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
2813
- const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
2814
2947
  if (input.resetToCanonical) {
2815
2948
  const reset = resetWorkspaceGit({
2816
2949
  workspacePath: workspaceShadowRoot,
2817
2950
  remoteUrl: workspaceRemoteUrl,
2818
- remoteAuth,
2819
- credentialHelper,
2951
+ credentialHelper: workspaceCredentialHelper,
2952
+ credentialUsername: workspaceCredentialUsername,
2820
2953
  gitIdentity: workspaceGitIdentity,
2821
2954
  mounts
2822
2955
  });
@@ -2845,8 +2978,8 @@ async function startWorker(options) {
2845
2978
  workerLabel: label,
2846
2979
  workspacePath: workspaceShadowRoot,
2847
2980
  remoteUrl: workspaceRemoteUrl,
2848
- remoteAuth,
2849
- credentialHelper,
2981
+ credentialHelper: workspaceCredentialHelper,
2982
+ credentialUsername: workspaceCredentialUsername,
2850
2983
  gitIdentity: workspaceGitIdentity,
2851
2984
  mounts,
2852
2985
  commitDetail: input.confirmationReason ?? input.trigger.detail,
@@ -2959,13 +3092,47 @@ async function startWorker(options) {
2959
3092
  try {
2960
3093
  return await workspaceSyncSingleFlight.runExclusive(async () => {
2961
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);
2962
3130
  configureGitHubAuth(message.githubCredential);
2963
3131
  visibleGitIdentity = message.gitIdentity;
2964
3132
  workspaceGitIdentity = message.gitIdentity;
2965
3133
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2966
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
2967
- desiredProjects: message.projects
2968
- });
3134
+ workspaceCredentialHelper = nextWorkspaceCredentialHelper;
3135
+ workspaceCredentialUsername = workerCredentialUsername;
2969
3136
  pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, message.projects);
2970
3137
  const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
2971
3138
  ...project,
@@ -2996,13 +3163,11 @@ async function startWorker(options) {
2996
3163
  for (const projectId of [...readyProjectIds]) {
2997
3164
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
2998
3165
  }
2999
- const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
3000
- const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
3001
3166
  ensureWorkspaceGitClone({
3002
3167
  workspacePath: workspaceShadowRoot,
3003
3168
  remoteUrl: message.workspaceRemoteUrl,
3004
- remoteAuth,
3005
- credentialHelper,
3169
+ credentialHelper: nextWorkspaceCredentialHelper,
3170
+ credentialUsername: workerCredentialUsername,
3006
3171
  gitIdentity: message.gitIdentity
3007
3172
  });
3008
3173
  pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
@@ -3801,5 +3966,6 @@ export {
3801
3966
  resolveWorkerFilePath,
3802
3967
  resolveWorkerSessionTarget,
3803
3968
  syncSessionArtifacts,
3969
+ workerGitSecurityTestHarness,
3804
3970
  writeWorkerTextFile
3805
3971
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.76",
3
+ "version": "0.0.77",
4
4
  "type": "module"
5
5
  }