import { randomUUID } from "node:crypto"; import { open as openFile, rename as renameFile, rm as removeFile, } from "node:fs/promises"; import path from "node:path"; import type { z } from "zod"; export type ValidatedJsonArtifactInput = Readonly<{ path: string; schema: z.ZodType; value: unknown; }>; export type ValidatedJsonArtifactFileSystem = Readonly<{ open: (path: string, flags: "wx") => Promise<{ writeFile(data: string, encoding: "utf8"): Promise; close(): Promise; }>; rename: (source: string, destination: string) => Promise; rm: (path: string, options: Readonly<{ force: true }>) => Promise; }>; type ValidatedJsonArtifactWriterDependencies = Readonly<{ createNonce?: () => string; fileSystem?: ValidatedJsonArtifactFileSystem; }>; const defaultFileSystem: ValidatedJsonArtifactFileSystem = Object.freeze({ open: async (target, flags) => openFile(target, flags), rename: async (source, destination) => renameFile(source, destination), rm: async (target, options) => removeFile(target, options), }); /** * Builds a writer whose only publish operation is an atomic sibling rename. * Dependency injection is limited to the file-system boundary so failure * ownership can be verified without exposing a caller-selected cleanup path. */ export function createValidatedJsonArtifactWriter( dependencies: ValidatedJsonArtifactWriterDependencies = {}, ) { const createNonce = dependencies.createNonce ?? randomUUID; const fileSystem = dependencies.fileSystem ?? defaultFileSystem; return async function writeArtifact( input: ValidatedJsonArtifactInput, ): Promise { const parsed = input.schema.parse(input.value); const serialized = JSON.stringify(parsed, null, 2); if (serialized === undefined) { throw new TypeError("Validated JSON artifact is not serializable"); } const temporaryPath = path.join( path.dirname(input.path), `.${path.basename(input.path)}.${createNonce()}.tmp`, ); let ownsTemporaryFile = false; try { const handle = await fileSystem.open(temporaryPath, "wx"); ownsTemporaryFile = true; try { await handle.writeFile(`${serialized}\n`, "utf8"); } finally { await handle.close(); } await fileSystem.rename(temporaryPath, input.path); } catch (error) { if (ownsTemporaryFile) { try { await fileSystem.rm(temporaryPath, { force: true }); } catch { // Preserve the publishing failure; cleanup is confined to our nonce. } } throw error; } }; } export const writeValidatedJsonArtifact = createValidatedJsonArtifactWriter();