fix: preserve deterministic runtime secret leases
This commit is contained in:
@@ -458,6 +458,7 @@ export async function publishDeterministicRuntimeConfigLease(options: {
|
||||
dataRoot: string;
|
||||
secretRoots: readonly string[];
|
||||
semanticRuntime: SemanticRuntimeConfig;
|
||||
workspaceSecretStore?: WorkspaceSecretStore;
|
||||
}): Promise<DeterministicRuntimeConfigLease> {
|
||||
const rendered = await renderActiveWorkspaceRuntime(options);
|
||||
const publishedConfig = applyCollectionLifecycle(rendered.renderedConfig, "require_existing");
|
||||
@@ -542,7 +543,7 @@ export async function publishDeterministicRuntimeConfigLease(options: {
|
||||
effectiveConfigIdentity: effectiveConfigIdentityValue,
|
||||
configFingerprint: configFingerprintValue,
|
||||
inputFingerprint: inputFingerprintValue,
|
||||
release: () => undefined,
|
||||
release: () => rendered.releaseSecrets(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -605,6 +606,6 @@ export async function publishDeterministicRuntimeConfigLease(options: {
|
||||
effectiveConfigIdentity: effectiveConfigIdentityValue,
|
||||
configFingerprint: configFingerprintValue,
|
||||
inputFingerprint: inputFingerprintValue,
|
||||
release: () => undefined,
|
||||
release: () => rendered.releaseSecrets(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import {
|
||||
fchmodSync,
|
||||
fsyncSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
const STORE_ERROR = "Workspace secret store is unavailable.";
|
||||
@@ -94,6 +93,7 @@ export class WorkspaceSecretStore {
|
||||
private readonly maxSecretBytes: number;
|
||||
private readonly keyPath: string;
|
||||
private readonly vaultPath: string;
|
||||
private readonly materializationReferences = new Map<string, number>();
|
||||
|
||||
constructor(options: WorkspaceSecretStoreOptions) {
|
||||
if (!options.installationId.trim()) throw new Error("Installation identifier is required.");
|
||||
@@ -184,11 +184,37 @@ export class WorkspaceSecretStore {
|
||||
}
|
||||
|
||||
let directory: string | undefined;
|
||||
let retained = false;
|
||||
try {
|
||||
const vault = this.readVault();
|
||||
const key = this.readKey();
|
||||
directory = mkdtempSync(join(this.runtimeRoot, "lease-"));
|
||||
chmodSync(directory, 0o700);
|
||||
const workspaceEntries = requirementIds
|
||||
.map((requirementId) => vault.entries[entryKey(workspaceId, requirementId)])
|
||||
.filter((entry): entry is EncryptedEntry => entry !== undefined)
|
||||
.sort((left, right) => left.requirementId.localeCompare(right.requirementId));
|
||||
const materializationId = createHash("sha256")
|
||||
.update(this.installationId)
|
||||
.update("\0")
|
||||
.update(workspaceId)
|
||||
.update("\0")
|
||||
.update(JSON.stringify(workspaceEntries))
|
||||
.digest("hex")
|
||||
.slice(0, 24);
|
||||
directory = join(
|
||||
this.runtimeRoot,
|
||||
`workspace-${createHash("sha256").update(workspaceId).digest("hex").slice(0, 16)}-${materializationId}`,
|
||||
);
|
||||
const references = this.materializationReferences.get(directory) ?? 0;
|
||||
if (references === 0) {
|
||||
try {
|
||||
mkdirSync(directory, { recursive: false, mode: 0o700 });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
mkdirSync(directory, { recursive: false, mode: 0o700 });
|
||||
}
|
||||
chmodSync(directory, 0o700);
|
||||
}
|
||||
const files = new Map<string, string>();
|
||||
for (const requirementId of [...new Set(requirementIds)]) {
|
||||
const entry = vault.entries[entryKey(workspaceId, requirementId)];
|
||||
@@ -207,18 +233,24 @@ export class WorkspaceSecretStore {
|
||||
decipher.update(Buffer.from(entry.ciphertext, "base64")),
|
||||
decipher.final(),
|
||||
]);
|
||||
const path = join(directory, randomBytes(16).toString("hex"));
|
||||
const fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o400);
|
||||
try {
|
||||
fchmodSync(fd, 0o400);
|
||||
writeFileSync(fd, plaintext);
|
||||
fsyncSync(fd);
|
||||
} finally {
|
||||
const path = join(directory, createHash("sha256").update(requirementId).digest("hex"));
|
||||
if (references === 0) {
|
||||
const fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o400);
|
||||
try {
|
||||
fchmodSync(fd, 0o400);
|
||||
writeFileSync(fd, plaintext);
|
||||
fsyncSync(fd);
|
||||
} finally {
|
||||
plaintext.fill(0);
|
||||
closeSync(fd);
|
||||
}
|
||||
} else {
|
||||
plaintext.fill(0);
|
||||
closeSync(fd);
|
||||
}
|
||||
files.set(requirementId, path);
|
||||
}
|
||||
this.materializationReferences.set(directory, references + 1);
|
||||
retained = true;
|
||||
let released = false;
|
||||
const leasedDirectory = directory;
|
||||
return {
|
||||
@@ -226,11 +258,19 @@ export class WorkspaceSecretStore {
|
||||
release: () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
rmSync(leasedDirectory, { recursive: true, force: true });
|
||||
const remaining = (this.materializationReferences.get(leasedDirectory) ?? 1) - 1;
|
||||
if (remaining > 0) {
|
||||
this.materializationReferences.set(leasedDirectory, remaining);
|
||||
} else {
|
||||
this.materializationReferences.delete(leasedDirectory);
|
||||
rmSync(leasedDirectory, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
if (directory !== undefined) rmSync(directory, { recursive: true, force: true });
|
||||
if (directory !== undefined && !retained && !this.materializationReferences.has(directory)) {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
throw new Error(STORE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user