import { existsSync } from 'node:fs'; import { mkdir, readFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { writeTextFileAtomic } from './atomic-write.js'; import { withCinemaFileLock } from './cinema-file-lock.js'; import { cinemaArtifactsDirFor } from './cinema-store.js'; import { stableCinemaJson } from './cinema-shot-compiler.js'; import { cinemaArtifactContentHash, validateCinemaCapabilitySnapshot, validateCinemaGenerationAuthorization, validateCinemaGenerationQuote, } from './cinema-provider-contracts.js'; import { assertCinemaProjectionQuoteParity, type CinemaProviderProjection } from './cinema-provider-projection.js'; import type { CinemaGenerationAuthorizationArtifact, CinemaGenerationQuoteArtifact, CinemaProviderCapabilitySnapshotArtifact, CinemaProviderContractValidation, } from './cinema-provider-types.js'; import type { CinemaSha256 } from './cinema-types.js'; export interface CinemaProviderProjectionBundleArtifact { schemaVersion: 1; bundleId: string; quoteId: string; projectSlug: string; routeId: string; capabilitySnapshotId: string; capabilitySnapshotHash: CinemaSha256; createdAt: string; projections: CinemaProviderProjection[]; contentHash: CinemaSha256; } function safe(value: string, label: string): string { if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) throw new Error(`${label} must be a path-safe identifier`); return value; } function contractsDir(root: string, projectSlug: string): string { return join(cinemaArtifactsDirFor(root, projectSlug), 'provider-contracts'); } export function cinemaCapabilitySnapshotPathFor(root: string, projectSlug: string, snapshotId: string): string { return join(contractsDir(root, projectSlug), 'capabilities', `${safe(snapshotId, 'snapshotId')}.json`); } export function cinemaGenerationQuotePathFor(root: string, projectSlug: string, quoteId: string): string { return join(contractsDir(root, projectSlug), 'quotes', `${safe(quoteId, 'quoteId')}.json`); } export function cinemaProviderProjectionBundlePathFor(root: string, projectSlug: string, quoteId: string): string { return join(contractsDir(root, projectSlug), 'projections', `${safe(quoteId, 'quoteId')}.json`); } export function cinemaGenerationAuthorizationPathFor(root: string, projectSlug: string, authorizationId: string): string { return join(contractsDir(root, projectSlug), 'authorizations', `${safe(authorizationId, 'authorizationId')}.json`); } function assertValid(label: string, validation: CinemaProviderContractValidation): void { if (validation.ok) return; throw new Error(`${label} failed validation: ${validation.issues.slice(0, 8).map((entry) => `${entry.code} (${entry.path})`).join(', ')}`); } async function readJson(path: string, label: string): Promise { if (!existsSync(path)) return null; try { return JSON.parse(await readFile(path, 'utf8')) as T; } catch (error) { throw new Error(`${label} is not valid JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`); } } async function writeImmutable(path: string, value: T, label: string): Promise { await mkdir(dirname(path), { recursive: true }); return withCinemaFileLock(path, async () => { const existing = await readJson(path, label); if (existing) { if (stableCinemaJson(existing) === stableCinemaJson(value)) return path; throw new Error(`${label} is immutable and already exists with different content: ${path}`); } await writeTextFileAtomic(path, `${JSON.stringify(value, null, 2)}\n`); return path; }); } export async function writeCinemaCapabilitySnapshot(root: string, snapshot: CinemaProviderCapabilitySnapshotArtifact): Promise { assertValid(`Cinema capability snapshot ${snapshot.snapshotId}`, validateCinemaCapabilitySnapshot(snapshot)); return writeImmutable( cinemaCapabilitySnapshotPathFor(root, snapshot.projectSlug, snapshot.snapshotId), snapshot, `Cinema capability snapshot ${snapshot.snapshotId}`, ); } export async function readCinemaCapabilitySnapshot(root: string, projectSlug: string, snapshotId: string): Promise { const value = await readJson( cinemaCapabilitySnapshotPathFor(root, projectSlug, snapshotId), `Cinema capability snapshot ${snapshotId}`, ); if (!value) return null; if (value.projectSlug !== projectSlug) throw new Error(`Cinema capability snapshot belongs to ${value.projectSlug}, expected ${projectSlug}`); assertValid(`Cinema capability snapshot ${snapshotId}`, validateCinemaCapabilitySnapshot(value)); return value; } export async function writeCinemaGenerationQuote( root: string, quote: CinemaGenerationQuoteArtifact, snapshot: CinemaProviderCapabilitySnapshotArtifact, ): Promise { assertValid(`Cinema generation quote ${quote.quoteId}`, validateCinemaGenerationQuote(quote, snapshot)); return writeImmutable( cinemaGenerationQuotePathFor(root, quote.projectSlug, quote.quoteId), quote, `Cinema generation quote ${quote.quoteId}`, ); } export async function readCinemaGenerationQuote(root: string, projectSlug: string, quoteId: string): Promise { const value = await readJson( cinemaGenerationQuotePathFor(root, projectSlug, quoteId), `Cinema generation quote ${quoteId}`, ); if (value && value.projectSlug !== projectSlug) throw new Error(`Cinema generation quote belongs to ${value.projectSlug}, expected ${projectSlug}`); return value; } function sealProjectionBundle(input: Omit): CinemaProviderProjectionBundleArtifact { const artifact = { ...structuredClone(input), contentHash: 'sha256:'.padEnd(71, '0') as CinemaSha256 }; artifact.contentHash = cinemaArtifactContentHash(artifact); return artifact; } function assertProjectionBundle(bundle: CinemaProviderProjectionBundleArtifact, snapshot: CinemaProviderCapabilitySnapshotArtifact, quote: CinemaGenerationQuoteArtifact): void { if (!bundle.bundleId.trim() || bundle.projections.length === 0) throw new Error('Cinema provider projection bundle must be non-empty'); if (bundle.projectSlug !== quote.projectSlug || bundle.quoteId !== quote.quoteId || bundle.routeId !== quote.routeId || bundle.capabilitySnapshotId !== snapshot.snapshotId || bundle.capabilitySnapshotHash !== snapshot.contentHash) { throw new Error('Cinema provider projection bundle does not bind the exact quote and capability snapshot'); } if (new Set(bundle.projections.map((projection) => projection.taskId)).size !== bundle.projections.length) throw new Error('Cinema provider projection bundle task IDs must be unique'); if (bundle.projections.length !== quote.jobs.length) throw new Error('Cinema provider projection bundle must cover every and only quoted job'); bundle.projections.forEach((projection, index) => { const job = quote.jobs[index]; if (projection.capabilitySnapshotId !== snapshot.snapshotId || projection.capabilitySnapshotHash !== snapshot.contentHash || projection.routeId !== quote.routeId || projection.fallbackPolicy !== 'forbidden' || projection.taskId !== job.jobId) { throw new Error(`Cinema provider projection ${projection.taskId} does not match quoted job ${job.jobId}`); } assertCinemaProjectionQuoteParity(projection, { routeId: quote.routeId, payloadHash: job.payloadHash, providerArgumentsHash: job.providerArgumentsHash, sourceHashes: job.sourceHashes }); }); if (bundle.contentHash !== cinemaArtifactContentHash(bundle)) throw new Error('Cinema provider projection bundle content hash does not match its canonical body'); } export async function writeCinemaProviderProjectionBundle( root: string, input: { quote: CinemaGenerationQuoteArtifact; snapshot: CinemaProviderCapabilitySnapshotArtifact; projections: CinemaProviderProjection[] }, ): Promise<{ bundle: CinemaProviderProjectionBundleArtifact; path: string }> { const bundle = sealProjectionBundle({ schemaVersion: 1, bundleId: `projections-${input.quote.quoteId}`, quoteId: input.quote.quoteId, projectSlug: input.quote.projectSlug, routeId: input.quote.routeId, capabilitySnapshotId: input.snapshot.snapshotId, capabilitySnapshotHash: input.snapshot.contentHash, createdAt: input.quote.createdAt, projections: input.projections, }); assertProjectionBundle(bundle, input.snapshot, input.quote); const path = await writeImmutable(cinemaProviderProjectionBundlePathFor(root, bundle.projectSlug, bundle.quoteId), bundle, `Cinema provider projection bundle ${bundle.bundleId}`); return { bundle, path }; } export async function readCinemaProviderProjectionBundle(root: string, projectSlug: string, quoteId: string): Promise { const bundle = await readJson(cinemaProviderProjectionBundlePathFor(root, projectSlug, quoteId), `Cinema provider projection bundle for ${quoteId}`); if (!bundle) return null; if (bundle.projectSlug !== projectSlug || bundle.quoteId !== quoteId) throw new Error(`Cinema provider projection bundle belongs to ${bundle.projectSlug}/${bundle.quoteId}, expected ${projectSlug}/${quoteId}`); if (bundle.contentHash !== cinemaArtifactContentHash(bundle)) throw new Error('Cinema provider projection bundle content hash does not match its canonical body'); return bundle; } export async function writeCinemaGenerationAuthorization( root: string, authorization: CinemaGenerationAuthorizationArtifact, quote: CinemaGenerationQuoteArtifact, snapshot: CinemaProviderCapabilitySnapshotArtifact, ): Promise { assertValid( `Cinema generation authorization ${authorization.authorizationId}`, validateCinemaGenerationAuthorization(authorization, quote, snapshot), ); return writeImmutable( cinemaGenerationAuthorizationPathFor(root, authorization.projectSlug, authorization.authorizationId), authorization, `Cinema generation authorization ${authorization.authorizationId}`, ); } export async function readCinemaGenerationAuthorization(root: string, projectSlug: string, authorizationId: string): Promise { const value = await readJson( cinemaGenerationAuthorizationPathFor(root, projectSlug, authorizationId), `Cinema generation authorization ${authorizationId}`, ); if (value && value.projectSlug !== projectSlug) throw new Error(`Cinema generation authorization belongs to ${value.projectSlug}, expected ${projectSlug}`); return value; }