@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
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Purpose: Restore and manage a disposable, loopback-only PostgreSQL runtime from
3
+ * the active verified Rehearsal baseline.
4
+ */
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import { randomBytes } from "node:crypto";
7
+ import { createReadStream } from "node:fs";
8
+ import { once } from "node:events";
9
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
10
+ import { join, resolve } from "node:path";
11
+ import { createInterface } from "node:readline";
12
+ import { pathToFileURL } from "node:url";
13
+ import { verifyActiveBaseline } from "../baseline/artifact.mjs";
14
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
15
+ import { formatCount } from "../shared/human_output.mjs";
16
+ import { readMigrationFileInventory } from "../runtime/migration_history.mjs";
17
+ import { createCleanProcessEnvironment } from "../shared/process_environment.mjs";
18
+ import { buildRestoreSqlPrefix, buildRestoreSqlSuffix, encodeBaselineRecordForCopy, summarizeRestoreError, } from "../runtime/restore.mjs";
19
+ import { assertCandidateConfirmation, readActiveRuntimeInput, readCandidateMigrationPlan, readMatchingRuntimeMarker, runRuntimeLifecycle, withOwnedRuntimeRollback, writeCandidateMigrationReceipt, writeRuntimeMarker, } from "../runtime/lifecycle_engine.mjs";
20
+ import { ensureLocalContainerRuntime } from "./supabase_environment.mjs";
21
+ import { parseRuntimeInvocation } from "./target.mjs";
22
+ import { buildRuntimeFinalizationSql, buildRuntimePostSchemaSql, buildRuntimePrerequisiteSql, buildRuntimeVerificationSql, validateRuntimePolicy, } from "../runtime/policy.mjs";
23
+ const repositoryRoot = process.cwd();
24
+ const invocation = parseRuntimeInvocation();
25
+ const loadedConfiguration = await loadRehearsalConfig({
26
+ projectRoot: repositoryRoot,
27
+ ...(invocation.configPath === undefined
28
+ ? {}
29
+ : { configPath: invocation.configPath }),
30
+ });
31
+ const { config, paths: configuredPaths } = loadedConfiguration;
32
+ if (!config.postgresql) {
33
+ throw new Error("The PostgreSQL runtime requires config.postgresql.");
34
+ }
35
+ const postgresqlConfig = config.postgresql;
36
+ const artifactRoot = configuredPaths.artifactDirectory;
37
+ const runtimeWorkdir = configuredPaths.runtimeWorkdir;
38
+ const runtimeMarkerPath = join(runtimeWorkdir, "baseline.json");
39
+ const runtimeCandidateReceiptPath = join(runtimeWorkdir, "candidate-receipt.json");
40
+ const environmentPath = configuredPaths.applicationEnvironment;
41
+ const manifestPath = configuredPaths.sanitizationPolicy;
42
+ const projectId = config.runtime.projectId;
43
+ const containerName = `${projectId}-postgresql`;
44
+ const volumeName = `${projectId}-postgresql-data`;
45
+ const label = `com.rehearsal-db.project=${projectId}`;
46
+ const image = postgresqlConfig.image;
47
+ const database = postgresqlConfig.database;
48
+ const databaseUser = postgresqlConfig.user;
49
+ const databasePort = config.runtime.ports.database;
50
+ const action = invocation.action;
51
+ const applicationMigrations = new URL("./", pathToFileURL(`${configuredPaths.migrationDirectory}/`));
52
+ const runtimeAdapter = configuredPaths.runtimeAdapter
53
+ ? (await import(pathToFileURL(configuredPaths.runtimeAdapter).href))
54
+ : null;
55
+ const runtimePolicy = configuredPaths.runtimePolicy
56
+ ? validateRuntimePolicy(JSON.parse(await readFile(configuredPaths.runtimePolicy, "utf8")))
57
+ : null;
58
+ const environmentKeyPattern = /^[A-Z][A-Z0-9_]*$/u;
59
+ const runCommand = (command, args, { capture = false, input, allowFailure = false } = {}) => {
60
+ const piped = capture || input !== undefined || allowFailure;
61
+ const result = spawnSync(command, args, {
62
+ cwd: repositoryRoot,
63
+ encoding: "utf8",
64
+ env: createCleanProcessEnvironment(),
65
+ input,
66
+ maxBuffer: 16 * 1024 * 1024,
67
+ stdio: piped ? ["pipe", "pipe", "pipe"] : "inherit",
68
+ });
69
+ if (!allowFailure && result.error)
70
+ throw result.error;
71
+ if (!allowFailure && result.status !== 0) {
72
+ const detail = [result.stdout, result.stderr]
73
+ .filter(Boolean)
74
+ .join("\n")
75
+ .trim();
76
+ throw new Error(`${command} ${args.join(" ")} failed${detail ? `:\n${detail}` : "."}`);
77
+ }
78
+ return result;
79
+ };
80
+ const commandSucceeds = (command, args) => runCommand(command, args, { allowFailure: true }).status === 0;
81
+ const pathExists = async (path) => stat(path)
82
+ .then(() => true)
83
+ .catch((error) => {
84
+ if (typeof error === "object" &&
85
+ error !== null &&
86
+ "code" in error &&
87
+ error.code === "ENOENT")
88
+ return false;
89
+ throw error;
90
+ });
91
+ const assertRuntimePath = () => {
92
+ if (resolve(runtimeWorkdir) !== join(resolve(artifactRoot), "runtime")) {
93
+ throw new Error("Refusing an unsafe Rehearsal runtime path.");
94
+ }
95
+ };
96
+ const assertDocker = () => {
97
+ ensureLocalContainerRuntime({
98
+ cwd: repositoryRoot,
99
+ autoStartColima: config.containerRuntime.autoStartColima,
100
+ });
101
+ };
102
+ const inspectResourceLabel = (kind, name) => runCommand("docker", [
103
+ kind,
104
+ "inspect",
105
+ "--format",
106
+ kind === "container"
107
+ ? '{{index .Config.Labels "com.rehearsal-db.project"}}'
108
+ : '{{index .Labels "com.rehearsal-db.project"}}',
109
+ name,
110
+ ], { allowFailure: true });
111
+ const resourceExists = (kind, name) => runCommand("docker", [kind, "inspect", name], { allowFailure: true })
112
+ .status === 0;
113
+ const assertOwnedResource = (kind, name) => {
114
+ if (!resourceExists(kind, name))
115
+ return false;
116
+ const inspection = inspectResourceLabel(kind, name);
117
+ if (inspection.status !== 0 || inspection.stdout.trim() !== projectId) {
118
+ throw new Error(`Refusing to manage Docker ${kind} ${name} because it is not owned by this Rehearsal project.`);
119
+ }
120
+ return true;
121
+ };
122
+ const removeRuntime = async () => {
123
+ assertRuntimePath();
124
+ if (commandSucceeds("docker", ["info"])) {
125
+ if (assertOwnedResource("container", containerName)) {
126
+ runCommand("docker", ["container", "rm", "--force", containerName], {
127
+ capture: true,
128
+ });
129
+ }
130
+ if (assertOwnedResource("volume", volumeName)) {
131
+ runCommand("docker", ["volume", "rm", "--force", volumeName], {
132
+ capture: true,
133
+ });
134
+ }
135
+ }
136
+ await rm(runtimeWorkdir, { recursive: true, force: true });
137
+ await rm(environmentPath, { force: true });
138
+ };
139
+ const waitForDatabase = async () => {
140
+ let consecutiveReadyChecks = 0;
141
+ for (let attempt = 0; attempt < 60; attempt += 1) {
142
+ if (commandSucceeds("docker", [
143
+ "exec",
144
+ containerName,
145
+ "pg_isready",
146
+ "--username",
147
+ databaseUser,
148
+ "--dbname",
149
+ database,
150
+ ])) {
151
+ consecutiveReadyChecks += 1;
152
+ if (consecutiveReadyChecks === 3)
153
+ return;
154
+ }
155
+ else {
156
+ consecutiveReadyChecks = 0;
157
+ }
158
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
159
+ }
160
+ throw new Error("The disposable PostgreSQL database did not become ready.");
161
+ };
162
+ const runPsql = (sql, { capture = true } = {}) => runCommand("docker", [
163
+ "exec",
164
+ "--interactive",
165
+ containerName,
166
+ "psql",
167
+ "--quiet",
168
+ "--no-align",
169
+ "--tuples-only",
170
+ "--set",
171
+ "ON_ERROR_STOP=1",
172
+ "--username",
173
+ databaseUser,
174
+ "--dbname",
175
+ database,
176
+ ], { capture, input: sql }).stdout;
177
+ const databaseEnvironment = (password) => ({
178
+ databaseUrl: [
179
+ "postgresql://",
180
+ databaseUser,
181
+ ":",
182
+ encodeURIComponent(password),
183
+ "@127.0.0.1:",
184
+ databasePort,
185
+ "/",
186
+ database,
187
+ "?sslmode=disable",
188
+ ].join(""),
189
+ host: "127.0.0.1",
190
+ port: databasePort,
191
+ database,
192
+ user: databaseUser,
193
+ password,
194
+ });
195
+ const quoteLiteral = (value) => `'${String(value).replaceAll("'", "''")}'`;
196
+ const initializeMigrationLedger = () => runPsql(`create schema if not exists rehearsal_internal;
197
+ create table if not exists rehearsal_internal.schema_migrations (
198
+ version text primary key,
199
+ name text not null,
200
+ file_sha256 text not null check (file_sha256 ~ '^[a-f0-9]{64}$'),
201
+ applied_at timestamptz not null default now()
202
+ );`);
203
+ const applyMigration = async ({ migration, directory, }) => {
204
+ const source = await readFile(join(directory, migration.filename), "utf8");
205
+ runPsql(source, { capture: false });
206
+ runPsql(`insert into rehearsal_internal.schema_migrations
207
+ (version, name, file_sha256)
208
+ values (
209
+ ${quoteLiteral(migration.version)},
210
+ ${quoteLiteral(migration.name)},
211
+ ${quoteLiteral(migration.fileSha256)}
212
+ );`);
213
+ };
214
+ const readRuntimeMigrationLedger = () => {
215
+ const output = runPsql(`select version || E'\\t' || name || E'\\t' || file_sha256
216
+ from rehearsal_internal.schema_migrations
217
+ order by version;`);
218
+ return output
219
+ .trim()
220
+ .split("\n")
221
+ .filter(Boolean)
222
+ .map((line) => {
223
+ const [version, name, fileSha256] = line.split("\t");
224
+ return { version, name, fileSha256 };
225
+ });
226
+ };
227
+ const verifyMigrationLedger = ({ currentFiles, baseline, }) => {
228
+ const ledger = readRuntimeMigrationLedger();
229
+ const baselineCount = Object.keys(baseline.migrations).length;
230
+ if (ledger.length < baselineCount || ledger.length > currentFiles.length) {
231
+ throw new Error("The PostgreSQL runtime migration ledger has an unexpected length.");
232
+ }
233
+ for (let index = 0; index < ledger.length; index += 1) {
234
+ const applied = ledger[index];
235
+ const source = currentFiles[index];
236
+ if (!applied ||
237
+ !source ||
238
+ applied.version !== source.version ||
239
+ applied.name !== source.name ||
240
+ applied.fileSha256 !== source.fileSha256) {
241
+ throw new Error(`The PostgreSQL runtime migration ledger differs at ${applied?.version ?? "unknown"}.`);
242
+ }
243
+ }
244
+ return { ledger, appliedCandidateCount: ledger.length - baselineCount };
245
+ };
246
+ const writeChunk = async (stream, chunk, completion) => {
247
+ if (stream.destroyed || stream.writableEnded) {
248
+ throw new Error("The Rehearsal restore connection closed early.");
249
+ }
250
+ if (!stream.write(chunk)) {
251
+ await Promise.race([once(stream, "drain"), completion]);
252
+ }
253
+ };
254
+ const restoreBaselineRows = async ({ baseline, paths, manifest, }) => {
255
+ const child = spawn("docker", [
256
+ "exec",
257
+ "--interactive",
258
+ containerName,
259
+ "psql",
260
+ "--quiet",
261
+ "--set",
262
+ "ON_ERROR_STOP=1",
263
+ "--username",
264
+ databaseUser,
265
+ "--dbname",
266
+ database,
267
+ ], {
268
+ cwd: repositoryRoot,
269
+ env: createCleanProcessEnvironment(),
270
+ stdio: ["pipe", "ignore", "pipe"],
271
+ });
272
+ let stderr = "";
273
+ child.stderr.setEncoding("utf8");
274
+ child.stderr.on("data", (chunk) => {
275
+ stderr = `${stderr}${chunk}`.slice(-16_384);
276
+ });
277
+ const completion = new Promise((complete, reject) => {
278
+ child.once("error", reject);
279
+ child.once("close", complete);
280
+ });
281
+ child.stdin.on("error", () => undefined);
282
+ try {
283
+ await writeChunk(child.stdin, buildRestoreSqlPrefix(), completion);
284
+ const lines = createInterface({
285
+ input: createReadStream(paths.dataPath, { encoding: "utf8" }),
286
+ crlfDelay: Infinity,
287
+ });
288
+ for await (const line of lines) {
289
+ if (line) {
290
+ await writeChunk(child.stdin, `${encodeBaselineRecordForCopy(line)}\n`, completion);
291
+ }
292
+ }
293
+ await writeChunk(child.stdin, buildRestoreSqlSuffix({
294
+ manifest,
295
+ tableCounts: baseline.tableCounts,
296
+ restoreSupabaseAuth: false,
297
+ }), completion);
298
+ child.stdin.end();
299
+ const exitCode = await completion;
300
+ if (exitCode !== 0) {
301
+ throw new Error(`The local Rehearsal restore failed. ${summarizeRestoreError(stderr)}`);
302
+ }
303
+ }
304
+ catch (error) {
305
+ if (child.exitCode === null)
306
+ child.kill("SIGTERM");
307
+ await completion.catch(() => undefined);
308
+ throw error;
309
+ }
310
+ };
311
+ const prepareProjectPrerequisites = () => {
312
+ if (runtimePolicy) {
313
+ runPsql(buildRuntimePrerequisiteSql(runtimePolicy));
314
+ console.log("Applied reviewed runtime prerequisites.");
315
+ }
316
+ };
317
+ const prepareProjectSchema = ({ baseline, environment, }) => {
318
+ if (!runtimeAdapter?.prepareSchema)
319
+ return;
320
+ const preparation = runtimeAdapter.prepareSchema({
321
+ baseline,
322
+ environment,
323
+ runSql: runPsql,
324
+ });
325
+ if (!preparation || typeof preparation.message !== "string") {
326
+ throw new Error("The project runtime adapter returned an invalid schema preparation result.");
327
+ }
328
+ console.log(preparation.message);
329
+ };
330
+ const finalizeProjectSchema = () => {
331
+ if (!runtimePolicy || runtimePolicy.triggers.length === 0)
332
+ return;
333
+ runPsql(buildRuntimePostSchemaSql(runtimePolicy));
334
+ console.log("Applied reviewed post-schema triggers.");
335
+ };
336
+ const finalizeProjectRuntime = () => {
337
+ if (!runtimePolicy || runtimePolicy.localRows.length === 0)
338
+ return;
339
+ runPsql(buildRuntimeFinalizationSql(runtimePolicy));
340
+ console.log("Applied reviewed local-only recipes.");
341
+ };
342
+ const configureProjectRuntime = ({ baseline, environment, }) => {
343
+ if (!runtimeAdapter?.configureRuntime) {
344
+ return {
345
+ environmentVariables: {},
346
+ message: "Project-neutral PostgreSQL runtime is configured.",
347
+ };
348
+ }
349
+ const result = runtimeAdapter.configureRuntime({
350
+ baseline,
351
+ environment,
352
+ runSql: runPsql,
353
+ });
354
+ if (!result ||
355
+ typeof result.message !== "string" ||
356
+ !result.environmentVariables ||
357
+ typeof result.environmentVariables !== "object" ||
358
+ Array.isArray(result.environmentVariables)) {
359
+ throw new Error("The project runtime adapter returned an invalid result.");
360
+ }
361
+ for (const [key, value] of Object.entries(result.environmentVariables)) {
362
+ if (!environmentKeyPattern.test(key) ||
363
+ typeof value !== "string" ||
364
+ config.safety.blockedEnvironmentVariables.includes(key)) {
365
+ throw new Error("The project runtime adapter returned an unsafe environment variable.");
366
+ }
367
+ }
368
+ return result;
369
+ };
370
+ const writeRuntimeEnvironment = async ({ baseline, environment, projectRuntime, }) => {
371
+ const variables = {
372
+ REHEARSAL_RUNTIME_ENVIRONMENT: "rehearsal",
373
+ REHEARSAL_BASELINE_ID: baseline.generationId,
374
+ DATABASE_URL: environment.databaseUrl,
375
+ PGHOST: environment.host,
376
+ PGPORT: String(environment.port),
377
+ PGDATABASE: environment.database,
378
+ PGUSER: environment.user,
379
+ PGPASSWORD: environment.password,
380
+ ...projectRuntime.environmentVariables,
381
+ };
382
+ await writeFile(environmentPath, [
383
+ "# Generated local Rehearsal runtime values. Do not commit.",
384
+ ...Object.entries(variables).map(([key, value]) => `${key}=${value}`),
385
+ "",
386
+ ].join("\n"), { mode: 0o600 });
387
+ };
388
+ const readActive = () => readActiveRuntimeInput({
389
+ artifactRoot,
390
+ manifestPath,
391
+ validate: ({ baseline, manifest }) => {
392
+ if (Object.keys(baseline.storageAssets ?? {}).length > 0) {
393
+ throw new Error("The PostgreSQL target cannot restore Supabase Storage assets. Create this baseline without an assets manifest.");
394
+ }
395
+ if (manifest.tables.some((table) => table.columns.some((column) => column.foreignKey?.schema === "auth"))) {
396
+ throw new Error("The PostgreSQL target cannot synthesize Supabase Auth rows. Remove auth-schema references or use the Supabase target.");
397
+ }
398
+ },
399
+ });
400
+ const createContainer = async (password) => {
401
+ assertDocker();
402
+ if (!commandSucceeds("docker", ["image", "inspect", image])) {
403
+ throw new Error(`The configured local PostgreSQL image is unavailable: ${image}. Pull it explicitly, review it, then retry.`);
404
+ }
405
+ runCommand("docker", ["volume", "create", "--label", label, volumeName], {
406
+ capture: true,
407
+ });
408
+ runCommand("docker", [
409
+ "run",
410
+ "--detach",
411
+ "--pull=never",
412
+ "--name",
413
+ containerName,
414
+ "--label",
415
+ label,
416
+ "--publish",
417
+ `127.0.0.1:${databasePort}:5432`,
418
+ "--env",
419
+ `POSTGRES_PASSWORD=${password}`,
420
+ "--env",
421
+ `POSTGRES_DB=${database}`,
422
+ "--env",
423
+ `POSTGRES_USER=${databaseUser}`,
424
+ "--volume",
425
+ `${volumeName}:/var/lib/postgresql/data`,
426
+ image,
427
+ ], { capture: true });
428
+ await waitForDatabase();
429
+ };
430
+ const resetRuntime = async () => {
431
+ const active = await readActive();
432
+ await removeRuntime();
433
+ await mkdir(runtimeWorkdir, { recursive: true, mode: 0o700 });
434
+ return withOwnedRuntimeRollback({
435
+ operation: async function resetRuntimeOperation() {
436
+ const password = randomBytes(24).toString("hex");
437
+ await createContainer(password);
438
+ const environment = databaseEnvironment(password);
439
+ prepareProjectPrerequisites();
440
+ initializeMigrationLedger();
441
+ const baselineMigrations = await readMigrationFileInventory(new URL("./", pathToFileURL(`${active.paths.migrationsDirectory}/`)));
442
+ for (const migration of baselineMigrations) {
443
+ await applyMigration({
444
+ migration,
445
+ directory: active.paths.migrationsDirectory,
446
+ });
447
+ }
448
+ prepareProjectSchema({ baseline: active.baseline, environment });
449
+ finalizeProjectSchema();
450
+ await restoreBaselineRows({ ...active });
451
+ finalizeProjectRuntime();
452
+ const projectRuntime = configureProjectRuntime({
453
+ baseline: active.baseline,
454
+ environment,
455
+ });
456
+ await writeRuntimeEnvironment({
457
+ baseline: active.baseline,
458
+ environment,
459
+ projectRuntime,
460
+ });
461
+ await writeRuntimeMarker({
462
+ path: runtimeMarkerPath,
463
+ marker: {
464
+ formatVersion: 1,
465
+ target: "postgresql",
466
+ generationId: active.baseline.generationId,
467
+ dataSha256: active.baseline.files["sanitized-data.ndjson"].sha256,
468
+ image,
469
+ databasePassword: password,
470
+ },
471
+ });
472
+ console.log(`Restored Rehearsal baseline ${active.baseline.generationId}: ${formatCount(active.baseline.rowCount, "row")} across ${formatCount(Object.keys(active.baseline.tableCounts).length, "table")}.`);
473
+ console.log(projectRuntime.message);
474
+ return environment;
475
+ },
476
+ rollback: removeRuntime,
477
+ });
478
+ };
479
+ const startRuntime = async () => {
480
+ const { baseline } = await readActive();
481
+ if (!(await pathExists(runtimeMarkerPath)))
482
+ return resetRuntime();
483
+ const mismatchMessage = "The local Rehearsal runtime does not match the active baseline; reset it before continuing.";
484
+ const marker = await readMatchingRuntimeMarker({
485
+ path: runtimeMarkerPath,
486
+ expected: {
487
+ target: "postgresql",
488
+ generationId: baseline.generationId,
489
+ dataSha256: baseline.files["sanitized-data.ndjson"].sha256,
490
+ image,
491
+ },
492
+ mismatchMessage,
493
+ });
494
+ if (typeof marker.databasePassword !== "string" ||
495
+ marker.databasePassword.length < 32)
496
+ throw new Error(mismatchMessage);
497
+ if (!assertOwnedResource("container", containerName)) {
498
+ return resetRuntime();
499
+ }
500
+ if (!commandSucceeds("docker", [
501
+ "inspect",
502
+ "--format",
503
+ "{{.State.Running}}",
504
+ containerName,
505
+ ])) {
506
+ throw new Error("The disposable PostgreSQL container cannot be inspected.");
507
+ }
508
+ const running = runCommand("docker", ["inspect", "--format", "{{.State.Running}}", containerName], { capture: true }).stdout.trim();
509
+ if (running !== "true") {
510
+ runCommand("docker", ["start", containerName], { capture: true });
511
+ }
512
+ await waitForDatabase();
513
+ const environment = databaseEnvironment(marker.databasePassword);
514
+ const projectRuntime = configureProjectRuntime({ baseline, environment });
515
+ await writeRuntimeEnvironment({ baseline, environment, projectRuntime });
516
+ return environment;
517
+ };
518
+ const readCandidateReceipt = (baseline) => readCandidateMigrationPlan({
519
+ baseline,
520
+ migrationDirectory: applicationMigrations,
521
+ });
522
+ const verifyRuntime = async () => {
523
+ const { baseline } = await readActive();
524
+ const environment = await startRuntime();
525
+ const receipt = await readCandidateReceipt(baseline);
526
+ const migrationVerification = verifyMigrationLedger({
527
+ currentFiles: receipt.currentFiles,
528
+ baseline,
529
+ });
530
+ let projectVerification;
531
+ if (runtimePolicy)
532
+ runPsql(buildRuntimeVerificationSql(runtimePolicy));
533
+ if (runtimeAdapter?.verifyRuntime) {
534
+ projectVerification = runtimeAdapter.verifyRuntime({
535
+ baseline,
536
+ environment,
537
+ runSql: runPsql,
538
+ });
539
+ if (!projectVerification ||
540
+ typeof projectVerification.message !== "string") {
541
+ throw new Error("The project runtime adapter returned an invalid verification result.");
542
+ }
543
+ }
544
+ console.log(`Verified Rehearsal runtime ${baseline.generationId}: immutable baseline and migration history are valid with ${formatCount(migrationVerification.appliedCandidateCount, "applied candidate migration")}. Runtime rows may differ from the baseline until the next reset.`);
545
+ if (projectVerification)
546
+ console.log(projectVerification.message);
547
+ return { baseline, receipt, migrationVerification };
548
+ };
549
+ const migrateRuntime = async () => {
550
+ const { baseline } = await readActive();
551
+ await startRuntime();
552
+ const receipt = await readCandidateReceipt(baseline);
553
+ assertCandidateConfirmation({
554
+ plan: receipt,
555
+ confirmation: invocation.confirmation,
556
+ });
557
+ return withOwnedRuntimeRollback({
558
+ operation: async function migrateRuntimeOperation() {
559
+ const ledger = readRuntimeMigrationLedger();
560
+ for (const migration of receipt.candidates.slice(Math.max(0, ledger.length - Object.keys(baseline.migrations).length))) {
561
+ await applyMigration({
562
+ migration,
563
+ directory: configuredPaths.migrationDirectory,
564
+ });
565
+ }
566
+ const verification = verifyMigrationLedger({
567
+ currentFiles: receipt.currentFiles,
568
+ baseline,
569
+ });
570
+ if (verification.appliedCandidateCount !== receipt.candidates.length) {
571
+ throw new Error("Applied PostgreSQL migration history does not match the confirmed candidate suffix.");
572
+ }
573
+ await writeCandidateMigrationReceipt({
574
+ path: runtimeCandidateReceiptPath,
575
+ target: "postgresql",
576
+ baseline,
577
+ plan: receipt,
578
+ });
579
+ console.log(receipt.candidates.length
580
+ ? `Applied ${formatCount(receipt.candidates.length, "confirmed candidate migration")} to the disposable PostgreSQL runtime.`
581
+ : `No candidate migrations follow baseline ${baseline.migrationCutoff}.`);
582
+ return receipt;
583
+ },
584
+ rollback: removeRuntime,
585
+ });
586
+ };
587
+ await runRuntimeLifecycle({
588
+ action,
589
+ operations: {
590
+ reset: resetRuntime,
591
+ start: async () => {
592
+ const environment = await startRuntime();
593
+ console.log(`Local Rehearsal PostgreSQL: ${environment.host}:${environment.port}/${environment.database}`);
594
+ },
595
+ status: async () => {
596
+ const baseline = await verifyActiveBaseline({ artifactRoot });
597
+ const receipt = await readCandidateReceipt(baseline);
598
+ const running = commandSucceeds("docker", ["info"]) &&
599
+ resourceExists("container", containerName) &&
600
+ runCommand("docker", ["inspect", "--format", "{{.State.Running}}", containerName], { capture: true }).stdout.trim() === "true";
601
+ console.log(`Local Rehearsal PostgreSQL: ${running ? `running on 127.0.0.1:${databasePort}` : "stopped"}`);
602
+ console.log(`Active sanitized baseline: ${baseline.generationId}`);
603
+ console.log(`Candidate migrations (${receipt.candidateSha256}): ${receipt.candidates.map((candidate) => candidate.filename).join(", ") || "none"}`);
604
+ },
605
+ candidates: async () => {
606
+ const baseline = await verifyActiveBaseline({ artifactRoot });
607
+ const receipt = await readCandidateReceipt(baseline);
608
+ console.log(`Candidate migrations (${receipt.candidateSha256}): ${receipt.candidates.map((candidate) => candidate.filename).join(", ") || "none"}`);
609
+ },
610
+ migrate: migrateRuntime,
611
+ verify: verifyRuntime,
612
+ stop: async () => {
613
+ if (commandSucceeds("docker", ["info"]) &&
614
+ assertOwnedResource("container", containerName)) {
615
+ runCommand("docker", ["stop", containerName], { capture: true });
616
+ }
617
+ },
618
+ discard: removeRuntime,
619
+ },
620
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Purpose: Restore and manage the disposable local Rehearsal Supabase runtime from
3
+ * the active verified sanitized baseline.
4
+ * Run: `npm run db:rehearsal:reset`, `npm run db:rehearsal:start`,
5
+ * `npm run db:rehearsal:status`, or `npm run db:rehearsal:stop`.
6
+ * Reset deletes only the dedicated local Rehearsal runtime and its Docker volumes.
7
+ */
8
+ export {};