@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.15

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.
Files changed (169) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +4 -1
  3. package/dist/package.json +100 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_engine.d.mts +162 -0
  21. package/dist/src/baseline/privacy_engine.mjs +1162 -0
  22. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  23. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  24. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  25. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  26. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  27. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  28. package/dist/src/cli/arguments.d.mts +43 -0
  29. package/dist/src/cli/arguments.mjs +135 -0
  30. package/dist/src/cli/guided.d.mts +51 -0
  31. package/dist/src/cli/guided.mjs +713 -0
  32. package/dist/src/cli/init_command.d.mts +21 -0
  33. package/dist/src/cli/init_command.mjs +90 -0
  34. package/dist/src/cli/project_commands.d.mts +157 -0
  35. package/dist/src/cli/project_commands.mjs +183 -0
  36. package/dist/src/cli/rehearsal.d.mts +7 -0
  37. package/dist/src/cli/rehearsal.mjs +706 -0
  38. package/dist/src/cli/renderers.d.mts +299 -0
  39. package/dist/src/cli/renderers.mjs +554 -0
  40. package/dist/src/cli/runtime_commands.d.mts +265 -0
  41. package/dist/src/cli/runtime_commands.mjs +599 -0
  42. package/dist/src/cli/source_commands.d.mts +432 -0
  43. package/dist/src/cli/source_commands.mjs +299 -0
  44. package/dist/src/cli/terminal.d.mts +59 -0
  45. package/dist/src/cli/terminal.mjs +230 -0
  46. package/dist/src/identity/claim.d.mts +144 -0
  47. package/dist/src/identity/claim.mjs +947 -0
  48. package/dist/src/identity/provider_configuration.d.mts +37 -0
  49. package/dist/src/identity/provider_configuration.mjs +132 -0
  50. package/dist/src/identity/storage.d.mts +21 -0
  51. package/dist/src/identity/storage.mjs +122 -0
  52. package/dist/src/index.d.mts +4 -0
  53. package/dist/src/index.mjs +3 -0
  54. package/dist/src/project/configuration.d.mts +181 -0
  55. package/dist/src/project/configuration.mjs +958 -0
  56. package/dist/src/project/install_scaffold.d.mts +42 -0
  57. package/dist/src/project/install_scaffold.mjs +263 -0
  58. package/dist/src/project/setup.d.mts +87 -0
  59. package/dist/src/project/setup.mjs +414 -0
  60. package/dist/src/project/support_report.d.mts +88 -0
  61. package/dist/src/project/support_report.mjs +83 -0
  62. package/dist/src/runtime/cleanup.d.mts +85 -0
  63. package/dist/src/runtime/cleanup.mjs +311 -0
  64. package/dist/src/runtime/migration_history.d.mts +51 -0
  65. package/dist/src/runtime/migration_history.mjs +170 -0
  66. package/dist/src/runtime/plan.d.mts +134 -0
  67. package/dist/src/runtime/plan.mjs +636 -0
  68. package/dist/src/runtime/policy.d.mts +66 -0
  69. package/dist/src/runtime/policy.mjs +369 -0
  70. package/dist/src/runtime/restore.d.mts +22 -0
  71. package/dist/src/runtime/restore.mjs +276 -0
  72. package/dist/src/runtime/service_environment.d.mts +11 -0
  73. package/dist/src/runtime/service_environment.mjs +58 -0
  74. package/dist/src/runtime/topology.d.mts +51 -0
  75. package/dist/src/runtime/topology.mjs +128 -0
  76. package/dist/src/shared/diagnostics.d.mts +97 -0
  77. package/dist/src/shared/diagnostics.mjs +170 -0
  78. package/dist/src/shared/human_output.d.mts +2 -0
  79. package/{src → dist/src}/shared/human_output.mjs +1 -3
  80. package/dist/src/shared/node_runtime.d.mts +14 -0
  81. package/dist/src/shared/node_runtime.mjs +25 -0
  82. package/dist/src/shared/operation_guard.d.mts +22 -0
  83. package/dist/src/shared/operation_guard.mjs +150 -0
  84. package/dist/src/shared/process_environment.d.mts +15 -0
  85. package/dist/src/shared/process_environment.mjs +50 -0
  86. package/dist/src/source/access.d.mts +165 -0
  87. package/dist/src/source/access.mjs +602 -0
  88. package/dist/src/source/asset_transfer.d.mts +27 -0
  89. package/dist/src/source/asset_transfer.mjs +202 -0
  90. package/dist/src/source/baseline.d.mts +66 -0
  91. package/dist/src/source/baseline.mjs +372 -0
  92. package/dist/src/source/external_reader.d.mts +42 -0
  93. package/dist/src/source/external_reader.mjs +278 -0
  94. package/dist/src/source/postgresql_access.d.mts +86 -0
  95. package/dist/src/source/postgresql_access.mjs +454 -0
  96. package/dist/src/source/privacy_paths.d.mts +10 -0
  97. package/dist/src/source/privacy_paths.mjs +27 -0
  98. package/dist/src/targets/driver.d.mts +2 -0
  99. package/dist/src/targets/driver.mjs +13 -0
  100. package/dist/src/targets/postgresql.d.mts +5 -0
  101. package/dist/src/targets/postgresql.mjs +641 -0
  102. package/dist/src/targets/supabase.d.mts +8 -0
  103. package/dist/src/targets/supabase.mjs +659 -0
  104. package/dist/src/targets/supabase_environment.d.mts +49 -0
  105. package/dist/src/targets/supabase_environment.mjs +162 -0
  106. package/dist/src/targets/target.d.mts +33 -0
  107. package/dist/src/targets/target.mjs +39 -0
  108. package/docs/README.md +1 -0
  109. package/docs/architecture.md +17 -9
  110. package/docs/configuration.md +49 -8
  111. package/docs/getting-started.md +28 -0
  112. package/docs/index.md +1 -0
  113. package/docs/production-source.md +43 -7
  114. package/docs/releasing.md +5 -3
  115. package/docs/roadmap.md +3 -2
  116. package/docs/sanitization.md +415 -14
  117. package/docs/standalone-workflow.md +12 -6
  118. package/docs/troubleshooting.md +5 -1
  119. package/docs/tutorial.md +5 -0
  120. package/docs/typescript.md +87 -0
  121. package/package.json +35 -29
  122. package/scripts/install/create_root_config.mjs +0 -36
  123. package/src/README.md +0 -17
  124. package/src/application/session.mjs +0 -435
  125. package/src/baseline/artifact.mjs +0 -684
  126. package/src/baseline/builder.mjs +0 -164
  127. package/src/baseline/input_discovery.mjs +0 -155
  128. package/src/baseline/policy_review.mjs +0 -92
  129. package/src/baseline/preparation.mjs +0 -323
  130. package/src/baseline/privacy_engine.mjs +0 -952
  131. package/src/baseline/sanitization_policy.mjs +0 -299
  132. package/src/baseline/schema_snapshot.mjs +0 -113
  133. package/src/cli/arguments.mjs +0 -126
  134. package/src/cli/guided.mjs +0 -814
  135. package/src/cli/init_command.mjs +0 -94
  136. package/src/cli/rehearsal.mjs +0 -970
  137. package/src/cli/renderers.mjs +0 -626
  138. package/src/cli/runtime_commands.mjs +0 -707
  139. package/src/cli/source_commands.mjs +0 -347
  140. package/src/cli/terminal.mjs +0 -275
  141. package/src/identity/claim.mjs +0 -1163
  142. package/src/identity/provider_configuration.mjs +0 -177
  143. package/src/identity/storage.mjs +0 -165
  144. package/src/project/configuration.mjs +0 -1337
  145. package/src/project/install_scaffold.mjs +0 -291
  146. package/src/project/setup.mjs +0 -466
  147. package/src/project/support_report.mjs +0 -105
  148. package/src/runtime/cleanup.mjs +0 -381
  149. package/src/runtime/migration_history.mjs +0 -220
  150. package/src/runtime/plan.mjs +0 -722
  151. package/src/runtime/policy.mjs +0 -438
  152. package/src/runtime/restore.mjs +0 -337
  153. package/src/runtime/service_environment.mjs +0 -82
  154. package/src/runtime/topology.mjs +0 -177
  155. package/src/shared/diagnostics.mjs +0 -207
  156. package/src/shared/node_runtime.mjs +0 -31
  157. package/src/shared/operation_guard.mjs +0 -162
  158. package/src/shared/process_environment.mjs +0 -68
  159. package/src/source/access.mjs +0 -753
  160. package/src/source/asset_transfer.mjs +0 -207
  161. package/src/source/baseline.mjs +0 -482
  162. package/src/source/external_reader.mjs +0 -255
  163. package/src/source/postgresql_access.mjs +0 -581
  164. package/src/source/privacy_paths.mjs +0 -38
  165. package/src/targets/postgresql.mjs +0 -808
  166. package/src/targets/supabase.mjs +0 -840
  167. package/src/targets/supabase_environment.mjs +0 -229
  168. package/src/targets/target.mjs +0 -66
  169. /package/{src/project → types}/configuration.d.mts +0 -0
@@ -1,684 +0,0 @@
1
- /**
2
- * Purpose: Write, verify, and atomically activate immutable sanitized Rehearsal
3
- * baseline generations. Do not run directly; this module is reusable script
4
- * infrastructure.
5
- */
6
-
7
- import { createHash, randomBytes } from "node:crypto";
8
- import {
9
- chmod,
10
- lstat,
11
- mkdir,
12
- open,
13
- readFile,
14
- readdir,
15
- readlink,
16
- rename,
17
- rm,
18
- symlink,
19
- } from "node:fs/promises";
20
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
21
- import { createMigrationReplayReceipt } from "../runtime/migration_history.mjs";
22
-
23
- const generationPattern = /^\d{8}T\d{6}Z-[a-f0-9]{12}$/u;
24
- const buildPrefix = ".building-";
25
- const generationsDirectoryName = "generations";
26
- const currentLinkName = "current";
27
- const migrationNamePattern = /^\d{14}_[a-z0-9_]+\.sql$/u;
28
- const storageBucketPattern = /^[a-z0-9][a-z0-9.-]{0,99}$/u;
29
- const maximumAssetBytes = 50 * 1024 * 1024;
30
- const maximumTotalAssetBytes = 2 * 1024 * 1024 * 1024;
31
- const relationPattern = /^(?:[a-z][a-z0-9_]{0,62}\.)?[a-z][a-z0-9_]{0,62}$/u;
32
- const relationKey = (schema, table) =>
33
- schema === "public" ? table : `${schema}.${table}`;
34
-
35
- const canonicalJson = (value) => `${JSON.stringify(value, null, "\t")}\n`;
36
-
37
- const assertGenerationId = (value) => {
38
- if (!generationPattern.test(value)) {
39
- throw new Error(
40
- `Invalid Rehearsal baseline generation identifier: ${value}`,
41
- );
42
- }
43
- return value;
44
- };
45
-
46
- const assertArtifactRoot = (artifactRoot) => {
47
- const resolved = resolve(artifactRoot);
48
- if (basename(resolved) !== ".rehearsal" || resolved === sep) {
49
- throw new Error(
50
- `Refusing an unsafe Rehearsal artifact root: ${artifactRoot}`,
51
- );
52
- }
53
- return resolved;
54
- };
55
-
56
- const pathInside = (parent, child) => {
57
- const childRelative = relative(parent, child);
58
- return (
59
- childRelative !== "" &&
60
- !childRelative.startsWith(`..${sep}`) &&
61
- childRelative !== ".." &&
62
- !childRelative.startsWith(sep)
63
- );
64
- };
65
-
66
- const assertInsideRoot = (root, target) => {
67
- if (!pathInside(root, target)) {
68
- throw new Error(`Rehearsal artifact path escaped its root: ${target}`);
69
- }
70
- return target;
71
- };
72
-
73
- const writeImmutableFile = async (path, content) => {
74
- const handle = await open(path, "wx", 0o600);
75
- try {
76
- await handle.writeFile(content);
77
- await handle.sync();
78
- } finally {
79
- await handle.close();
80
- }
81
- await chmod(path, 0o400);
82
- };
83
-
84
- const assetRelativePath = (bucket, objectPath) => {
85
- if (!storageBucketPattern.test(bucket)) {
86
- throw new Error(`Invalid Rehearsal Storage bucket: ${bucket}.`);
87
- }
88
- const segments = objectPath.split("/");
89
- if (
90
- !objectPath ||
91
- segments.some((segment) => !segment || segment === "." || segment === "..")
92
- ) {
93
- throw new Error(`Invalid Rehearsal Storage object path: ${objectPath}.`);
94
- }
95
- return join(
96
- "storage",
97
- encodeURIComponent(bucket),
98
- ...segments.map(encodeURIComponent),
99
- );
100
- };
101
-
102
- const verifyNoSymlink = async (path, label) => {
103
- try {
104
- const stats = await lstat(path);
105
- if (stats.isSymbolicLink()) {
106
- throw new Error(`${label} may not be a symbolic link.`);
107
- }
108
- } catch (error) {
109
- if (error?.code !== "ENOENT") throw error;
110
- }
111
- };
112
-
113
- const makeArtifactTreeWritable = async (path) => {
114
- await chmod(path, 0o700).catch(() => undefined);
115
- for (const entry of await readdir(path, { withFileTypes: true }).catch(
116
- () => [],
117
- )) {
118
- const child = join(path, entry.name);
119
- if (entry.isDirectory()) await makeArtifactTreeWritable(child);
120
- else if (!entry.isSymbolicLink()) await chmod(child, 0o600);
121
- }
122
- };
123
-
124
- export const createBaselineGenerationId = ({ now = new Date() } = {}) => {
125
- const timestamp = now
126
- .toISOString()
127
- .replace(/\.\d{3}Z$/u, "Z")
128
- .replaceAll("-", "")
129
- .replaceAll(":", "");
130
- return `${timestamp}-${randomBytes(6).toString("hex")}`;
131
- };
132
-
133
- export const createAndActivateBaseline = async ({
134
- artifactRoot,
135
- generationId = createBaselineGenerationId(),
136
- records,
137
- metadata,
138
- expectedTables = [],
139
- migrationFiles = [],
140
- assets = [],
141
- schemaSql = null,
142
- activate = true,
143
- }) => {
144
- const root = assertArtifactRoot(artifactRoot);
145
- assertGenerationId(generationId);
146
- if (!records?.[Symbol.asyncIterator] && !records?.[Symbol.iterator]) {
147
- throw new Error(
148
- "A Rehearsal baseline requires an iterable sanitized record stream.",
149
- );
150
- }
151
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
152
- throw new Error("A Rehearsal baseline requires explicit metadata.");
153
- }
154
- if (
155
- !Array.isArray(expectedTables) ||
156
- expectedTables.some((name) => !relationPattern.test(name)) ||
157
- new Set(expectedTables).size !== expectedTables.length
158
- ) {
159
- throw new Error(
160
- "Rehearsal expected table names are invalid or duplicated.",
161
- );
162
- }
163
- const expectedTableSet = new Set(expectedTables);
164
- await verifyNoSymlink(root, "The Rehearsal artifact root");
165
- await mkdir(root, { recursive: true, mode: 0o700 });
166
- const generationsRoot = assertInsideRoot(
167
- root,
168
- join(root, generationsDirectoryName),
169
- );
170
- await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
171
- await mkdir(generationsRoot, { recursive: true, mode: 0o700 });
172
-
173
- const finalDirectory = assertInsideRoot(
174
- root,
175
- join(generationsRoot, generationId),
176
- );
177
- const buildDirectory = assertInsideRoot(
178
- root,
179
- join(
180
- root,
181
- `${buildPrefix}${generationId}-${randomBytes(4).toString("hex")}`,
182
- ),
183
- );
184
- const dataPath = join(buildDirectory, "sanitized-data.ndjson");
185
- let dataHandle;
186
- let finalCreated = false;
187
- let rowCount = 0;
188
- const tableCounts = new Map(expectedTables.map((name) => [name, 0]));
189
- const dataHash = createHash("sha256");
190
-
191
- try {
192
- await mkdir(buildDirectory, { mode: 0o700 });
193
- dataHandle = await open(dataPath, "wx", 0o600);
194
- for await (const record of records) {
195
- if (
196
- !record ||
197
- (record.schema !== undefined &&
198
- !/^[a-z][a-z0-9_]{0,62}$/u.test(record.schema)) ||
199
- typeof record.table !== "string" ||
200
- !/^[a-z][a-z0-9_]{0,62}$/u.test(record.table) ||
201
- !record.row ||
202
- typeof record.row !== "object" ||
203
- Array.isArray(record.row)
204
- ) {
205
- throw new Error("A sanitized baseline record has an invalid shape.");
206
- }
207
- const key = relationKey(record.schema ?? "public", record.table);
208
- if (expectedTableSet.size && !expectedTableSet.has(key)) {
209
- throw new Error(`The baseline stream exposed unexpected table ${key}.`);
210
- }
211
- const line = `${JSON.stringify(record)}\n`;
212
- dataHash.update(line);
213
- await dataHandle.write(line, null, "utf8");
214
- rowCount += 1;
215
- tableCounts.set(key, (tableCounts.get(key) ?? 0) + 1);
216
- }
217
- await dataHandle.sync();
218
- await dataHandle.close();
219
- dataHandle = null;
220
- await chmod(dataPath, 0o400);
221
- let schemaReceipt;
222
- if (schemaSql !== null) {
223
- if (typeof schemaSql !== "string" || schemaSql.length === 0) {
224
- throw new Error("A Rehearsal production schema snapshot is invalid.");
225
- }
226
- const schemaPath = join(buildDirectory, "production-schema.sql");
227
- await writeImmutableFile(schemaPath, schemaSql);
228
- schemaReceipt = {
229
- sha256: createHash("sha256").update(schemaSql).digest("hex"),
230
- bytes: Buffer.byteLength(schemaSql),
231
- };
232
- }
233
-
234
- const resolvedMigrationFiles =
235
- typeof migrationFiles === "function"
236
- ? await migrationFiles()
237
- : await migrationFiles;
238
- if (!Array.isArray(resolvedMigrationFiles)) {
239
- throw new Error("Rehearsal migration source must be an array.");
240
- }
241
- if (!/^[a-f0-9]{64}$/u.test(metadata.migrationHistorySha256 ?? "")) {
242
- throw new Error(
243
- "A Rehearsal baseline requires a migration-history SHA-256.",
244
- );
245
- }
246
- if (metadata.sourceMigrationHistory) {
247
- const migrationReceipt = createMigrationReplayReceipt({
248
- files: resolvedMigrationFiles,
249
- ledger: metadata.sourceMigrationHistory,
250
- });
251
- if (migrationReceipt.historySha256 !== metadata.migrationHistorySha256) {
252
- throw new Error(
253
- "The Rehearsal baseline migration-history SHA-256 is invalid.",
254
- );
255
- }
256
- }
257
- const migrationDirectory = join(buildDirectory, "migrations");
258
- await mkdir(migrationDirectory, { mode: 0o700 });
259
- const migrationManifest = {};
260
- for (const migration of resolvedMigrationFiles) {
261
- if (
262
- !migration ||
263
- !migrationNamePattern.test(migration.filename) ||
264
- typeof migration.content !== "string" ||
265
- Object.hasOwn(migrationManifest, migration.filename)
266
- ) {
267
- throw new Error("A Rehearsal migration bundle entry is invalid.");
268
- }
269
- const contentHash = createHash("sha256")
270
- .update(migration.content)
271
- .digest("hex");
272
- if (migration.fileSha256 && migration.fileSha256 !== contentHash) {
273
- throw new Error(
274
- `Rehearsal migration source checksum mismatch for ${migration.filename}.`,
275
- );
276
- }
277
- await writeImmutableFile(
278
- join(migrationDirectory, migration.filename),
279
- migration.content,
280
- );
281
- migrationManifest[migration.filename] = { sha256: contentHash };
282
- }
283
- await chmod(migrationDirectory, 0o500);
284
-
285
- const assetManifest = {};
286
- let totalAssetBytes = 0;
287
- for await (const asset of assets) {
288
- if (
289
- !asset ||
290
- typeof asset.bucket !== "string" ||
291
- typeof asset.objectPath !== "string" ||
292
- (!asset.content?.[Symbol.asyncIterator] &&
293
- !asset.content?.[Symbol.iterator] &&
294
- !Buffer.isBuffer(asset.content))
295
- ) {
296
- throw new Error("A Rehearsal Storage asset has an invalid shape.");
297
- }
298
- const relativePath = assetRelativePath(asset.bucket, asset.objectPath);
299
- if (Object.hasOwn(assetManifest, relativePath)) {
300
- throw new Error(`Duplicate Rehearsal Storage asset: ${relativePath}.`);
301
- }
302
- const target = assertInsideRoot(
303
- buildDirectory,
304
- join(buildDirectory, relativePath),
305
- );
306
- await mkdir(dirname(target), { recursive: true, mode: 0o700 });
307
- const handle = await open(target, "wx", 0o600);
308
- const hash = createHash("sha256");
309
- let bytes = 0;
310
- try {
311
- const chunks = Buffer.isBuffer(asset.content)
312
- ? [asset.content]
313
- : asset.content;
314
- for await (const chunk of chunks) {
315
- const buffer = Buffer.from(chunk);
316
- bytes += buffer.length;
317
- totalAssetBytes += buffer.length;
318
- if (
319
- bytes > maximumAssetBytes ||
320
- totalAssetBytes > maximumTotalAssetBytes
321
- ) {
322
- throw new Error(
323
- "The Rehearsal Storage asset boundary was exceeded.",
324
- );
325
- }
326
- hash.update(buffer);
327
- await handle.write(buffer);
328
- }
329
- await handle.sync();
330
- } finally {
331
- await handle.close();
332
- }
333
- await chmod(target, 0o400);
334
- assetManifest[relativePath] = {
335
- sha256: hash.digest("hex"),
336
- bytes,
337
- bucket: asset.bucket,
338
- objectPath: asset.objectPath,
339
- contentType: asset.contentType ?? "application/octet-stream",
340
- };
341
- }
342
-
343
- const manifest = {
344
- ...metadata,
345
- formatVersion: schemaReceipt ? 2 : 1,
346
- generationId,
347
- rowCount,
348
- tableCounts: Object.fromEntries([...tableCounts].sort()),
349
- files: {
350
- "sanitized-data.ndjson": {
351
- sha256: dataHash.digest("hex"),
352
- rows: rowCount,
353
- },
354
- ...(schemaReceipt ? { "production-schema.sql": schemaReceipt } : {}),
355
- },
356
- storageAssets: assetManifest,
357
- migrations: migrationManifest,
358
- };
359
- await writeImmutableFile(
360
- join(buildDirectory, "baseline-manifest.json"),
361
- canonicalJson(manifest),
362
- );
363
- await rename(buildDirectory, finalDirectory);
364
- finalCreated = true;
365
- await chmod(finalDirectory, 0o500);
366
-
367
- if (activate) {
368
- await activateBaselineGeneration({
369
- artifactRoot: root,
370
- generationId,
371
- });
372
- }
373
- return manifest;
374
- } catch (error) {
375
- let cleanupFailure;
376
- if (dataHandle) await dataHandle.close().catch(() => undefined);
377
- await makeArtifactTreeWritable(buildDirectory).catch(() => undefined);
378
- await rm(buildDirectory, { recursive: true, force: true }).catch(
379
- (cleanupError) => {
380
- cleanupFailure = cleanupError;
381
- },
382
- );
383
- if (finalCreated) {
384
- await makeArtifactTreeWritable(finalDirectory).catch(() => undefined);
385
- await rm(finalDirectory, { recursive: true, force: true }).catch(
386
- (cleanupError) => {
387
- cleanupFailure ??= cleanupError;
388
- },
389
- );
390
- }
391
- if (
392
- cleanupFailure &&
393
- error &&
394
- (typeof error === "object" || typeof error === "function")
395
- ) {
396
- try {
397
- Object.defineProperty(error, "cleanupError", {
398
- value: cleanupFailure,
399
- enumerable: false,
400
- });
401
- } catch {
402
- // Preserve the preparation failure even when its Error is immutable.
403
- }
404
- }
405
- throw error;
406
- }
407
- };
408
-
409
- export const activateBaselineGeneration = async ({
410
- artifactRoot,
411
- generationId,
412
- }) => {
413
- const root = assertArtifactRoot(artifactRoot);
414
- assertGenerationId(generationId);
415
- const generationDirectory = assertInsideRoot(
416
- root,
417
- join(root, generationsDirectoryName, generationId),
418
- );
419
- const manifest = JSON.parse(
420
- await readFile(join(generationDirectory, "baseline-manifest.json"), "utf8"),
421
- );
422
- if (manifest.generationId !== generationId) {
423
- throw new Error(
424
- "The staged baseline identifier does not match its manifest.",
425
- );
426
- }
427
- const temporaryLink = assertInsideRoot(
428
- root,
429
- join(root, `.current-${randomBytes(4).toString("hex")}`),
430
- );
431
- await symlink(join(generationsDirectoryName, generationId), temporaryLink);
432
- await rename(temporaryLink, join(root, currentLinkName));
433
- return manifest;
434
- };
435
-
436
- export const removeStagedBaselineGeneration = async ({
437
- artifactRoot,
438
- generationId,
439
- }) => {
440
- const root = assertArtifactRoot(artifactRoot);
441
- assertGenerationId(generationId);
442
- try {
443
- const active = await resolveActiveBaselinePaths({ artifactRoot: root });
444
- if (basename(active.generationDirectory) === generationId) {
445
- throw new Error(
446
- "Refusing to remove the active Rehearsal baseline generation.",
447
- );
448
- }
449
- } catch (error) {
450
- if (error?.code !== "ENOENT") throw error;
451
- }
452
- const target = assertInsideRoot(
453
- root,
454
- join(root, generationsDirectoryName, generationId),
455
- );
456
- await verifyNoSymlink(target, "A staged Rehearsal baseline generation");
457
- await makeArtifactTreeWritable(target);
458
- await rm(target, { recursive: true, force: true });
459
- };
460
-
461
- export const resolveActiveBaselinePaths = async ({ artifactRoot }) => {
462
- const root = assertArtifactRoot(artifactRoot);
463
- const currentTarget = await readlink(join(root, currentLinkName));
464
- const generationDirectory = resolve(root, currentTarget);
465
- assertInsideRoot(root, generationDirectory);
466
- if (dirname(generationDirectory) !== join(root, generationsDirectoryName)) {
467
- throw new Error(
468
- "The active Rehearsal baseline points outside generations.",
469
- );
470
- }
471
- return {
472
- artifactRoot: root,
473
- generationDirectory,
474
- dataPath: join(generationDirectory, "sanitized-data.ndjson"),
475
- manifestPath: join(generationDirectory, "baseline-manifest.json"),
476
- migrationsDirectory: join(generationDirectory, "migrations"),
477
- schemaPath: join(generationDirectory, "production-schema.sql"),
478
- };
479
- };
480
-
481
- export const verifyActiveBaseline = async ({ artifactRoot }) => {
482
- const paths = await resolveActiveBaselinePaths({ artifactRoot });
483
- const { generationDirectory } = paths;
484
- const manifest = JSON.parse(await readFile(paths.manifestPath, "utf8"));
485
- assertGenerationId(manifest.generationId);
486
- if (basename(generationDirectory) !== manifest.generationId) {
487
- throw new Error(
488
- "The active Rehearsal baseline identifier does not match its path.",
489
- );
490
- }
491
- const data = await readFile(paths.dataPath);
492
- const actualHash = createHash("sha256").update(data).digest("hex");
493
- if (actualHash !== manifest.files?.["sanitized-data.ndjson"]?.sha256) {
494
- throw new Error("The active Rehearsal baseline data checksum is invalid.");
495
- }
496
- if (manifest.files?.["production-schema.sql"]) {
497
- const schema = await readFile(paths.schemaPath);
498
- const receipt = manifest.files["production-schema.sql"];
499
- if (
500
- schema.length !== receipt.bytes ||
501
- createHash("sha256").update(schema).digest("hex") !== receipt.sha256
502
- ) {
503
- throw new Error(
504
- "The active Rehearsal production schema checksum is invalid.",
505
- );
506
- }
507
- }
508
- const migrationDirectory = paths.migrationsDirectory;
509
- const expectedMigrationNames = Object.keys(manifest.migrations ?? {}).sort();
510
- const actualMigrationNames = (await readdir(migrationDirectory)).sort();
511
- if (expectedMigrationNames.join("\0") !== actualMigrationNames.join("\0")) {
512
- throw new Error("The active Rehearsal migration bundle is not exact.");
513
- }
514
- for (const filename of expectedMigrationNames) {
515
- if (!migrationNamePattern.test(filename)) {
516
- throw new Error("The active Rehearsal migration manifest is invalid.");
517
- }
518
- const migrationHash = createHash("sha256")
519
- .update(await readFile(join(migrationDirectory, filename)))
520
- .digest("hex");
521
- if (migrationHash !== manifest.migrations[filename]?.sha256) {
522
- throw new Error(
523
- `The active Rehearsal migration checksum is invalid for ${filename}.`,
524
- );
525
- }
526
- }
527
- for (const [relativePath, receipt] of Object.entries(
528
- manifest.storageAssets ?? {},
529
- )) {
530
- if (
531
- relativePath !== assetRelativePath(receipt.bucket, receipt.objectPath)
532
- ) {
533
- throw new Error("The active Rehearsal Storage manifest path is invalid.");
534
- }
535
- const assetPath = assertInsideRoot(
536
- generationDirectory,
537
- join(generationDirectory, relativePath),
538
- );
539
- const asset = await readFile(assetPath);
540
- if (
541
- asset.length !== receipt.bytes ||
542
- createHash("sha256").update(asset).digest("hex") !== receipt.sha256
543
- ) {
544
- throw new Error(
545
- `The active Rehearsal Storage checksum is invalid for ${relativePath}.`,
546
- );
547
- }
548
- }
549
- return manifest;
550
- };
551
-
552
- export const listIncompleteBaselineBuilds = async ({ artifactRoot }) => {
553
- const root = assertArtifactRoot(artifactRoot);
554
- try {
555
- return (await readdir(root)).filter((name) => name.startsWith(buildPrefix));
556
- } catch (error) {
557
- if (error?.code === "ENOENT") return [];
558
- throw error;
559
- }
560
- };
561
-
562
- export const planBaselineGenerationPrune = async ({
563
- artifactRoot,
564
- retain = 2,
565
- }) => {
566
- const root = assertArtifactRoot(artifactRoot);
567
- if (!Number.isSafeInteger(retain) || retain < 1) {
568
- throw new Error("Rehearsal baseline retention must be a positive integer.");
569
- }
570
- const active = await resolveActiveBaselinePaths({ artifactRoot: root });
571
- const generationsRoot = join(root, generationsDirectoryName);
572
- await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
573
- const entries = await readdir(generationsRoot, { withFileTypes: true });
574
- for (const entry of entries) {
575
- if (
576
- !entry.isDirectory() ||
577
- entry.isSymbolicLink() ||
578
- !generationPattern.test(entry.name)
579
- ) {
580
- throw new Error(
581
- `Refusing to prune an unexpected Rehearsal generation entry: ${entry.name}`,
582
- );
583
- }
584
- }
585
- const activeName = basename(active.generationDirectory);
586
- const retained = new Set(
587
- [
588
- activeName,
589
- ...entries
590
- .map((entry) => entry.name)
591
- .sort()
592
- .reverse(),
593
- ]
594
- .filter((name, index, values) => values.indexOf(name) === index)
595
- .slice(0, retain),
596
- );
597
- const removed = entries
598
- .map((entry) => entry.name)
599
- .filter((name) => !retained.has(name))
600
- .sort();
601
- return { retained: [...retained], removed };
602
- };
603
-
604
- export const planBaselineReplacementPrune = async ({
605
- artifactRoot,
606
- retain = 2,
607
- }) => {
608
- const root = assertArtifactRoot(artifactRoot);
609
- if (!Number.isSafeInteger(retain) || retain < 1) {
610
- throw new Error("Rehearsal baseline retention must be a positive integer.");
611
- }
612
- await resolveActiveBaselinePaths({ artifactRoot: root });
613
- const generationsRoot = join(root, generationsDirectoryName);
614
- await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
615
- const entries = await readdir(generationsRoot, { withFileTypes: true });
616
- for (const entry of entries) {
617
- if (
618
- !entry.isDirectory() ||
619
- entry.isSymbolicLink() ||
620
- !generationPattern.test(entry.name)
621
- ) {
622
- throw new Error(
623
- `Refusing to plan replacement around an unexpected Rehearsal generation entry: ${entry.name}`,
624
- );
625
- }
626
- }
627
- const current = entries
628
- .map((entry) => entry.name)
629
- .sort()
630
- .reverse();
631
- const retained = current.slice(0, Math.max(0, retain - 1));
632
- const retainedSet = new Set(retained);
633
- return {
634
- retainedAfterReplacement: retained,
635
- removedAfterReplacement: current
636
- .filter((name) => !retainedSet.has(name))
637
- .sort(),
638
- };
639
- };
640
-
641
- export const pruneBaselineGenerations = async ({
642
- artifactRoot,
643
- retain = 2,
644
- expectedRemoved,
645
- }) => {
646
- const root = assertArtifactRoot(artifactRoot);
647
- const plan = await planBaselineGenerationPrune({
648
- artifactRoot: root,
649
- retain,
650
- });
651
- const generationsRoot = join(root, generationsDirectoryName);
652
- if (
653
- expectedRemoved &&
654
- (expectedRemoved.length !== plan.removed.length ||
655
- expectedRemoved.some((name, index) => name !== plan.removed[index]))
656
- ) {
657
- throw new Error(
658
- "Rehearsal baseline generations changed after the cleanup preview.",
659
- );
660
- }
661
- for (const name of plan.removed) {
662
- const active = await resolveActiveBaselinePaths({ artifactRoot: root });
663
- if (basename(active.generationDirectory) === name) {
664
- throw new Error(
665
- "Refusing to prune the active Rehearsal baseline generation.",
666
- );
667
- }
668
- const target = assertInsideRoot(
669
- generationsRoot,
670
- join(generationsRoot, name),
671
- );
672
- await verifyNoSymlink(target, "A Rehearsal baseline generation");
673
- await makeArtifactTreeWritable(target);
674
- await rm(target, { recursive: true, force: true });
675
- }
676
- return plan;
677
- };
678
-
679
- export const removeBaselineArtifactRoot = async ({ artifactRoot }) => {
680
- const root = assertArtifactRoot(artifactRoot);
681
- await verifyNoSymlink(root, "The Rehearsal artifact root");
682
- await makeArtifactTreeWritable(root);
683
- await rm(root, { recursive: true, force: true });
684
- };