@rehearsal-db/core 0.1.0-beta.14 → 0.1.0-beta.16

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 (236) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -0
  3. package/dist/package.json +101 -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_contract.d.mts +147 -0
  21. package/dist/src/baseline/privacy_contract.mjs +2 -0
  22. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  23. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  24. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  25. package/dist/src/baseline/privacy_engine.mjs +4 -0
  26. package/dist/src/baseline/privacy_key.d.mts +7 -0
  27. package/dist/src/baseline/privacy_key.mjs +42 -0
  28. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  29. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  30. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  31. package/dist/src/baseline/privacy_policy.mjs +237 -0
  32. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  33. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  34. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  35. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  36. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  37. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  38. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  39. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  40. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  41. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  42. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  43. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  44. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  45. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  46. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  47. package/dist/src/cli/application_command_handlers.mjs +52 -0
  48. package/dist/src/cli/arguments.d.mts +38 -0
  49. package/dist/src/cli/arguments.mjs +128 -0
  50. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  51. package/dist/src/cli/basic_command_handlers.mjs +56 -0
  52. package/dist/src/cli/cli_commands.d.mts +14 -0
  53. package/dist/src/cli/cli_commands.mjs +32 -0
  54. package/dist/src/cli/command_contract.d.mts +57 -0
  55. package/dist/src/cli/command_contract.mjs +39 -0
  56. package/dist/src/cli/command_output.d.mts +12 -0
  57. package/dist/src/cli/command_output.mjs +20 -0
  58. package/dist/src/cli/command_registry.d.mts +7 -0
  59. package/dist/src/cli/command_registry.mjs +14 -0
  60. package/dist/src/cli/guided.d.mts +53 -0
  61. package/dist/src/cli/guided.mjs +719 -0
  62. package/dist/src/cli/identity_command_handlers.d.mts +8 -0
  63. package/dist/src/cli/identity_command_handlers.mjs +43 -0
  64. package/dist/src/cli/init_command.d.mts +21 -0
  65. package/dist/src/cli/init_command.mjs +90 -0
  66. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  67. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  68. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  69. package/dist/src/cli/project_command_handlers.mjs +92 -0
  70. package/dist/src/cli/project_commands.d.mts +159 -0
  71. package/dist/src/cli/project_commands.mjs +183 -0
  72. package/dist/src/cli/rehearsal.d.mts +7 -0
  73. package/dist/src/cli/rehearsal.mjs +190 -0
  74. package/dist/src/cli/renderers.d.mts +299 -0
  75. package/dist/src/cli/renderers.mjs +554 -0
  76. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  77. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  78. package/dist/src/cli/runtime_commands.d.mts +269 -0
  79. package/dist/src/cli/runtime_commands.mjs +601 -0
  80. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  81. package/dist/src/cli/source_command_handlers.mjs +62 -0
  82. package/dist/src/cli/source_commands.d.mts +436 -0
  83. package/dist/src/cli/source_commands.mjs +301 -0
  84. package/dist/src/cli/terminal.d.mts +59 -0
  85. package/dist/src/cli/terminal.mjs +230 -0
  86. package/dist/src/identity/claim.d.mts +5 -0
  87. package/dist/src/identity/claim.mjs +4 -0
  88. package/dist/src/identity/claim_contract.d.mts +135 -0
  89. package/dist/src/identity/claim_contract.mjs +6 -0
  90. package/dist/src/identity/claim_executor.d.mts +10 -0
  91. package/dist/src/identity/claim_executor.mjs +358 -0
  92. package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
  93. package/dist/src/identity/claim_identity_lookup.mjs +125 -0
  94. package/dist/src/identity/claim_plan.d.mts +8 -0
  95. package/dist/src/identity/claim_plan.mjs +61 -0
  96. package/dist/src/identity/claim_policy.d.mts +3 -0
  97. package/dist/src/identity/claim_policy.mjs +377 -0
  98. package/dist/src/identity/claim_sql.d.mts +10 -0
  99. package/dist/src/identity/claim_sql.mjs +46 -0
  100. package/dist/src/identity/claim_verification.d.mts +13 -0
  101. package/dist/src/identity/claim_verification.mjs +35 -0
  102. package/dist/src/identity/provider_configuration.d.mts +37 -0
  103. package/dist/src/identity/provider_configuration.mjs +132 -0
  104. package/dist/src/identity/storage.d.mts +21 -0
  105. package/dist/src/identity/storage.mjs +122 -0
  106. package/dist/src/index.d.mts +4 -0
  107. package/dist/src/index.mjs +3 -0
  108. package/dist/src/project/configuration.d.mts +13 -0
  109. package/dist/src/project/configuration.mjs +9 -0
  110. package/dist/src/project/configuration_contract.d.mts +125 -0
  111. package/dist/src/project/configuration_contract.mjs +27 -0
  112. package/dist/src/project/configuration_discovery.d.mts +7 -0
  113. package/dist/src/project/configuration_discovery.mjs +37 -0
  114. package/dist/src/project/configuration_normalization.d.mts +107 -0
  115. package/dist/src/project/configuration_normalization.mjs +302 -0
  116. package/dist/src/project/configuration_rendering.d.mts +14 -0
  117. package/dist/src/project/configuration_rendering.mjs +224 -0
  118. package/dist/src/project/configuration_resolution.d.mts +125 -0
  119. package/dist/src/project/configuration_resolution.mjs +81 -0
  120. package/dist/src/project/configuration_schema.d.mts +22 -0
  121. package/dist/src/project/configuration_schema.mjs +160 -0
  122. package/dist/src/project/configuration_validation.d.mts +22 -0
  123. package/dist/src/project/configuration_validation.mjs +136 -0
  124. package/dist/src/project/install_scaffold.d.mts +42 -0
  125. package/dist/src/project/install_scaffold.mjs +263 -0
  126. package/dist/src/project/project_detection.d.mts +14 -0
  127. package/dist/src/project/project_detection.mjs +47 -0
  128. package/dist/src/project/setup.d.mts +87 -0
  129. package/dist/src/project/setup.mjs +414 -0
  130. package/dist/src/project/support_report.d.mts +88 -0
  131. package/dist/src/project/support_report.mjs +83 -0
  132. package/dist/src/runtime/cleanup.d.mts +85 -0
  133. package/dist/src/runtime/cleanup.mjs +311 -0
  134. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  135. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  136. package/dist/src/runtime/migration_history.d.mts +51 -0
  137. package/dist/src/runtime/migration_history.mjs +170 -0
  138. package/dist/src/runtime/plan.d.mts +134 -0
  139. package/dist/src/runtime/plan.mjs +637 -0
  140. package/dist/src/runtime/policy.d.mts +66 -0
  141. package/dist/src/runtime/policy.mjs +369 -0
  142. package/dist/src/runtime/restore.d.mts +22 -0
  143. package/dist/src/runtime/restore.mjs +276 -0
  144. package/dist/src/runtime/service_environment.d.mts +11 -0
  145. package/dist/src/runtime/service_environment.mjs +58 -0
  146. package/dist/src/runtime/topology.d.mts +51 -0
  147. package/dist/src/runtime/topology.mjs +128 -0
  148. package/dist/src/shared/diagnostics.d.mts +97 -0
  149. package/dist/src/shared/diagnostics.mjs +170 -0
  150. package/dist/src/shared/human_output.d.mts +2 -0
  151. package/{src → dist/src}/shared/human_output.mjs +1 -3
  152. package/dist/src/shared/node_runtime.d.mts +14 -0
  153. package/dist/src/shared/node_runtime.mjs +25 -0
  154. package/dist/src/shared/operation_guard.d.mts +22 -0
  155. package/dist/src/shared/operation_guard.mjs +150 -0
  156. package/dist/src/shared/process_environment.d.mts +15 -0
  157. package/dist/src/shared/process_environment.mjs +50 -0
  158. package/dist/src/source/access.d.mts +165 -0
  159. package/dist/src/source/access.mjs +602 -0
  160. package/dist/src/source/asset_transfer.d.mts +27 -0
  161. package/dist/src/source/asset_transfer.mjs +202 -0
  162. package/dist/src/source/baseline.d.mts +66 -0
  163. package/dist/src/source/baseline.mjs +372 -0
  164. package/dist/src/source/external_reader.d.mts +42 -0
  165. package/dist/src/source/external_reader.mjs +278 -0
  166. package/dist/src/source/postgresql_access.d.mts +86 -0
  167. package/dist/src/source/postgresql_access.mjs +454 -0
  168. package/dist/src/source/privacy_paths.d.mts +10 -0
  169. package/dist/src/source/privacy_paths.mjs +27 -0
  170. package/dist/src/targets/driver.d.mts +2 -0
  171. package/dist/src/targets/driver.mjs +13 -0
  172. package/dist/src/targets/postgresql.d.mts +5 -0
  173. package/dist/src/targets/postgresql.mjs +620 -0
  174. package/dist/src/targets/supabase.d.mts +8 -0
  175. package/dist/src/targets/supabase.mjs +627 -0
  176. package/dist/src/targets/supabase_environment.d.mts +49 -0
  177. package/dist/src/targets/supabase_environment.mjs +162 -0
  178. package/dist/src/targets/target.d.mts +33 -0
  179. package/dist/src/targets/target.mjs +39 -0
  180. package/docs/README.md +1 -0
  181. package/docs/architecture.md +133 -10
  182. package/docs/configuration.md +9 -3
  183. package/docs/index.md +1 -0
  184. package/docs/releasing.md +8 -3
  185. package/docs/typescript.md +134 -0
  186. package/package.json +36 -29
  187. package/types/configuration.d.mts +5 -0
  188. package/scripts/install/create_root_config.mjs +0 -19
  189. package/src/README.md +0 -17
  190. package/src/application/session.mjs +0 -435
  191. package/src/baseline/artifact.mjs +0 -684
  192. package/src/baseline/builder.mjs +0 -164
  193. package/src/baseline/input_discovery.mjs +0 -155
  194. package/src/baseline/policy_review.mjs +0 -92
  195. package/src/baseline/preparation.mjs +0 -323
  196. package/src/baseline/privacy_engine.mjs +0 -1399
  197. package/src/baseline/privacy_structured_json.mjs +0 -554
  198. package/src/baseline/sanitization_policy.mjs +0 -299
  199. package/src/baseline/schema_snapshot.mjs +0 -113
  200. package/src/cli/arguments.mjs +0 -126
  201. package/src/cli/guided.mjs +0 -814
  202. package/src/cli/init_command.mjs +0 -94
  203. package/src/cli/rehearsal.mjs +0 -972
  204. package/src/cli/renderers.mjs +0 -638
  205. package/src/cli/runtime_commands.mjs +0 -707
  206. package/src/cli/source_commands.mjs +0 -355
  207. package/src/cli/terminal.mjs +0 -275
  208. package/src/identity/claim.mjs +0 -1165
  209. package/src/identity/provider_configuration.mjs +0 -177
  210. package/src/identity/storage.mjs +0 -165
  211. package/src/project/configuration.d.mts +0 -164
  212. package/src/project/configuration.mjs +0 -1337
  213. package/src/project/install_scaffold.mjs +0 -325
  214. package/src/project/setup.mjs +0 -474
  215. package/src/project/support_report.mjs +0 -105
  216. package/src/runtime/cleanup.mjs +0 -381
  217. package/src/runtime/migration_history.mjs +0 -220
  218. package/src/runtime/plan.mjs +0 -722
  219. package/src/runtime/policy.mjs +0 -438
  220. package/src/runtime/restore.mjs +0 -337
  221. package/src/runtime/service_environment.mjs +0 -82
  222. package/src/runtime/topology.mjs +0 -177
  223. package/src/shared/diagnostics.mjs +0 -207
  224. package/src/shared/node_runtime.mjs +0 -31
  225. package/src/shared/operation_guard.mjs +0 -162
  226. package/src/shared/process_environment.mjs +0 -68
  227. package/src/source/access.mjs +0 -798
  228. package/src/source/asset_transfer.mjs +0 -259
  229. package/src/source/baseline.mjs +0 -482
  230. package/src/source/external_reader.mjs +0 -368
  231. package/src/source/postgresql_access.mjs +0 -638
  232. package/src/source/privacy_paths.mjs +0 -38
  233. package/src/targets/postgresql.mjs +0 -808
  234. package/src/targets/supabase.mjs +0 -840
  235. package/src/targets/supabase_environment.mjs +0 -229
  236. package/src/targets/target.mjs +0 -66
@@ -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
- };