gencow 0.1.231 → 0.1.233

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/bin/gencow.mjs CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  updateEnvLocalUrl,
39
39
  } from "../lib/cli-project-runtime.mjs";
40
40
  import { createCodegenCommand } from "../lib/codegen-command.mjs";
41
+ import { resolveCliArtifactSourceRevision } from "../lib/deployment-artifact-provenance.mjs";
41
42
  import { runCliCommand } from "../lib/cli-command-runner.mjs";
42
43
  import { createDoctorCommand } from "../lib/doctor-command.mjs";
43
44
  import { maybeCheckCliVersion } from "../lib/cli-version-check.mjs";
@@ -89,8 +90,12 @@ import { failReleaseAttempt, finalizeReleaseAttempt, prepareReleaseAttempt } fro
89
90
  const __dirname = dirname(fileURLToPath(import.meta.url));
90
91
  const CLI_PACKAGE_JSON = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8"));
91
92
  const CLI_VERSION = CLI_PACKAGE_JSON.version || "0.0.0";
93
+ const CLI_SOURCE_REVISION = resolveCliArtifactSourceRevision(CLI_VERSION);
92
94
  const _drizzleKitCmd = (subcmd, options = {}) => buildDrizzleKitCommand(subcmd, options);
93
- const generateApiTs = createApiCodegenRuntime({});
95
+ const generateApiTs = createApiCodegenRuntime({
96
+ cliVersion: CLI_VERSION,
97
+ sourceRevision: CLI_SOURCE_REVISION,
98
+ });
94
99
  const runAddCommand = createAddCommand({
95
100
  loadConfig,
96
101
  updateComponentReadmeImpl: updateComponentReadme,
@@ -113,6 +118,8 @@ const runAppCommand = createAppCommand({
113
118
  const runBackupCommand = createBackupCommand();
114
119
  const { login: runLoginCommand, logout: runLogoutCommand, whoami: runWhoamiCommand } = createAuthCommands();
115
120
  const runCodegenCommand = createCodegenCommand({
121
+ cliVersion: CLI_VERSION,
122
+ sourceRevision: CLI_SOURCE_REVISION,
116
123
  loadConfig,
117
124
  findServerRoot,
118
125
  resolveCoreImport,
@@ -159,6 +166,7 @@ const {
159
166
  dbStudio: runDbStudioCommand,
160
167
  } = createDbCommands({
161
168
  cliVersion: CLI_VERSION,
169
+ sourceRevision: CLI_SOURCE_REVISION,
162
170
  buildEnv,
163
171
  cleanupOldBackups,
164
172
  drizzleKitCmdImpl: _drizzleKitCmd,
@@ -169,6 +177,7 @@ const {
169
177
  });
170
178
  const runDevCloudCommand = createDevCloudCommand({
171
179
  cliVersion: CLI_VERSION,
180
+ sourceRevision: CLI_SOURCE_REVISION,
172
181
  buildEnvImpl: buildEnv,
173
182
  requireCreds,
174
183
  loadConfig,
@@ -246,6 +255,7 @@ const runStaticDeployRuntime = createStaticDeployRuntime({
246
255
  const runDeployCommand = createDeployCommand({
247
256
  buildEnvImpl: buildEnv,
248
257
  cliVersion: CLI_VERSION,
258
+ sourceRevision: CLI_SOURCE_REVISION,
249
259
  cwdImpl: () => process.cwd(),
250
260
  drizzleKitCmdImpl: _drizzleKitCmd,
251
261
  existsSyncImpl: existsSync,
@@ -280,6 +290,8 @@ const runInitCommand = createInitCommand({
280
290
  templateFeatureDir: resolve(__dirname, "..", "templateFeature"),
281
291
  runCodegenImpl: async (projectDir) => {
282
292
  const runProjectCodegen = createCodegenCommand({
293
+ cliVersion: CLI_VERSION,
294
+ sourceRevision: CLI_SOURCE_REVISION,
283
295
  loadConfig: (opts = {}) => loadConfig({ ...opts, cwd: projectDir }),
284
296
  findServerRoot,
285
297
  resolveCoreImport,
package/core/index.js CHANGED
@@ -3016,6 +3016,7 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
3016
3016
  "GENCOW_PLATFORM_DB",
3017
3017
  "GENCOW_MANAGED_PUBLIC_ORIGINS",
3018
3018
  "PLATFORM_OPENAI_KEY",
3019
+ "PLATFORM_OPENROUTER_KEY",
3019
3020
  "PLATFORM_GOOGLE_KEY",
3020
3021
  "PLATFORM_GOOGLE_CLIENT_ID",
3021
3022
  "PLATFORM_GOOGLE_CLIENT_SECRET",
@@ -25,7 +25,7 @@ function collectFiles(backendRoot, archiveRoot, excludes, directory = backendRoo
25
25
  throw new Error(`Bundle entry ${JSON.stringify(entry)} must be a regular file`);
26
26
  }
27
27
  const relativePath = canonicalPath(relative(backendRoot, path));
28
- const archivePath = canonicalPath(`${archiveRoot}/${relativePath}`);
28
+ const archivePath = canonicalPath(archiveRoot ? `${archiveRoot}/${relativePath}` : relativePath);
29
29
  if (!shouldIncludeTarEntry(archivePath, excludes)) continue;
30
30
  const bytes = readFileSync(path);
31
31
  files.push({ relativePath, archivePath, size: bytes.length, sha256: sha256(bytes) });
@@ -33,6 +33,19 @@ function collectFiles(backendRoot, archiveRoot, excludes, directory = backendRoo
33
33
  return files;
34
34
  }
35
35
 
36
+ export function buildArchivePayloadFilePlan({
37
+ workspaceRoot,
38
+ excludes = [],
39
+ excludedPaths = [".gencow/migration-bundle-manifest.json"],
40
+ }) {
41
+ const files = collectFiles(workspaceRoot, "", excludes).filter(
42
+ (file) => !excludedPaths.includes(file.relativePath),
43
+ );
44
+ assertUniqueCanonicalPaths(files);
45
+ const rows = files.map((file) => [file.relativePath, file.size, file.sha256]);
46
+ return { rows, sha256: sha256(JSON.stringify(rows)) };
47
+ }
48
+
36
49
  function assertUniqueCanonicalPaths(files) {
37
50
  const exact = new Set();
38
51
  const folded = new Set();
@@ -146,6 +146,8 @@ export function createApiCodegenRuntime(options = {}) {
146
146
  successImpl = success,
147
147
  updateComponentReadmeImpl = updateComponentReadme,
148
148
  emitAuthSchemaArtifactImpl,
149
+ cliVersion,
150
+ sourceRevision,
149
151
  warnImpl = warn,
150
152
  writeFileSyncImpl = writeFileSync,
151
153
  } = options;
@@ -174,6 +176,8 @@ export function createApiCodegenRuntime(options = {}) {
174
176
  resolvePathImpl,
175
177
  serverPublishDir: resolveCodegenServerPublishDir(cwd, config),
176
178
  successImpl,
179
+ cliVersion,
180
+ sourceRevision,
177
181
  });
178
182
  assertToolingRuntimeCapabilities(bundled, ["registryRuntime"], {
179
183
  label: "tooling runtime",
@@ -10,6 +10,7 @@ import {
10
10
  resolveProjectSelection,
11
11
  selectCodegenClient,
12
12
  } from "./project-context.mjs";
13
+ import { writeCodegenProvenance } from "./deployment-artifact-provenance.mjs";
13
14
 
14
15
  export { formatAuthSchemaConflictMessage } from "./auth-schema-provenance-guard.mjs";
15
16
 
@@ -193,6 +194,8 @@ export async function runCodegenWithReporting({
193
194
  resolvePathImpl = resolve,
194
195
  serverPublishDir,
195
196
  successImpl,
197
+ cliVersion,
198
+ sourceRevision,
196
199
  }) {
197
200
  const bundled = await (loadCodegenBundleImpl ?? loadCodegenBundleDefault)();
198
201
 
@@ -223,6 +226,13 @@ export async function runCodegenWithReporting({
223
226
  });
224
227
  logCodegenDiagnostics(logImpl, result.diagnostics, diagnosticsColors);
225
228
  assertCodegenSucceeded(result);
229
+ if (cliVersion && sourceRevision) {
230
+ writeCodegenProvenance({
231
+ cliVersion,
232
+ sourceRevision,
233
+ serverPublishDir: effectiveServerPublishDir,
234
+ });
235
+ }
226
236
  return { bundled, result };
227
237
  }
228
238
 
@@ -347,6 +357,8 @@ export function createCodegenCommand(deps) {
347
357
  publishDir,
348
358
  resolvePathImpl: deps.resolvePathImpl,
349
359
  successImpl: deps.successImpl,
360
+ cliVersion: deps.cliVersion,
361
+ sourceRevision: deps.sourceRevision,
350
362
  });
351
363
  deps.successImpl("Codegen completed successfully.");
352
364
  } catch (error) {
@@ -149,6 +149,7 @@ function renderCloudSeedMissingFile({ infoImpl = info, logImpl = log, warnImpl =
149
149
 
150
150
  export function createDbCommands({
151
151
  cliVersion = "0.0.0",
152
+ sourceRevision,
152
153
  buildEnv,
153
154
  cleanupOldBackups,
154
155
  createInterfaceImpl,
@@ -219,6 +220,7 @@ export function createDbCommands({
219
220
  warnImpl,
220
221
  writeFileSyncImpl,
221
222
  cliVersion,
223
+ sourceRevision,
222
224
  };
223
225
  const runDbPush = createDbPushCommand(dbMigrationDependencies);
224
226
  const runDbCheck = createDbCheckCommand(dbMigrationDependencies);
@@ -426,6 +426,7 @@ function createDbMigrationCommand(deps, command) {
426
426
  cliVersion: deps.cliVersion,
427
427
  generatorVersion,
428
428
  sourceMode,
429
+ sourceRevision: deps.sourceRevision,
429
430
  schemaSourcePaths,
430
431
  cpSyncImpl: deps.cpSyncImpl,
431
432
  mkdirSyncImpl: deps.mkdirSyncImpl,
@@ -445,7 +446,9 @@ function createDbMigrationCommand(deps, command) {
445
446
  emitFailure(
446
447
  error?.code === "MIGRATION_BUNDLE_LOCAL_INTEGRITY_FAILED"
447
448
  ? buildLocalBundleIntegrityFailureDiagnostic(clientCorrelationId())
448
- : buildClientBundleFailureDiagnostic(clientCorrelationId(), retryCommand),
449
+ : error?.code === "MIGRATION_BUNDLE_SCHEMA_CHANGE_WITHOUT_FORWARD_MIGRATION"
450
+ ? buildClientUserFixableDiagnostic(error.code, clientCorrelationId())
451
+ : buildClientBundleFailureDiagnostic(clientCorrelationId(), retryCommand),
449
452
  );
450
453
  return;
451
454
  }
@@ -482,6 +485,12 @@ function createDbMigrationCommand(deps, command) {
482
485
  "X-Gencow-Environment": envLabel,
483
486
  "X-Gencow-Migration-Operation-ID": receiptId,
484
487
  "X-Gencow-Migration-Bundle-Sha256": stagedBundle.manifest.bundleSha256,
488
+ ...(stagedBundle.manifest.artifactIdentitySha256
489
+ ? {
490
+ "X-Gencow-Migration-Artifact-Identity-Sha256":
491
+ stagedBundle.manifest.artifactIdentitySha256,
492
+ }
493
+ : {}),
485
494
  },
486
495
  body: bundleData,
487
496
  },
@@ -13,6 +13,7 @@ export const CANONICAL_BUNDLE_BACKEND_ROOT = "gencow";
13
13
 
14
14
  export async function writeCanonicalDeployBundle({
15
15
  cliVersion,
16
+ sourceRevision,
16
17
  cwd,
17
18
  backendRoot,
18
19
  dependencyAuditManifest,
@@ -122,6 +123,7 @@ export async function writeCanonicalDeployBundle({
122
123
  config: canonicalManifestConfig ?? undefined,
123
124
  backendExcludes: envExcludes,
124
125
  cliVersion,
126
+ sourceRevision,
125
127
  generatorVersion,
126
128
  existsSyncImpl,
127
129
  mkdirSyncImpl,
@@ -63,6 +63,8 @@ export async function prepareDeployCodegenArtifacts({
63
63
  rmSyncImpl,
64
64
  runCodegenForDeployImpl = runCodegenWithReporting,
65
65
  successImpl,
66
+ cliVersion,
67
+ sourceRevision,
66
68
  }) {
67
69
  const authSchemaPath = resolvePathImpl(cwd, backendRoot, "schema-auth.ts");
68
70
  const serverPublishDir = resolveCodegenServerPublishDir(cwd, config);
@@ -109,6 +111,8 @@ export async function prepareDeployCodegenArtifacts({
109
111
  publishDir: deployCodegenClientDir,
110
112
  serverPublishDir: deployCodegenServerDir,
111
113
  successImpl,
114
+ cliVersion,
115
+ sourceRevision,
112
116
  });
113
117
  } catch {
114
118
  cleanup();
@@ -1,8 +1,12 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
 
3
- import { MIGRATION_BUNDLE_CAPABILITY_V1 } from "@gencow/migration-contract";
3
+ import {
4
+ MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2,
5
+ MIGRATION_BUNDLE_CAPABILITY_V1,
6
+ } from "@gencow/migration-contract";
4
7
 
5
8
  export const DEPLOY_CLAIM_PROTOCOL_VERSION = 1;
9
+ export const DEPLOY_CLAIM_PROTOCOL_VERSION_V2 = 2;
6
10
  const TOP_LEVEL_FIELDS = [
7
11
  "protocolVersion",
8
12
  "requestId",
@@ -139,10 +143,65 @@ export function canonicalizeDeployClaimV1(input) {
139
143
  return claim;
140
144
  }
141
145
 
146
+ export function canonicalizeDeployClaimV2(input) {
147
+ requireExactFields(input, TOP_LEVEL_FIELDS, "deploy claim");
148
+ if (input.protocolVersion !== DEPLOY_CLAIM_PROTOCOL_VERSION_V2) {
149
+ throw new Error("deploy claim protocolVersion is not supported");
150
+ }
151
+ const migrationBundle = requireObject(input.migrationBundle, "deploy claim migrationBundle");
152
+ requireExactFields(
153
+ migrationBundle,
154
+ ["manifestSha256", "requiredCapability", "artifactIdentitySha256"],
155
+ "deploy claim migrationBundle",
156
+ );
157
+ const legacy = canonicalizeDeployClaimV1({
158
+ ...input,
159
+ protocolVersion: DEPLOY_CLAIM_PROTOCOL_VERSION,
160
+ migrationBundle: {
161
+ manifestSha256: migrationBundle.manifestSha256,
162
+ requiredCapability:
163
+ migrationBundle.requiredCapability === MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2
164
+ ? MIGRATION_BUNDLE_CAPABILITY_V1
165
+ : migrationBundle.requiredCapability,
166
+ },
167
+ });
168
+ const identitySha256 = requireSha256(
169
+ migrationBundle.artifactIdentitySha256,
170
+ "migration artifactIdentitySha256",
171
+ true,
172
+ );
173
+ if (
174
+ legacy.declaredRuntime !== "server" ||
175
+ identitySha256 === null ||
176
+ migrationBundle.requiredCapability !== MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2
177
+ ) {
178
+ throw new Error("deploy claim artifact identity is inconsistent");
179
+ }
180
+ return {
181
+ ...legacy,
182
+ protocolVersion: DEPLOY_CLAIM_PROTOCOL_VERSION_V2,
183
+ migrationBundle: {
184
+ manifestSha256: legacy.migrationBundle.manifestSha256,
185
+ requiredCapability: identitySha256 ? MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2 : "none",
186
+ artifactIdentitySha256: identitySha256,
187
+ },
188
+ };
189
+ }
190
+
191
+ export function canonicalizeDeployClaim(input) {
192
+ return input?.protocolVersion === DEPLOY_CLAIM_PROTOCOL_VERSION_V2
193
+ ? canonicalizeDeployClaimV2(input)
194
+ : canonicalizeDeployClaimV1(input);
195
+ }
196
+
142
197
  export function encodeDeployClaimV1(input) {
143
198
  return Buffer.from(JSON.stringify(canonicalizeDeployClaimV1(input))).toString("base64url");
144
199
  }
145
200
 
201
+ export function encodeDeployClaimV2(input) {
202
+ return Buffer.from(JSON.stringify(canonicalizeDeployClaimV2(input))).toString("base64url");
203
+ }
204
+
146
205
  export function hashDeployClaimV1(input) {
147
206
  return createHash("sha256")
148
207
  .update(JSON.stringify(canonicalizeDeployClaimV1(input)))
@@ -3,9 +3,19 @@ import { Readable } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import { list } from "tar";
5
5
 
6
- import { MIGRATION_BUNDLE_CAPABILITY_V1, inspectPnpmDependencyLock } from "@gencow/migration-contract";
6
+ import {
7
+ MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2,
8
+ MIGRATION_BUNDLE_CAPABILITY_V1,
9
+ inspectPnpmDependencyLock,
10
+ } from "@gencow/migration-contract";
7
11
 
8
- import { canonicalizeDeployClaimV1, createDeployRequestId, encodeDeployClaimV1 } from "./deploy-contract.mjs";
12
+ import {
13
+ canonicalizeDeployClaimV1,
14
+ canonicalizeDeployClaimV2,
15
+ createDeployRequestId,
16
+ encodeDeployClaimV1,
17
+ encodeDeployClaimV2,
18
+ } from "./deploy-contract.mjs";
9
19
  import { DEPLOY_LOCKFILE_CANDIDATES, UNSUPPORTED_DEPLOY_LOCKFILES } from "./deploy-lockfile-selection.mjs";
10
20
 
11
21
  const PACKAGE_MANIFEST = "package.json";
@@ -139,8 +149,19 @@ export async function buildDeployClaimFromArchive({
139
149
  assertDeployDependencyLock(packageBytes, lockfile ? selected.get(lockfile[0]) : null);
140
150
  }
141
151
  const migrationBytes = selected.get(MIGRATION_MANIFEST);
142
- const claim = canonicalizeDeployClaimV1({
143
- protocolVersion: 1,
152
+ let artifactIdentitySha256 = null;
153
+ if (migrationBytes) {
154
+ try {
155
+ const manifest = JSON.parse(migrationBytes.toString("utf8"));
156
+ artifactIdentitySha256 = manifest.artifactIdentitySha256 ?? null;
157
+ } catch {
158
+ throw new Error("Migration manifest is not valid JSON");
159
+ }
160
+ }
161
+ const protocolVersion = artifactIdentitySha256 ? 2 : 1;
162
+ const canonicalize = protocolVersion === 2 ? canonicalizeDeployClaimV2 : canonicalizeDeployClaimV1;
163
+ const claim = canonicalize({
164
+ protocolVersion,
144
165
  requestId,
145
166
  sourceBundleSha256: sha256(bundleBuffer),
146
167
  declaredRuntime,
@@ -152,9 +173,17 @@ export async function buildDeployClaimFromArchive({
152
173
  },
153
174
  migrationBundle: {
154
175
  manifestSha256: migrationBytes ? sha256(migrationBytes) : null,
155
- requiredCapability: migrationBytes ? MIGRATION_BUNDLE_CAPABILITY_V1 : "none",
176
+ requiredCapability: migrationBytes
177
+ ? artifactIdentitySha256
178
+ ? MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2
179
+ : MIGRATION_BUNDLE_CAPABILITY_V1
180
+ : "none",
181
+ ...(protocolVersion === 2 ? { artifactIdentitySha256 } : {}),
156
182
  },
157
183
  generatedBy: { name: "gencow-cli", version: cliVersion },
158
184
  });
159
- return { claim, header: encodeDeployClaimV1(claim) };
185
+ return {
186
+ claim,
187
+ header: protocolVersion === 2 ? encodeDeployClaimV2(claim) : encodeDeployClaimV1(claim),
188
+ };
160
189
  }
@@ -42,6 +42,7 @@ import {
42
42
  } from "./deploy-lockfile-selection.mjs";
43
43
  import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
44
44
  import { writeCanonicalDeployBundle } from "./deploy-bundle-staging.mjs";
45
+ import { buildClientUserFixableDiagnostic } from "./migration-client-diagnostic.mjs";
45
46
 
46
47
  async function loadProjectConfigDefault(options) {
47
48
  const { loadConfig } = await import("./cli-project-runtime.mjs");
@@ -112,6 +113,7 @@ export function createDeployPackageRuntime({
112
113
  buildEnvImpl,
113
114
  buildDeployClaimFromArchiveImpl = buildDeployClaimFromArchive,
114
115
  cliVersion = "0.0.0",
116
+ sourceRevision,
115
117
  cwdImpl = () => process.cwd(),
116
118
  drizzleKitCmdImpl,
117
119
  errorImpl = error,
@@ -212,6 +214,8 @@ export function createDeployPackageRuntime({
212
214
  rmSyncImpl,
213
215
  runCodegenForDeployImpl,
214
216
  successImpl,
217
+ cliVersion,
218
+ sourceRevision,
215
219
  });
216
220
  }
217
221
  if (
@@ -461,6 +465,7 @@ export function createDeployPackageRuntime({
461
465
  try {
462
466
  ({ bundleBackendRoot } = await writeCanonicalDeployBundle({
463
467
  cliVersion,
468
+ sourceRevision,
464
469
  cwd,
465
470
  backendRoot,
466
471
  dependencyAuditManifest,
@@ -484,7 +489,15 @@ export function createDeployPackageRuntime({
484
489
  writeFileSyncImpl,
485
490
  }));
486
491
  } catch (caught) {
487
- errorImpl(`Packaging failed: ${caught.message}`);
492
+ if (caught?.code === "MIGRATION_BUNDLE_SCHEMA_CHANGE_WITHOUT_FORWARD_MIGRATION") {
493
+ for (const line of renderMigrationDiagnosticLines(
494
+ buildClientUserFixableDiagnostic(caught.code, `client-${Date.now()}`),
495
+ )) {
496
+ errorImpl(line);
497
+ }
498
+ } else {
499
+ errorImpl(`Packaging failed: ${caught.message}`);
500
+ }
488
501
  exitImpl(1);
489
502
  cleanupPreparedDeploySource();
490
503
  return null;
@@ -624,7 +637,7 @@ export function createDeployPackageRuntime({
624
637
  ...(activeDeployClaim
625
638
  ? {
626
639
  "X-Gencow-Deploy-Claim": activeDeployClaim.header,
627
- "X-Gencow-Deploy-Protocol": "1",
640
+ "X-Gencow-Deploy-Protocol": String(activeDeployClaim.claim.protocolVersion ?? 1),
628
641
  "X-Gencow-CLI-Version": activeDeployClaim.claim.generatedBy.version,
629
642
  }
630
643
  : {}),
@@ -352,6 +352,7 @@ async function runRollbackDeploy({
352
352
  export function createDeployCommand({
353
353
  buildEnvImpl,
354
354
  cliVersion,
355
+ sourceRevision,
355
356
  createDeployPackageRuntimeImpl = createDeployPackageRuntime,
356
357
  createExistingBundleCandidateImpl = createExistingBundleCandidate,
357
358
  createInterfaceImpl,
@@ -394,6 +395,7 @@ export function createDeployCommand({
394
395
  const packageRuntimeOptions = {
395
396
  buildEnvImpl,
396
397
  cliVersion,
398
+ sourceRevision,
397
399
  createDeployPackageRuntimeImpl,
398
400
  drizzleKitCmdImpl,
399
401
  errorImpl,
@@ -0,0 +1,151 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, lstatSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ canonicalizeCodegenArtifactProvenanceV1,
8
+ compareCanonicalCodegenArtifactPaths,
9
+ hashGeneratedSchemaArtifacts,
10
+ } from "@gencow/migration-contract/artifact-identity";
11
+
12
+ const SHA256 = /^[a-f0-9]{64}$/u;
13
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
14
+ export const CODEGEN_PROVENANCE_FILENAME = ".gencow-codegen-provenance.json";
15
+ const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+ const CLI_IDENTITY_FILES = [
17
+ "package.json",
18
+ "bin/gencow.mjs",
19
+ "lib/codegen-command.mjs",
20
+ "lib/migration-bundle-manifest.mjs",
21
+ "lib/backend-bundle-file-plan.mjs",
22
+ "lib/deployment-artifact-provenance.mjs",
23
+ "runtime/tooling.mjs",
24
+ ];
25
+
26
+ function normalizeProvenance(value, label) {
27
+ if (
28
+ !value ||
29
+ typeof value !== "object" ||
30
+ Array.isArray(value) ||
31
+ Object.keys(value).length !== 2 ||
32
+ typeof value.cliVersion !== "string" ||
33
+ !VERSION.test(value.cliVersion) ||
34
+ typeof value.sourceRevision !== "string" ||
35
+ !SHA256.test(value.sourceRevision)
36
+ ) {
37
+ throw new Error(`${label} artifact provenance is invalid`);
38
+ }
39
+ return { cliVersion: value.cliVersion, sourceRevision: value.sourceRevision };
40
+ }
41
+
42
+ export function deriveCliSourceRevision(cliVersion, sourceFiles = []) {
43
+ if (typeof cliVersion !== "string" || !VERSION.test(cliVersion)) {
44
+ throw new Error("CLI version is invalid");
45
+ }
46
+ const rows = sourceFiles.map(({ path, bytes }) => [
47
+ String(path).replaceAll("\\", "/").normalize("NFC"),
48
+ Buffer.byteLength(bytes),
49
+ createHash("sha256").update(bytes).digest("hex"),
50
+ ]);
51
+ rows.sort((left, right) => left[0].localeCompare(right[0]));
52
+ return createHash("sha256").update(JSON.stringify({ cliVersion, rows })).digest("hex");
53
+ }
54
+
55
+ export function resolveCliArtifactSourceRevision(cliVersion, options = {}) {
56
+ const root = options.cliRoot ?? CLI_ROOT;
57
+ const read = options.readFileSyncImpl ?? readFileSync;
58
+ return deriveCliSourceRevision(
59
+ cliVersion,
60
+ CLI_IDENTITY_FILES.map((path) => ({ path, bytes: read(resolve(root, path)) })),
61
+ );
62
+ }
63
+
64
+ function collectGeneratedArtifacts(root, directory = root) {
65
+ const rows = [];
66
+ for (const entry of readdirSync(directory).sort()) {
67
+ if (entry === CODEGEN_PROVENANCE_FILENAME) continue;
68
+ const path = resolve(directory, entry);
69
+ const stats = lstatSync(path);
70
+ if (stats.isDirectory()) {
71
+ rows.push(...collectGeneratedArtifacts(root, path));
72
+ continue;
73
+ }
74
+ if (!stats.isFile() || stats.isSymbolicLink()) {
75
+ throw new Error("Generated schema artifact must be a regular file");
76
+ }
77
+ const bytes = readFileSync(path);
78
+ rows.push({
79
+ path: relative(root, path).replaceAll("\\", "/").normalize("NFC"),
80
+ size: bytes.length,
81
+ sha256: createHash("sha256").update(bytes).digest("hex"),
82
+ });
83
+ }
84
+ return rows.sort((left, right) => compareCanonicalCodegenArtifactPaths(left.path, right.path));
85
+ }
86
+
87
+ export function buildCodegenProvenance({ cliVersion, sourceRevision, serverPublishDir }) {
88
+ const producer = normalizeProvenance({ cliVersion, sourceRevision }, "codegen");
89
+ const artifacts = collectGeneratedArtifacts(serverPublishDir);
90
+ return canonicalizeCodegenArtifactProvenanceV1({
91
+ version: 1,
92
+ ...producer,
93
+ generatedSchemaDigest: hashGeneratedSchemaArtifacts(artifacts),
94
+ artifacts,
95
+ });
96
+ }
97
+
98
+ export function writeCodegenProvenance({ cliVersion, sourceRevision, serverPublishDir }) {
99
+ const provenance = buildCodegenProvenance({ cliVersion, sourceRevision, serverPublishDir });
100
+ const markerPath = resolve(serverPublishDir, CODEGEN_PROVENANCE_FILENAME);
101
+ const temporaryPath = resolve(
102
+ dirname(serverPublishDir),
103
+ `.${basename(serverPublishDir)}-${CODEGEN_PROVENANCE_FILENAME}.tmp-${process.pid}-${randomUUID()}`,
104
+ );
105
+ try {
106
+ writeFileSync(temporaryPath, `${JSON.stringify(provenance)}\n`, { mode: 0o600, flag: "wx" });
107
+ renameSync(temporaryPath, markerPath);
108
+ } finally {
109
+ rmSync(temporaryPath, { force: true });
110
+ }
111
+ return provenance;
112
+ }
113
+
114
+ export function readCodegenProvenance(path) {
115
+ if (!existsSync(path)) return null;
116
+ const stats = lstatSync(path);
117
+ if (!stats.isFile() || stats.isSymbolicLink()) {
118
+ throw new Error("codegen artifact provenance is invalid");
119
+ }
120
+ try {
121
+ return canonicalizeCodegenArtifactProvenanceV1(JSON.parse(readFileSync(path, "utf8")));
122
+ } catch {
123
+ throw new Error("codegen artifact provenance is invalid");
124
+ }
125
+ }
126
+
127
+ export function verifyCodegenProvenance(path) {
128
+ const provenance = readCodegenProvenance(path);
129
+ if (!provenance) return null;
130
+ const actual = collectGeneratedArtifacts(dirname(path));
131
+ if (
132
+ JSON.stringify(actual) !== JSON.stringify(provenance.artifacts) ||
133
+ hashGeneratedSchemaArtifacts(actual) !== provenance.generatedSchemaDigest
134
+ ) {
135
+ const error = new Error("PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH");
136
+ error.code = "PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH";
137
+ throw error;
138
+ }
139
+ return provenance;
140
+ }
141
+
142
+ export function assertMatchingCodegenAndPackagerProvenance(input) {
143
+ const codegen = normalizeProvenance(input?.codegen, "codegen");
144
+ const packager = normalizeProvenance(input?.packager, "packager");
145
+ if (codegen.cliVersion !== packager.cliVersion || codegen.sourceRevision !== packager.sourceRevision) {
146
+ const error = new Error("PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH");
147
+ error.code = "PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH";
148
+ throw error;
149
+ }
150
+ return packager;
151
+ }
@@ -96,6 +96,7 @@ export async function createDevCloudDeployBundle({
96
96
  envFile,
97
97
  functionsDir,
98
98
  cliVersion = "0.0.0",
99
+ sourceRevision,
99
100
  generatorVersion = "unknown",
100
101
  config,
101
102
  auditDependencyVersionsImpl = auditDependencyVersions,
@@ -232,6 +233,7 @@ export async function createDevCloudDeployBundle({
232
233
  backendExcludes: envExcludes,
233
234
  config: loadedProjectConfig ? canonicalConfig : undefined,
234
235
  cliVersion,
236
+ sourceRevision,
235
237
  generatorVersion,
236
238
  existsSyncImpl,
237
239
  mkdirSyncImpl,