@growth-labs/cms 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -168,6 +168,49 @@ manual publish with a 409 and leaves the content status unchanged; throwing from
168
168
  the hook fails closed with a 503. This keeps media-specific readiness checks in
169
169
  the host site while preventing a configured site from bypassing them.
170
170
 
171
+ ## Consumer D1 migrations
172
+
173
+ Wrangler accepts one `migrations_dir`, while a site commonly already owns
174
+ numbered analytics, SEO, or product migrations. Vendor the exact SQL from the
175
+ installed CMS package into that existing directory, review it, and commit it:
176
+
177
+ ```bash
178
+ pnpm exec growth-labs-cms-migrations sync --dir migrations --start 10
179
+ pnpm exec growth-labs-cms-migrations verify --dir migrations
180
+ ```
181
+
182
+ `--start` is the first unused site sequence and is mandatory. `sync` reserves a
183
+ contiguous range, copies every package migration without rewriting it, and writes
184
+ `migrations/.growth-labs-cms-migrations.json` with the package version and SHA-256
185
+ receipt. It fails before writing if any site SQL already owns a reserved sequence.
186
+ Existing managed SQL is never overwritten: consumer drift, upstream mutation of
187
+ an already-vendored migration, a changed starting sequence, or a symlink target
188
+ fails closed.
189
+
190
+ The default receipt path is the only manifest allowed inside `migrations/`.
191
+ When `--manifest` is supplied, it must name a `.json` file outside the migrations
192
+ directory whose parent is an existing non-symlink directory; it can never
193
+ collide with or become Wrangler SQL. Sync stages the full SQL set and receipt
194
+ before linking targets. A later link, verification, or receipt failure removes
195
+ every SQL file created by that attempt. An interrupted process may leave only
196
+ exact-hash linked SQL and/or `*.tmp-<uuid>` staging files, but no valid completed
197
+ receipt; `verify` therefore fails closed until a repeated `sync` reconciles the
198
+ batch. Wrangler ignores the temp files, which are safe to delete after the
199
+ repeated sync and verify succeed. Never run the managed D1 workflow without the
200
+ required verify step.
201
+
202
+ Both `sync` and `verify` acquire an exclusive adjacent `<receipt>.lock` file,
203
+ so concurrent invocations fail closed instead of racing the manifest update.
204
+ An abruptly terminated process can leave a stale lock; first confirm no vendor
205
+ or verify process is running, then remove only that lock and repeat `sync`
206
+ followed by `verify`.
207
+
208
+ Run `sync` deliberately after a package upgrade, inspect and commit the new SQL
209
+ and receipt, then require `verify` in pull-request CI before the managed D1
210
+ migration workflow. The command only reads and writes local files; it never opens
211
+ a network connection or applies a database migration. Do not call schema setup
212
+ from a request handler.
213
+
171
214
  ## Bounded publication kernel
172
215
 
173
216
  `publishOne` is the build-independent publication path for a single content item.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/cli/migrations.ts"],"names":[],"mappings":""}
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { syncCmsMigrations, verifyCmsMigrations } from '../migration-vendor.js';
3
+ function usage() {
4
+ return [
5
+ 'Usage:',
6
+ ' growth-labs-cms-migrations sync --dir <migrations-dir> --start <positive-integer>',
7
+ ' growth-labs-cms-migrations verify --dir <migrations-dir>',
8
+ '',
9
+ 'Optional for both commands: --manifest <receipt-path>',
10
+ ].join('\n');
11
+ }
12
+ function requireValue(argv, index, flag) {
13
+ const value = argv[index + 1];
14
+ if (!value || value.startsWith('--'))
15
+ throw new Error(`${flag} requires a value`);
16
+ return value;
17
+ }
18
+ function parseArgs(argv) {
19
+ const command = argv[0];
20
+ if (command !== 'sync' && command !== 'verify')
21
+ throw new Error(usage());
22
+ let migrationsDir;
23
+ let startSequence;
24
+ let manifestPath;
25
+ for (let index = 1; index < argv.length; index += 1) {
26
+ const flag = argv[index];
27
+ if (flag === '--dir') {
28
+ migrationsDir = requireValue(argv, index, flag);
29
+ index += 1;
30
+ continue;
31
+ }
32
+ if (flag === '--start') {
33
+ startSequence = Number(requireValue(argv, index, flag));
34
+ index += 1;
35
+ continue;
36
+ }
37
+ if (flag === '--manifest') {
38
+ manifestPath = requireValue(argv, index, flag);
39
+ index += 1;
40
+ continue;
41
+ }
42
+ throw new Error(`unknown argument: ${flag}\n\n${usage()}`);
43
+ }
44
+ if (!migrationsDir)
45
+ throw new Error(`--dir is required\n\n${usage()}`);
46
+ if (command === 'sync' && startSequence === undefined) {
47
+ throw new Error(`--start is required for sync\n\n${usage()}`);
48
+ }
49
+ if (command === 'verify' && startSequence !== undefined) {
50
+ throw new Error('--start is read from the committed manifest during verify');
51
+ }
52
+ return { command, migrationsDir, startSequence, manifestPath };
53
+ }
54
+ async function main() {
55
+ const args = parseArgs(process.argv.slice(2));
56
+ const result = args.command === 'sync'
57
+ ? await syncCmsMigrations({
58
+ migrationsDir: args.migrationsDir,
59
+ startSequence: args.startSequence,
60
+ manifestPath: args.manifestPath,
61
+ })
62
+ : await verifyCmsMigrations({
63
+ migrationsDir: args.migrationsDir,
64
+ manifestPath: args.manifestPath,
65
+ });
66
+ process.stdout.write(`${JSON.stringify({ ok: true, command: args.command, ...result })}\n`);
67
+ }
68
+ main().catch((error) => {
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ process.stderr.write(`growth-labs-cms-migrations: ${message}\n`);
71
+ process.exitCode = 1;
72
+ });
73
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/cli/migrations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export interface StagedMigrationVendorFile {
2
+ tempPath: string;
3
+ targetPath: string;
4
+ }
5
+ export interface MigrationVendorFileOperations {
6
+ link(source: string, target: string): Promise<void>;
7
+ unlink(path: string): Promise<void>;
8
+ }
9
+ export declare function rollbackNewFiles(paths: string[], operations?: MigrationVendorFileOperations): Promise<void>;
10
+ /**
11
+ * Link every pre-staged file without replacing an existing directory entry.
12
+ * A later failure removes all targets linked by this attempt and every temp.
13
+ */
14
+ export declare function commitNewFilesAtomically(files: StagedMigrationVendorFile[], operations?: MigrationVendorFileOperations): Promise<string[]>;
15
+ /** Commit staged SQL, then run the receipt/final verification boundary. */
16
+ export declare function commitStagedMigrationBatch(files: StagedMigrationVendorFile[], finalize: () => Promise<void>, operations?: MigrationVendorFileOperations): Promise<string[]>;
17
+ //# sourceMappingURL=migration-vendor-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-vendor-files.d.ts","sourceRoot":"","sources":["../src/migration-vendor-files.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC7C,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAmBD,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,yBAAyB,EAAE,EAClC,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,MAAM,EAAE,CAAC,CA8BnB;AAED,2EAA2E;AAC3E,wBAAsB,0BAA0B,CAC/C,KAAK,EAAE,yBAAyB,EAAE,EAClC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBnB"}
@@ -0,0 +1,67 @@
1
+ import { link, unlink } from 'node:fs/promises';
2
+ const DEFAULT_OPERATIONS = { link, unlink };
3
+ async function removePaths(paths, operations) {
4
+ const errors = [];
5
+ for (const path of [...paths].reverse()) {
6
+ try {
7
+ await operations.unlink(path);
8
+ }
9
+ catch (error) {
10
+ if (error.code !== 'ENOENT')
11
+ errors.push(error);
12
+ }
13
+ }
14
+ return errors;
15
+ }
16
+ export async function rollbackNewFiles(paths, operations = DEFAULT_OPERATIONS) {
17
+ const errors = await removePaths(paths, operations);
18
+ if (errors.length > 0) {
19
+ throw new AggregateError(errors, 'failed to roll back newly linked CMS migration files');
20
+ }
21
+ }
22
+ /**
23
+ * Link every pre-staged file without replacing an existing directory entry.
24
+ * A later failure removes all targets linked by this attempt and every temp.
25
+ */
26
+ export async function commitNewFilesAtomically(files, operations = DEFAULT_OPERATIONS) {
27
+ const committed = [];
28
+ let failure;
29
+ try {
30
+ for (const file of files) {
31
+ await operations.link(file.tempPath, file.targetPath);
32
+ committed.push(file.targetPath);
33
+ }
34
+ }
35
+ catch (error) {
36
+ failure = error;
37
+ }
38
+ const cleanupErrors = await removePaths(files.map((file) => file.tempPath), operations);
39
+ if (failure) {
40
+ const rollbackErrors = await removePaths(committed, operations);
41
+ const errors = [failure, ...cleanupErrors, ...rollbackErrors].filter((error) => error !== undefined);
42
+ const primary = failure instanceof Error ? `: ${failure.message}` : '';
43
+ throw new AggregateError(errors, `CMS migration file commit failed${primary}`);
44
+ }
45
+ // A hard-linked target remains valid when unlinking only its staged temp
46
+ // fails. The caller's outer finally retries temp cleanup; rolling targets
47
+ // back here would turn a harmless leftover temp into data loss.
48
+ return committed;
49
+ }
50
+ /** Commit staged SQL, then run the receipt/final verification boundary. */
51
+ export async function commitStagedMigrationBatch(files, finalize, operations = DEFAULT_OPERATIONS) {
52
+ const committed = await commitNewFilesAtomically(files, operations);
53
+ try {
54
+ await finalize();
55
+ return committed;
56
+ }
57
+ catch (error) {
58
+ try {
59
+ await rollbackNewFiles(committed, operations);
60
+ }
61
+ catch (rollbackError) {
62
+ throw new AggregateError([error, rollbackError], 'CMS migration batch finalization failed and rollback was incomplete');
63
+ }
64
+ throw error;
65
+ }
66
+ }
67
+ //# sourceMappingURL=migration-vendor-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-vendor-files.js","sourceRoot":"","sources":["../src/migration-vendor-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ declare const PACKAGE_NAME = "@growth-labs/cms";
2
+ export interface SyncCmsMigrationsOptions {
3
+ /** Existing consumer-owned Wrangler migrations directory. */
4
+ migrationsDir: string;
5
+ /** First unused sequence number reserved for the package migrations. */
6
+ startSequence: number;
7
+ /** Override only when a consumer keeps the receipt outside migrationsDir. */
8
+ manifestPath?: string;
9
+ }
10
+ export interface VerifyCmsMigrationsOptions {
11
+ migrationsDir: string;
12
+ manifestPath?: string;
13
+ }
14
+ export interface CmsMigrationVendorResult {
15
+ packageName: typeof PACKAGE_NAME;
16
+ packageVersion: string;
17
+ startSequence: number;
18
+ managedFiles: string[];
19
+ written: number;
20
+ verified: number;
21
+ manifestPath: string;
22
+ }
23
+ export declare function syncCmsMigrations(options: SyncCmsMigrationsOptions): Promise<CmsMigrationVendorResult>;
24
+ export declare function verifyCmsMigrations(options: VerifyCmsMigrationsOptions): Promise<CmsMigrationVendorResult>;
25
+ export {};
26
+ //# sourceMappingURL=migration-vendor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-vendor.d.ts","sourceRoot":"","sources":["../src/migration-vendor.ts"],"names":[],"mappings":"AAiBA,QAAA,MAAM,YAAY,qBAAqB,CAAA;AAgCvC,MAAM,WAAW,wBAAwB;IACxC,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,0BAA0B;IAC1C,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,wBAAwB;IACxC,WAAW,EAAE,OAAO,YAAY,CAAA;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACpB;AAgSD,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAiDnC;AAED,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,0BAA0B,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAmCnC"}
@@ -0,0 +1,321 @@
1
+ import { link, lstat, readdir, readFile, realpath, rename, unlink, writeFile, } from 'node:fs/promises';
2
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { commitStagedMigrationBatch, } from './migration-vendor-files.js';
5
+ const PACKAGE_NAME = '@growth-labs/cms';
6
+ const MANIFEST_NAME = '.growth-labs-cms-migrations.json';
7
+ const MANIFEST_SCHEMA_VERSION = 1;
8
+ const MIGRATION_FILE = /^\d{4}_[a-z0-9][a-z0-9_-]*\.sql$/;
9
+ async function packageContract() {
10
+ const packageRoot = fileURLToPath(new URL('..', import.meta.url));
11
+ const packageJsonPath = join(packageRoot, 'package.json');
12
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
13
+ if (packageJson.name !== PACKAGE_NAME || !packageJson.version) {
14
+ throw new Error('installed CMS package metadata is invalid');
15
+ }
16
+ return {
17
+ packageVersion: packageJson.version,
18
+ sourceDir: join(packageRoot, 'migrations'),
19
+ };
20
+ }
21
+ async function sha256(bytes) {
22
+ const input = new Uint8Array(bytes.byteLength);
23
+ input.set(bytes);
24
+ const digest = await crypto.subtle.digest('SHA-256', input.buffer);
25
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
26
+ }
27
+ async function requireExistingDirectory(path, label = 'migrationsDir') {
28
+ let stat;
29
+ try {
30
+ stat = await lstat(path);
31
+ }
32
+ catch {
33
+ throw new Error(`${label} must be an existing directory: ${path}`);
34
+ }
35
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
36
+ throw new Error(`${label} must be an existing non-symlink directory: ${path}`);
37
+ }
38
+ return realpath(path);
39
+ }
40
+ function assertStartSequence(value) {
41
+ if (!Number.isSafeInteger(value) || value < 1) {
42
+ throw new Error('startSequence must be a positive integer');
43
+ }
44
+ }
45
+ function sequencePrefix(sequence) {
46
+ return String(sequence).padStart(4, '0');
47
+ }
48
+ async function planMigrations(sourceDir, startSequence) {
49
+ assertStartSequence(startSequence);
50
+ const sourceNames = (await readdir(sourceDir)).filter((name) => MIGRATION_FILE.test(name)).sort();
51
+ if (sourceNames.length === 0)
52
+ throw new Error('installed CMS package has no migration SQL');
53
+ if (startSequence + sourceNames.length - 1 > 9999) {
54
+ throw new Error('reserved CMS migration sequence exceeds 9999');
55
+ }
56
+ return Promise.all(sourceNames.map(async (source, index) => {
57
+ const sequence = startSequence + index;
58
+ const suffix = source.replace(/^\d{4}_/, '');
59
+ const target = `${sequencePrefix(sequence)}_growth_labs_cms_${suffix}`;
60
+ const bytes = new Uint8Array(await readFile(join(sourceDir, source)));
61
+ return { source, target, sequence, sha256: await sha256(bytes), bytes };
62
+ }));
63
+ }
64
+ function manifestFor(packageVersion, startSequence, plan) {
65
+ return {
66
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
67
+ packageName: PACKAGE_NAME,
68
+ packageVersion,
69
+ startSequence,
70
+ files: plan.map(({ source, target, sha256: hash }) => ({ source, target, sha256: hash })),
71
+ };
72
+ }
73
+ async function readManifest(path) {
74
+ try {
75
+ const stat = await lstat(path);
76
+ if (!stat.isFile() || stat.isSymbolicLink()) {
77
+ throw new Error(`CMS migration manifest must be a regular file: ${path}`);
78
+ }
79
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
80
+ if (parsed.schemaVersion !== MANIFEST_SCHEMA_VERSION ||
81
+ parsed.packageName !== PACKAGE_NAME ||
82
+ !parsed.packageVersion ||
83
+ !Number.isSafeInteger(parsed.startSequence) ||
84
+ parsed.startSequence < 1 ||
85
+ !Array.isArray(parsed.files)) {
86
+ throw new Error(`CMS migration manifest is invalid: ${path}`);
87
+ }
88
+ for (const entry of parsed.files) {
89
+ if (!entry ||
90
+ typeof entry.source !== 'string' ||
91
+ typeof entry.target !== 'string' ||
92
+ !MIGRATION_FILE.test(entry.source) ||
93
+ !MIGRATION_FILE.test(entry.target) ||
94
+ !/^([a-f0-9]{64})$/.test(entry.sha256)) {
95
+ throw new Error(`CMS migration manifest contains an invalid file entry: ${path}`);
96
+ }
97
+ }
98
+ return parsed;
99
+ }
100
+ catch (error) {
101
+ if (error.code === 'ENOENT')
102
+ return null;
103
+ throw error;
104
+ }
105
+ }
106
+ function entriesEqual(left, right) {
107
+ return (left.source === right.source && left.target === right.target && left.sha256 === right.sha256);
108
+ }
109
+ function assertManifestCanAdvance(current, next) {
110
+ if (!current)
111
+ return;
112
+ if (current.startSequence !== next.startSequence) {
113
+ throw new Error(`CMS migration manifest reserves startSequence ${current.startSequence}; received ${next.startSequence}`);
114
+ }
115
+ if (current.files.length > next.files.length) {
116
+ throw new Error('installed CMS package removed migrations recorded by the consumer manifest');
117
+ }
118
+ for (const [index, entry] of current.files.entries()) {
119
+ const nextEntry = next.files[index];
120
+ if (!nextEntry || !entriesEqual(entry, nextEntry)) {
121
+ throw new Error(`installed CMS migration changed after vendoring: ${entry.source}`);
122
+ }
123
+ }
124
+ }
125
+ async function assertNoSequenceCollisions(migrationsDir, plan) {
126
+ const existing = await readdir(migrationsDir);
127
+ for (const migration of plan) {
128
+ const prefix = `${sequencePrefix(migration.sequence)}_`;
129
+ const collisions = existing.filter((name) => name.endsWith('.sql') && name.startsWith(prefix) && name !== migration.target);
130
+ if (collisions.length > 0) {
131
+ throw new Error(`reserved sequence ${sequencePrefix(migration.sequence)} collides with ${collisions.join(', ')}`);
132
+ }
133
+ }
134
+ }
135
+ async function verifyTargets(migrationsDir, plan, allowMissing) {
136
+ let verified = 0;
137
+ const missing = [];
138
+ for (const migration of plan) {
139
+ const targetPath = join(migrationsDir, migration.target);
140
+ try {
141
+ const stat = await lstat(targetPath);
142
+ if (!stat.isFile() || stat.isSymbolicLink()) {
143
+ throw new Error(`managed CMS migration must be a regular file: ${migration.target}`);
144
+ }
145
+ const targetHash = await sha256(new Uint8Array(await readFile(targetPath)));
146
+ if (targetHash !== migration.sha256) {
147
+ throw new Error(`CMS migration hash mismatch: ${migration.target}`);
148
+ }
149
+ verified += 1;
150
+ }
151
+ catch (error) {
152
+ if (error.code === 'ENOENT' && allowMissing) {
153
+ missing.push(migration);
154
+ continue;
155
+ }
156
+ throw error;
157
+ }
158
+ }
159
+ return { verified, missing };
160
+ }
161
+ async function stageNewFile(path, bytes) {
162
+ const temp = `${path}.tmp-${crypto.randomUUID()}`;
163
+ await writeFile(temp, bytes, { flag: 'wx' });
164
+ return temp;
165
+ }
166
+ async function stageManifest(path, manifest) {
167
+ const temp = `${path}.tmp-${crypto.randomUUID()}`;
168
+ await writeFile(temp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
169
+ return temp;
170
+ }
171
+ async function commitManifest(tempPath, manifestPath, currentManifest) {
172
+ if (currentManifest) {
173
+ const latest = await readManifest(manifestPath);
174
+ if (!latest || JSON.stringify(latest) !== JSON.stringify(currentManifest)) {
175
+ throw new Error('CMS migration manifest changed during sync');
176
+ }
177
+ await rename(tempPath, manifestPath);
178
+ return;
179
+ }
180
+ await link(tempPath, manifestPath);
181
+ }
182
+ function isInsideDirectory(directory, path) {
183
+ const fromDirectory = relative(directory, path);
184
+ return (fromDirectory === '' ||
185
+ (!fromDirectory.startsWith(`..${sep}`) && fromDirectory !== '..' && !isAbsolute(fromDirectory)));
186
+ }
187
+ async function resolveManifestPath(migrationsDir, override) {
188
+ if (!override)
189
+ return join(migrationsDir, MANIFEST_NAME);
190
+ const requested = resolve(override);
191
+ if (extname(requested).toLowerCase() !== '.json') {
192
+ throw new Error('CMS migration manifest override must be a .json file');
193
+ }
194
+ if (isInsideDirectory(migrationsDir, requested)) {
195
+ throw new Error('CMS migration manifest override must be outside migrationsDir');
196
+ }
197
+ const parent = await requireExistingDirectory(dirname(requested), 'manifest parent');
198
+ const resolvedPath = join(parent, basename(requested));
199
+ if (isInsideDirectory(migrationsDir, resolvedPath)) {
200
+ throw new Error('CMS migration manifest override must be outside migrationsDir');
201
+ }
202
+ return resolvedPath;
203
+ }
204
+ async function withManifestLock(manifestPath, action) {
205
+ const lockPath = `${manifestPath}.lock`;
206
+ try {
207
+ await writeFile(lockPath, `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`, {
208
+ flag: 'wx',
209
+ mode: 0o600,
210
+ });
211
+ }
212
+ catch (error) {
213
+ if (error.code === 'EEXIST') {
214
+ throw new Error(`another CMS migration operation is already active: ${lockPath}`);
215
+ }
216
+ throw error;
217
+ }
218
+ let result;
219
+ let operationError;
220
+ try {
221
+ result = await action();
222
+ }
223
+ catch (error) {
224
+ operationError = error;
225
+ }
226
+ let releaseError;
227
+ try {
228
+ await unlink(lockPath);
229
+ }
230
+ catch (error) {
231
+ releaseError = error;
232
+ }
233
+ if (operationError && releaseError) {
234
+ throw new AggregateError([operationError, releaseError], 'CMS migration operation failed and its receipt lock could not be released');
235
+ }
236
+ if (operationError)
237
+ throw operationError;
238
+ if (releaseError)
239
+ throw releaseError;
240
+ return result;
241
+ }
242
+ export async function syncCmsMigrations(options) {
243
+ assertStartSequence(options.startSequence);
244
+ const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir));
245
+ const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath);
246
+ return withManifestLock(manifestPath, async () => {
247
+ const contract = await packageContract();
248
+ const plan = await planMigrations(contract.sourceDir, options.startSequence);
249
+ const nextManifest = manifestFor(contract.packageVersion, options.startSequence, plan);
250
+ const currentManifest = await readManifest(manifestPath);
251
+ assertManifestCanAdvance(currentManifest, nextManifest);
252
+ await assertNoSequenceCollisions(migrationsDir, plan);
253
+ const preflight = await verifyTargets(migrationsDir, plan, true);
254
+ const staged = [];
255
+ let manifestTemp = null;
256
+ let verified = 0;
257
+ try {
258
+ for (const migration of preflight.missing) {
259
+ const targetPath = join(migrationsDir, migration.target);
260
+ staged.push({
261
+ targetPath,
262
+ tempPath: await stageNewFile(targetPath, migration.bytes),
263
+ });
264
+ }
265
+ const stagedManifest = await stageManifest(manifestPath, nextManifest);
266
+ manifestTemp = stagedManifest;
267
+ await commitStagedMigrationBatch(staged, async () => {
268
+ await assertNoSequenceCollisions(migrationsDir, plan);
269
+ const final = await verifyTargets(migrationsDir, plan, false);
270
+ verified = final.verified;
271
+ await commitManifest(stagedManifest, manifestPath, currentManifest);
272
+ });
273
+ return {
274
+ packageName: PACKAGE_NAME,
275
+ packageVersion: contract.packageVersion,
276
+ startSequence: options.startSequence,
277
+ managedFiles: plan.map(({ target }) => target),
278
+ written: preflight.missing.length,
279
+ verified,
280
+ manifestPath,
281
+ };
282
+ }
283
+ finally {
284
+ await Promise.all([
285
+ ...staged.map(({ tempPath }) => unlink(tempPath).catch(() => { })),
286
+ ...(manifestTemp ? [unlink(manifestTemp).catch(() => { })] : []),
287
+ ]);
288
+ }
289
+ });
290
+ }
291
+ export async function verifyCmsMigrations(options) {
292
+ const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir));
293
+ const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath);
294
+ return withManifestLock(manifestPath, async () => {
295
+ const manifest = await readManifest(manifestPath);
296
+ if (!manifest)
297
+ throw new Error(`CMS migration manifest is missing: ${manifestPath}`);
298
+ const contract = await packageContract();
299
+ if (manifest.packageVersion !== contract.packageVersion) {
300
+ throw new Error(`CMS migration manifest version ${manifest.packageVersion} does not match installed ${contract.packageVersion}`);
301
+ }
302
+ const plan = await planMigrations(contract.sourceDir, manifest.startSequence);
303
+ const expected = manifestFor(contract.packageVersion, manifest.startSequence, plan);
304
+ if (expected.files.length !== manifest.files.length ||
305
+ expected.files.some((entry, index) => !entriesEqual(entry, manifest.files[index]))) {
306
+ throw new Error('CMS migration manifest does not match the installed package contract');
307
+ }
308
+ await assertNoSequenceCollisions(migrationsDir, plan);
309
+ const result = await verifyTargets(migrationsDir, plan, false);
310
+ return {
311
+ packageName: PACKAGE_NAME,
312
+ packageVersion: contract.packageVersion,
313
+ startSequence: manifest.startSequence,
314
+ managedFiles: plan.map(({ target }) => target),
315
+ written: 0,
316
+ verified: result.verified,
317
+ manifestPath,
318
+ };
319
+ });
320
+ }
321
+ //# sourceMappingURL=migration-vendor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-vendor.js","sourceRoot":"","sources":["../src/migration-vendor.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@growth-labs/cms",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
+ "bin": {
6
+ "growth-labs-cms-migrations": "./dist/cli/migrations.js"
7
+ },
5
8
  "types": "./dist/index.d.ts",
6
9
  "exports": {
7
10
  ".": {
@@ -24,6 +27,10 @@
24
27
  "types": "./dist/integration/index.d.ts",
25
28
  "import": "./dist/integration/index.js"
26
29
  },
30
+ "./migration-vendor": {
31
+ "types": "./dist/migration-vendor.d.ts",
32
+ "import": "./dist/migration-vendor.js"
33
+ },
27
34
  "./ui": {
28
35
  "types": "./dist/ui/components/CmsApp.d.ts",
29
36
  "import": "./dist/ui/components/CmsApp.js"
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { syncCmsMigrations, verifyCmsMigrations } from '../migration-vendor.js'
4
+
5
+ interface ParsedArgs {
6
+ command: 'sync' | 'verify'
7
+ migrationsDir: string
8
+ startSequence?: number
9
+ manifestPath?: string
10
+ }
11
+
12
+ function usage(): string {
13
+ return [
14
+ 'Usage:',
15
+ ' growth-labs-cms-migrations sync --dir <migrations-dir> --start <positive-integer>',
16
+ ' growth-labs-cms-migrations verify --dir <migrations-dir>',
17
+ '',
18
+ 'Optional for both commands: --manifest <receipt-path>',
19
+ ].join('\n')
20
+ }
21
+
22
+ function requireValue(argv: string[], index: number, flag: string): string {
23
+ const value = argv[index + 1]
24
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`)
25
+ return value
26
+ }
27
+
28
+ function parseArgs(argv: string[]): ParsedArgs {
29
+ const command = argv[0]
30
+ if (command !== 'sync' && command !== 'verify') throw new Error(usage())
31
+ let migrationsDir: string | undefined
32
+ let startSequence: number | undefined
33
+ let manifestPath: string | undefined
34
+
35
+ for (let index = 1; index < argv.length; index += 1) {
36
+ const flag = argv[index]
37
+ if (flag === '--dir') {
38
+ migrationsDir = requireValue(argv, index, flag)
39
+ index += 1
40
+ continue
41
+ }
42
+ if (flag === '--start') {
43
+ startSequence = Number(requireValue(argv, index, flag))
44
+ index += 1
45
+ continue
46
+ }
47
+ if (flag === '--manifest') {
48
+ manifestPath = requireValue(argv, index, flag)
49
+ index += 1
50
+ continue
51
+ }
52
+ throw new Error(`unknown argument: ${flag}\n\n${usage()}`)
53
+ }
54
+
55
+ if (!migrationsDir) throw new Error(`--dir is required\n\n${usage()}`)
56
+ if (command === 'sync' && startSequence === undefined) {
57
+ throw new Error(`--start is required for sync\n\n${usage()}`)
58
+ }
59
+ if (command === 'verify' && startSequence !== undefined) {
60
+ throw new Error('--start is read from the committed manifest during verify')
61
+ }
62
+ return { command, migrationsDir, startSequence, manifestPath }
63
+ }
64
+
65
+ async function main(): Promise<void> {
66
+ const args = parseArgs(process.argv.slice(2))
67
+ const result =
68
+ args.command === 'sync'
69
+ ? await syncCmsMigrations({
70
+ migrationsDir: args.migrationsDir,
71
+ startSequence: args.startSequence as number,
72
+ manifestPath: args.manifestPath,
73
+ })
74
+ : await verifyCmsMigrations({
75
+ migrationsDir: args.migrationsDir,
76
+ manifestPath: args.manifestPath,
77
+ })
78
+ process.stdout.write(`${JSON.stringify({ ok: true, command: args.command, ...result })}\n`)
79
+ }
80
+
81
+ main().catch((error: unknown) => {
82
+ const message = error instanceof Error ? error.message : String(error)
83
+ process.stderr.write(`growth-labs-cms-migrations: ${message}\n`)
84
+ process.exitCode = 1
85
+ })
@@ -0,0 +1,100 @@
1
+ import { link, unlink } from 'node:fs/promises'
2
+
3
+ export interface StagedMigrationVendorFile {
4
+ tempPath: string
5
+ targetPath: string
6
+ }
7
+
8
+ export interface MigrationVendorFileOperations {
9
+ link(source: string, target: string): Promise<void>
10
+ unlink(path: string): Promise<void>
11
+ }
12
+
13
+ const DEFAULT_OPERATIONS: MigrationVendorFileOperations = { link, unlink }
14
+
15
+ async function removePaths(
16
+ paths: string[],
17
+ operations: MigrationVendorFileOperations,
18
+ ): Promise<unknown[]> {
19
+ const errors: unknown[] = []
20
+ for (const path of [...paths].reverse()) {
21
+ try {
22
+ await operations.unlink(path)
23
+ } catch (error) {
24
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') errors.push(error)
25
+ }
26
+ }
27
+ return errors
28
+ }
29
+
30
+ export async function rollbackNewFiles(
31
+ paths: string[],
32
+ operations: MigrationVendorFileOperations = DEFAULT_OPERATIONS,
33
+ ): Promise<void> {
34
+ const errors = await removePaths(paths, operations)
35
+ if (errors.length > 0) {
36
+ throw new AggregateError(errors, 'failed to roll back newly linked CMS migration files')
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Link every pre-staged file without replacing an existing directory entry.
42
+ * A later failure removes all targets linked by this attempt and every temp.
43
+ */
44
+ export async function commitNewFilesAtomically(
45
+ files: StagedMigrationVendorFile[],
46
+ operations: MigrationVendorFileOperations = DEFAULT_OPERATIONS,
47
+ ): Promise<string[]> {
48
+ const committed: string[] = []
49
+ let failure: unknown
50
+
51
+ try {
52
+ for (const file of files) {
53
+ await operations.link(file.tempPath, file.targetPath)
54
+ committed.push(file.targetPath)
55
+ }
56
+ } catch (error) {
57
+ failure = error
58
+ }
59
+
60
+ const cleanupErrors = await removePaths(
61
+ files.map((file) => file.tempPath),
62
+ operations,
63
+ )
64
+ if (failure) {
65
+ const rollbackErrors = await removePaths(committed, operations)
66
+ const errors = [failure, ...cleanupErrors, ...rollbackErrors].filter(
67
+ (error): error is NonNullable<unknown> => error !== undefined,
68
+ )
69
+ const primary = failure instanceof Error ? `: ${failure.message}` : ''
70
+ throw new AggregateError(errors, `CMS migration file commit failed${primary}`)
71
+ }
72
+ // A hard-linked target remains valid when unlinking only its staged temp
73
+ // fails. The caller's outer finally retries temp cleanup; rolling targets
74
+ // back here would turn a harmless leftover temp into data loss.
75
+
76
+ return committed
77
+ }
78
+
79
+ /** Commit staged SQL, then run the receipt/final verification boundary. */
80
+ export async function commitStagedMigrationBatch(
81
+ files: StagedMigrationVendorFile[],
82
+ finalize: () => Promise<void>,
83
+ operations: MigrationVendorFileOperations = DEFAULT_OPERATIONS,
84
+ ): Promise<string[]> {
85
+ const committed = await commitNewFilesAtomically(files, operations)
86
+ try {
87
+ await finalize()
88
+ return committed
89
+ } catch (error) {
90
+ try {
91
+ await rollbackNewFiles(committed, operations)
92
+ } catch (rollbackError) {
93
+ throw new AggregateError(
94
+ [error, rollbackError],
95
+ 'CMS migration batch finalization failed and rollback was incomplete',
96
+ )
97
+ }
98
+ throw error
99
+ }
100
+ }
@@ -0,0 +1,450 @@
1
+ import {
2
+ link,
3
+ lstat,
4
+ readdir,
5
+ readFile,
6
+ realpath,
7
+ rename,
8
+ unlink,
9
+ writeFile,
10
+ } from 'node:fs/promises'
11
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import {
14
+ commitStagedMigrationBatch,
15
+ type StagedMigrationVendorFile,
16
+ } from './migration-vendor-files.js'
17
+
18
+ const PACKAGE_NAME = '@growth-labs/cms'
19
+ const MANIFEST_NAME = '.growth-labs-cms-migrations.json'
20
+ const MANIFEST_SCHEMA_VERSION = 1
21
+ const MIGRATION_FILE = /^\d{4}_[a-z0-9][a-z0-9_-]*\.sql$/
22
+
23
+ interface PackageJson {
24
+ name?: string
25
+ version?: string
26
+ }
27
+
28
+ interface PlannedMigration {
29
+ source: string
30
+ target: string
31
+ sequence: number
32
+ sha256: string
33
+ bytes: Uint8Array
34
+ }
35
+
36
+ interface CmsMigrationManifestEntry {
37
+ source: string
38
+ target: string
39
+ sha256: string
40
+ }
41
+
42
+ interface CmsMigrationManifest {
43
+ schemaVersion: 1
44
+ packageName: typeof PACKAGE_NAME
45
+ packageVersion: string
46
+ startSequence: number
47
+ files: CmsMigrationManifestEntry[]
48
+ }
49
+
50
+ export interface SyncCmsMigrationsOptions {
51
+ /** Existing consumer-owned Wrangler migrations directory. */
52
+ migrationsDir: string
53
+ /** First unused sequence number reserved for the package migrations. */
54
+ startSequence: number
55
+ /** Override only when a consumer keeps the receipt outside migrationsDir. */
56
+ manifestPath?: string
57
+ }
58
+
59
+ export interface VerifyCmsMigrationsOptions {
60
+ migrationsDir: string
61
+ manifestPath?: string
62
+ }
63
+
64
+ export interface CmsMigrationVendorResult {
65
+ packageName: typeof PACKAGE_NAME
66
+ packageVersion: string
67
+ startSequence: number
68
+ managedFiles: string[]
69
+ written: number
70
+ verified: number
71
+ manifestPath: string
72
+ }
73
+
74
+ interface PackageContract {
75
+ packageVersion: string
76
+ sourceDir: string
77
+ }
78
+
79
+ async function packageContract(): Promise<PackageContract> {
80
+ const packageRoot = fileURLToPath(new URL('..', import.meta.url))
81
+ const packageJsonPath = join(packageRoot, 'package.json')
82
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as PackageJson
83
+ if (packageJson.name !== PACKAGE_NAME || !packageJson.version) {
84
+ throw new Error('installed CMS package metadata is invalid')
85
+ }
86
+ return {
87
+ packageVersion: packageJson.version,
88
+ sourceDir: join(packageRoot, 'migrations'),
89
+ }
90
+ }
91
+
92
+ async function sha256(bytes: Uint8Array): Promise<string> {
93
+ const input = new Uint8Array(bytes.byteLength)
94
+ input.set(bytes)
95
+ const digest = await crypto.subtle.digest('SHA-256', input.buffer)
96
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('')
97
+ }
98
+
99
+ async function requireExistingDirectory(path: string, label = 'migrationsDir'): Promise<string> {
100
+ let stat: Awaited<ReturnType<typeof lstat>>
101
+ try {
102
+ stat = await lstat(path)
103
+ } catch {
104
+ throw new Error(`${label} must be an existing directory: ${path}`)
105
+ }
106
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
107
+ throw new Error(`${label} must be an existing non-symlink directory: ${path}`)
108
+ }
109
+ return realpath(path)
110
+ }
111
+
112
+ function assertStartSequence(value: number): void {
113
+ if (!Number.isSafeInteger(value) || value < 1) {
114
+ throw new Error('startSequence must be a positive integer')
115
+ }
116
+ }
117
+
118
+ function sequencePrefix(sequence: number): string {
119
+ return String(sequence).padStart(4, '0')
120
+ }
121
+
122
+ async function planMigrations(
123
+ sourceDir: string,
124
+ startSequence: number,
125
+ ): Promise<PlannedMigration[]> {
126
+ assertStartSequence(startSequence)
127
+ const sourceNames = (await readdir(sourceDir)).filter((name) => MIGRATION_FILE.test(name)).sort()
128
+ if (sourceNames.length === 0) throw new Error('installed CMS package has no migration SQL')
129
+ if (startSequence + sourceNames.length - 1 > 9999) {
130
+ throw new Error('reserved CMS migration sequence exceeds 9999')
131
+ }
132
+
133
+ return Promise.all(
134
+ sourceNames.map(async (source, index) => {
135
+ const sequence = startSequence + index
136
+ const suffix = source.replace(/^\d{4}_/, '')
137
+ const target = `${sequencePrefix(sequence)}_growth_labs_cms_${suffix}`
138
+ const bytes = new Uint8Array(await readFile(join(sourceDir, source)))
139
+ return { source, target, sequence, sha256: await sha256(bytes), bytes }
140
+ }),
141
+ )
142
+ }
143
+
144
+ function manifestFor(
145
+ packageVersion: string,
146
+ startSequence: number,
147
+ plan: PlannedMigration[],
148
+ ): CmsMigrationManifest {
149
+ return {
150
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
151
+ packageName: PACKAGE_NAME,
152
+ packageVersion,
153
+ startSequence,
154
+ files: plan.map(({ source, target, sha256: hash }) => ({ source, target, sha256: hash })),
155
+ }
156
+ }
157
+
158
+ async function readManifest(path: string): Promise<CmsMigrationManifest | null> {
159
+ try {
160
+ const stat = await lstat(path)
161
+ if (!stat.isFile() || stat.isSymbolicLink()) {
162
+ throw new Error(`CMS migration manifest must be a regular file: ${path}`)
163
+ }
164
+ const parsed = JSON.parse(await readFile(path, 'utf8')) as Partial<CmsMigrationManifest>
165
+ if (
166
+ parsed.schemaVersion !== MANIFEST_SCHEMA_VERSION ||
167
+ parsed.packageName !== PACKAGE_NAME ||
168
+ !parsed.packageVersion ||
169
+ !Number.isSafeInteger(parsed.startSequence) ||
170
+ (parsed.startSequence as number) < 1 ||
171
+ !Array.isArray(parsed.files)
172
+ ) {
173
+ throw new Error(`CMS migration manifest is invalid: ${path}`)
174
+ }
175
+ for (const entry of parsed.files) {
176
+ if (
177
+ !entry ||
178
+ typeof entry.source !== 'string' ||
179
+ typeof entry.target !== 'string' ||
180
+ !MIGRATION_FILE.test(entry.source) ||
181
+ !MIGRATION_FILE.test(entry.target) ||
182
+ !/^([a-f0-9]{64})$/.test(entry.sha256)
183
+ ) {
184
+ throw new Error(`CMS migration manifest contains an invalid file entry: ${path}`)
185
+ }
186
+ }
187
+ return parsed as CmsMigrationManifest
188
+ } catch (error) {
189
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
190
+ throw error
191
+ }
192
+ }
193
+
194
+ function entriesEqual(left: CmsMigrationManifestEntry, right: CmsMigrationManifestEntry): boolean {
195
+ return (
196
+ left.source === right.source && left.target === right.target && left.sha256 === right.sha256
197
+ )
198
+ }
199
+
200
+ function assertManifestCanAdvance(
201
+ current: CmsMigrationManifest | null,
202
+ next: CmsMigrationManifest,
203
+ ): void {
204
+ if (!current) return
205
+ if (current.startSequence !== next.startSequence) {
206
+ throw new Error(
207
+ `CMS migration manifest reserves startSequence ${current.startSequence}; received ${next.startSequence}`,
208
+ )
209
+ }
210
+ if (current.files.length > next.files.length) {
211
+ throw new Error('installed CMS package removed migrations recorded by the consumer manifest')
212
+ }
213
+ for (const [index, entry] of current.files.entries()) {
214
+ const nextEntry = next.files[index]
215
+ if (!nextEntry || !entriesEqual(entry, nextEntry)) {
216
+ throw new Error(`installed CMS migration changed after vendoring: ${entry.source}`)
217
+ }
218
+ }
219
+ }
220
+
221
+ async function assertNoSequenceCollisions(
222
+ migrationsDir: string,
223
+ plan: PlannedMigration[],
224
+ ): Promise<void> {
225
+ const existing = await readdir(migrationsDir)
226
+ for (const migration of plan) {
227
+ const prefix = `${sequencePrefix(migration.sequence)}_`
228
+ const collisions = existing.filter(
229
+ (name) => name.endsWith('.sql') && name.startsWith(prefix) && name !== migration.target,
230
+ )
231
+ if (collisions.length > 0) {
232
+ throw new Error(
233
+ `reserved sequence ${sequencePrefix(migration.sequence)} collides with ${collisions.join(', ')}`,
234
+ )
235
+ }
236
+ }
237
+ }
238
+
239
+ async function verifyTargets(
240
+ migrationsDir: string,
241
+ plan: PlannedMigration[],
242
+ allowMissing: boolean,
243
+ ): Promise<{ verified: number; missing: PlannedMigration[] }> {
244
+ let verified = 0
245
+ const missing: PlannedMigration[] = []
246
+ for (const migration of plan) {
247
+ const targetPath = join(migrationsDir, migration.target)
248
+ try {
249
+ const stat = await lstat(targetPath)
250
+ if (!stat.isFile() || stat.isSymbolicLink()) {
251
+ throw new Error(`managed CMS migration must be a regular file: ${migration.target}`)
252
+ }
253
+ const targetHash = await sha256(new Uint8Array(await readFile(targetPath)))
254
+ if (targetHash !== migration.sha256) {
255
+ throw new Error(`CMS migration hash mismatch: ${migration.target}`)
256
+ }
257
+ verified += 1
258
+ } catch (error) {
259
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' && allowMissing) {
260
+ missing.push(migration)
261
+ continue
262
+ }
263
+ throw error
264
+ }
265
+ }
266
+ return { verified, missing }
267
+ }
268
+
269
+ async function stageNewFile(path: string, bytes: Uint8Array): Promise<string> {
270
+ const temp = `${path}.tmp-${crypto.randomUUID()}`
271
+ await writeFile(temp, bytes, { flag: 'wx' })
272
+ return temp
273
+ }
274
+
275
+ async function stageManifest(path: string, manifest: CmsMigrationManifest): Promise<string> {
276
+ const temp = `${path}.tmp-${crypto.randomUUID()}`
277
+ await writeFile(temp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
278
+ return temp
279
+ }
280
+
281
+ async function commitManifest(
282
+ tempPath: string,
283
+ manifestPath: string,
284
+ currentManifest: CmsMigrationManifest | null,
285
+ ): Promise<void> {
286
+ if (currentManifest) {
287
+ const latest = await readManifest(manifestPath)
288
+ if (!latest || JSON.stringify(latest) !== JSON.stringify(currentManifest)) {
289
+ throw new Error('CMS migration manifest changed during sync')
290
+ }
291
+ await rename(tempPath, manifestPath)
292
+ return
293
+ }
294
+ await link(tempPath, manifestPath)
295
+ }
296
+
297
+ function isInsideDirectory(directory: string, path: string): boolean {
298
+ const fromDirectory = relative(directory, path)
299
+ return (
300
+ fromDirectory === '' ||
301
+ (!fromDirectory.startsWith(`..${sep}`) && fromDirectory !== '..' && !isAbsolute(fromDirectory))
302
+ )
303
+ }
304
+
305
+ async function resolveManifestPath(migrationsDir: string, override?: string): Promise<string> {
306
+ if (!override) return join(migrationsDir, MANIFEST_NAME)
307
+ const requested = resolve(override)
308
+ if (extname(requested).toLowerCase() !== '.json') {
309
+ throw new Error('CMS migration manifest override must be a .json file')
310
+ }
311
+ if (isInsideDirectory(migrationsDir, requested)) {
312
+ throw new Error('CMS migration manifest override must be outside migrationsDir')
313
+ }
314
+ const parent = await requireExistingDirectory(dirname(requested), 'manifest parent')
315
+ const resolvedPath = join(parent, basename(requested))
316
+ if (isInsideDirectory(migrationsDir, resolvedPath)) {
317
+ throw new Error('CMS migration manifest override must be outside migrationsDir')
318
+ }
319
+ return resolvedPath
320
+ }
321
+
322
+ async function withManifestLock<T>(manifestPath: string, action: () => Promise<T>): Promise<T> {
323
+ const lockPath = `${manifestPath}.lock`
324
+ try {
325
+ await writeFile(lockPath, `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`, {
326
+ flag: 'wx',
327
+ mode: 0o600,
328
+ })
329
+ } catch (error) {
330
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
331
+ throw new Error(`another CMS migration operation is already active: ${lockPath}`)
332
+ }
333
+ throw error
334
+ }
335
+
336
+ let result: T | undefined
337
+ let operationError: unknown
338
+ try {
339
+ result = await action()
340
+ } catch (error) {
341
+ operationError = error
342
+ }
343
+ let releaseError: unknown
344
+ try {
345
+ await unlink(lockPath)
346
+ } catch (error) {
347
+ releaseError = error
348
+ }
349
+ if (operationError && releaseError) {
350
+ throw new AggregateError(
351
+ [operationError, releaseError],
352
+ 'CMS migration operation failed and its receipt lock could not be released',
353
+ )
354
+ }
355
+ if (operationError) throw operationError
356
+ if (releaseError) throw releaseError
357
+ return result as T
358
+ }
359
+
360
+ export async function syncCmsMigrations(
361
+ options: SyncCmsMigrationsOptions,
362
+ ): Promise<CmsMigrationVendorResult> {
363
+ assertStartSequence(options.startSequence)
364
+ const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir))
365
+ const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath)
366
+ return withManifestLock(manifestPath, async () => {
367
+ const contract = await packageContract()
368
+ const plan = await planMigrations(contract.sourceDir, options.startSequence)
369
+ const nextManifest = manifestFor(contract.packageVersion, options.startSequence, plan)
370
+ const currentManifest = await readManifest(manifestPath)
371
+ assertManifestCanAdvance(currentManifest, nextManifest)
372
+ await assertNoSequenceCollisions(migrationsDir, plan)
373
+ const preflight = await verifyTargets(migrationsDir, plan, true)
374
+ const staged: StagedMigrationVendorFile[] = []
375
+ let manifestTemp: string | null = null
376
+ let verified = 0
377
+
378
+ try {
379
+ for (const migration of preflight.missing) {
380
+ const targetPath = join(migrationsDir, migration.target)
381
+ staged.push({
382
+ targetPath,
383
+ tempPath: await stageNewFile(targetPath, migration.bytes),
384
+ })
385
+ }
386
+ const stagedManifest = await stageManifest(manifestPath, nextManifest)
387
+ manifestTemp = stagedManifest
388
+ await commitStagedMigrationBatch(staged, async () => {
389
+ await assertNoSequenceCollisions(migrationsDir, plan)
390
+ const final = await verifyTargets(migrationsDir, plan, false)
391
+ verified = final.verified
392
+ await commitManifest(stagedManifest, manifestPath, currentManifest)
393
+ })
394
+
395
+ return {
396
+ packageName: PACKAGE_NAME,
397
+ packageVersion: contract.packageVersion,
398
+ startSequence: options.startSequence,
399
+ managedFiles: plan.map(({ target }) => target),
400
+ written: preflight.missing.length,
401
+ verified,
402
+ manifestPath,
403
+ }
404
+ } finally {
405
+ await Promise.all([
406
+ ...staged.map(({ tempPath }) => unlink(tempPath).catch(() => {})),
407
+ ...(manifestTemp ? [unlink(manifestTemp).catch(() => {})] : []),
408
+ ])
409
+ }
410
+ })
411
+ }
412
+
413
+ export async function verifyCmsMigrations(
414
+ options: VerifyCmsMigrationsOptions,
415
+ ): Promise<CmsMigrationVendorResult> {
416
+ const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir))
417
+ const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath)
418
+ return withManifestLock(manifestPath, async () => {
419
+ const manifest = await readManifest(manifestPath)
420
+ if (!manifest) throw new Error(`CMS migration manifest is missing: ${manifestPath}`)
421
+ const contract = await packageContract()
422
+ if (manifest.packageVersion !== contract.packageVersion) {
423
+ throw new Error(
424
+ `CMS migration manifest version ${manifest.packageVersion} does not match installed ${contract.packageVersion}`,
425
+ )
426
+ }
427
+ const plan = await planMigrations(contract.sourceDir, manifest.startSequence)
428
+ const expected = manifestFor(contract.packageVersion, manifest.startSequence, plan)
429
+ if (
430
+ expected.files.length !== manifest.files.length ||
431
+ expected.files.some(
432
+ (entry, index) => !entriesEqual(entry, manifest.files[index] as CmsMigrationManifestEntry),
433
+ )
434
+ ) {
435
+ throw new Error('CMS migration manifest does not match the installed package contract')
436
+ }
437
+ await assertNoSequenceCollisions(migrationsDir, plan)
438
+ const result = await verifyTargets(migrationsDir, plan, false)
439
+
440
+ return {
441
+ packageName: PACKAGE_NAME,
442
+ packageVersion: contract.packageVersion,
443
+ startSequence: manifest.startSequence,
444
+ managedFiles: plan.map(({ target }) => target),
445
+ written: 0,
446
+ verified: result.verified,
447
+ manifestPath,
448
+ }
449
+ })
450
+ }