Copy seeded secrets key into worktree instances
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { copySeededSecretsKey } from "../commands/worktree.js";
|
||||||
import {
|
import {
|
||||||
buildWorktreeConfig,
|
buildWorktreeConfig,
|
||||||
buildWorktreeEnvEntries,
|
buildWorktreeEnvEntries,
|
||||||
@@ -122,4 +125,50 @@ describe("worktree helpers", () => {
|
|||||||
expect(full.excludedTables).toEqual([]);
|
expect(full.excludedTables).toEqual([]);
|
||||||
expect(full.nullifyColumns).toEqual({});
|
expect(full.nullifyColumns).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
||||||
|
try {
|
||||||
|
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||||
|
const sourceKeyPath = path.join(tempRoot, "source", "secrets", "master.key");
|
||||||
|
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
|
||||||
|
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
|
||||||
|
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
|
||||||
|
|
||||||
|
const sourceConfig = buildSourceConfig();
|
||||||
|
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
|
||||||
|
|
||||||
|
copySeededSecretsKey({
|
||||||
|
sourceConfigPath,
|
||||||
|
sourceConfig,
|
||||||
|
sourceEnvEntries: {},
|
||||||
|
targetKeyFilePath: targetKeyPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("source-master-key");
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes the source inline secrets master key into the seeded worktree instance", () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
||||||
|
try {
|
||||||
|
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||||
|
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
|
||||||
|
|
||||||
|
copySeededSecretsKey({
|
||||||
|
sourceConfigPath,
|
||||||
|
sourceConfig: buildSourceConfig(),
|
||||||
|
sourceEnvEntries: {
|
||||||
|
PAPERCLIP_SECRETS_MASTER_KEY: "inline-source-master-key",
|
||||||
|
},
|
||||||
|
targetKeyFilePath: targetKeyPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("inline-source-master-key");
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, readFileSync, rmSync } from "node:fs";
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
@@ -18,6 +18,7 @@ import { expandHomePrefix } from "../config/home.js";
|
|||||||
import type { PaperclipConfig } from "../config/schema.js";
|
import type { PaperclipConfig } from "../config/schema.js";
|
||||||
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
||||||
import { printPaperclipCliBanner } from "../utils/banner.js";
|
import { printPaperclipCliBanner } from "../utils/banner.js";
|
||||||
|
import { resolveRuntimeLikePath } from "../utils/path-resolver.js";
|
||||||
import {
|
import {
|
||||||
buildWorktreeConfig,
|
buildWorktreeConfig,
|
||||||
buildWorktreeEnvEntries,
|
buildWorktreeEnvEntries,
|
||||||
@@ -154,6 +155,54 @@ function resolveSourceConnectionString(config: PaperclipConfig, envEntries: Reco
|
|||||||
return `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
return `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copySeededSecretsKey(input: {
|
||||||
|
sourceConfigPath: string;
|
||||||
|
sourceConfig: PaperclipConfig;
|
||||||
|
sourceEnvEntries: Record<string, string>;
|
||||||
|
targetKeyFilePath: string;
|
||||||
|
}): void {
|
||||||
|
if (input.sourceConfig.secrets.provider !== "local_encrypted") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(path.dirname(input.targetKeyFilePath), { recursive: true });
|
||||||
|
|
||||||
|
const sourceInlineMasterKey =
|
||||||
|
nonEmpty(input.sourceEnvEntries.PAPERCLIP_SECRETS_MASTER_KEY) ??
|
||||||
|
nonEmpty(process.env.PAPERCLIP_SECRETS_MASTER_KEY);
|
||||||
|
if (sourceInlineMasterKey) {
|
||||||
|
writeFileSync(input.targetKeyFilePath, sourceInlineMasterKey, {
|
||||||
|
encoding: "utf8",
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
chmodSync(input.targetKeyFilePath, 0o600);
|
||||||
|
} catch {
|
||||||
|
// best effort
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceKeyFileOverride =
|
||||||
|
nonEmpty(input.sourceEnvEntries.PAPERCLIP_SECRETS_MASTER_KEY_FILE) ??
|
||||||
|
nonEmpty(process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE);
|
||||||
|
const sourceConfiguredKeyPath = sourceKeyFileOverride ?? input.sourceConfig.secrets.localEncrypted.keyFilePath;
|
||||||
|
const sourceKeyFilePath = resolveRuntimeLikePath(sourceConfiguredKeyPath, input.sourceConfigPath);
|
||||||
|
|
||||||
|
if (!existsSync(sourceKeyFilePath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot seed worktree database because source local_encrypted secrets key was not found at ${sourceKeyFilePath}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyFileSync(sourceKeyFilePath, input.targetKeyFilePath);
|
||||||
|
try {
|
||||||
|
chmodSync(input.targetKeyFilePath, 0o600);
|
||||||
|
} catch {
|
||||||
|
// best effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): Promise<EmbeddedPostgresHandle> {
|
async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): Promise<EmbeddedPostgresHandle> {
|
||||||
const moduleName = "embedded-postgres";
|
const moduleName = "embedded-postgres";
|
||||||
let EmbeddedPostgres: EmbeddedPostgresCtor;
|
let EmbeddedPostgres: EmbeddedPostgresCtor;
|
||||||
@@ -215,6 +264,12 @@ async function seedWorktreeDatabase(input: {
|
|||||||
const seedPlan = resolveWorktreeSeedPlan(input.seedMode);
|
const seedPlan = resolveWorktreeSeedPlan(input.seedMode);
|
||||||
const sourceEnvFile = resolvePaperclipEnvFile(input.sourceConfigPath);
|
const sourceEnvFile = resolvePaperclipEnvFile(input.sourceConfigPath);
|
||||||
const sourceEnvEntries = readPaperclipEnvEntries(sourceEnvFile);
|
const sourceEnvEntries = readPaperclipEnvEntries(sourceEnvFile);
|
||||||
|
copySeededSecretsKey({
|
||||||
|
sourceConfigPath: input.sourceConfigPath,
|
||||||
|
sourceConfig: input.sourceConfig,
|
||||||
|
sourceEnvEntries,
|
||||||
|
targetKeyFilePath: input.targetPaths.secretsKeyFilePath,
|
||||||
|
});
|
||||||
let sourceHandle: EmbeddedPostgresHandle | null = null;
|
let sourceHandle: EmbeddedPostgresHandle | null = null;
|
||||||
let targetHandle: EmbeddedPostgresHandle | null = null;
|
let targetHandle: EmbeddedPostgresHandle | null = null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user