@rehearsal-db/core 0.1.0-beta.9 → 0.1.0-rc.2

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 (247) hide show
  1. package/CHANGELOG.md +319 -1
  2. package/COMPATIBILITY.md +15 -0
  3. package/README.md +73 -16
  4. package/dist/package.json +100 -0
  5. package/dist/scripts/install/create_root_config.d.mts +3 -0
  6. package/dist/scripts/install/create_root_config.mjs +13 -0
  7. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  8. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  9. package/dist/src/application/interactive_task.d.mts +30 -0
  10. package/dist/src/application/interactive_task.mjs +83 -0
  11. package/dist/src/application/session.d.mts +97 -0
  12. package/dist/src/application/session.mjs +380 -0
  13. package/dist/src/baseline/artifact.d.mts +119 -0
  14. package/dist/src/baseline/artifact.mjs +522 -0
  15. package/dist/src/baseline/builder.d.mts +19 -0
  16. package/dist/src/baseline/builder.mjs +142 -0
  17. package/dist/src/baseline/input_discovery.d.mts +13 -0
  18. package/dist/src/baseline/input_discovery.mjs +149 -0
  19. package/dist/src/baseline/policy_review.d.mts +56 -0
  20. package/dist/src/baseline/policy_review.mjs +73 -0
  21. package/dist/src/baseline/preparation.d.mts +63 -0
  22. package/dist/src/baseline/preparation.mjs +295 -0
  23. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  24. package/dist/src/baseline/privacy_contract.mjs +2 -0
  25. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  26. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  27. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  28. package/dist/src/baseline/privacy_engine.mjs +4 -0
  29. package/dist/src/baseline/privacy_key.d.mts +7 -0
  30. package/dist/src/baseline/privacy_key.mjs +42 -0
  31. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  32. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  33. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  34. package/dist/src/baseline/privacy_policy.mjs +237 -0
  35. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  36. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  37. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  38. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  39. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  40. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  41. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  42. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  43. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  44. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  45. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  46. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  47. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  48. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  49. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  50. package/dist/src/cli/application_command_handlers.mjs +52 -0
  51. package/dist/src/cli/arguments.d.mts +38 -0
  52. package/dist/src/cli/arguments.mjs +130 -0
  53. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  54. package/dist/src/cli/basic_command_handlers.mjs +57 -0
  55. package/dist/src/cli/cli_commands.d.mts +14 -0
  56. package/dist/src/cli/cli_commands.mjs +34 -0
  57. package/dist/src/cli/command_contract.d.mts +57 -0
  58. package/dist/src/cli/command_contract.mjs +39 -0
  59. package/dist/src/cli/command_output.d.mts +12 -0
  60. package/dist/src/cli/command_output.mjs +20 -0
  61. package/dist/src/cli/command_registry.d.mts +7 -0
  62. package/dist/src/cli/command_registry.mjs +14 -0
  63. package/dist/src/cli/guided.d.mts +53 -0
  64. package/dist/src/cli/guided.mjs +727 -0
  65. package/dist/src/cli/identity_command_handlers.d.mts +12 -0
  66. package/dist/src/cli/identity_command_handlers.mjs +239 -0
  67. package/dist/src/cli/identity_renderer.d.mts +8 -0
  68. package/dist/src/cli/identity_renderer.mjs +33 -0
  69. package/dist/src/cli/init_command.d.mts +21 -0
  70. package/dist/src/cli/init_command.mjs +90 -0
  71. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  72. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  73. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  74. package/dist/src/cli/project_command_handlers.mjs +92 -0
  75. package/dist/src/cli/project_commands.d.mts +283 -0
  76. package/dist/src/cli/project_commands.mjs +192 -0
  77. package/dist/src/cli/rehearsal.d.mts +7 -0
  78. package/dist/src/cli/rehearsal.mjs +190 -0
  79. package/dist/src/cli/renderers.d.mts +299 -0
  80. package/dist/src/cli/renderers.mjs +554 -0
  81. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  82. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  83. package/dist/src/cli/runtime_commands.d.mts +294 -0
  84. package/dist/src/cli/runtime_commands.mjs +619 -0
  85. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  86. package/dist/src/cli/source_command_handlers.mjs +62 -0
  87. package/dist/src/cli/source_commands.d.mts +436 -0
  88. package/dist/src/cli/source_commands.mjs +301 -0
  89. package/dist/src/cli/terminal.d.mts +59 -0
  90. package/dist/src/cli/terminal.mjs +230 -0
  91. package/dist/src/identity/claim.d.mts +5 -0
  92. package/dist/src/identity/claim.mjs +4 -0
  93. package/dist/src/identity/claim_contract.d.mts +135 -0
  94. package/dist/src/identity/claim_contract.mjs +6 -0
  95. package/dist/src/identity/claim_executor.d.mts +10 -0
  96. package/dist/src/identity/claim_executor.mjs +358 -0
  97. package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
  98. package/dist/src/identity/claim_identity_lookup.mjs +134 -0
  99. package/dist/src/identity/claim_plan.d.mts +8 -0
  100. package/dist/src/identity/claim_plan.mjs +61 -0
  101. package/dist/src/identity/claim_policy.d.mts +3 -0
  102. package/dist/src/identity/claim_policy.mjs +377 -0
  103. package/dist/src/identity/claim_sql.d.mts +10 -0
  104. package/dist/src/identity/claim_sql.mjs +46 -0
  105. package/dist/src/identity/claim_verification.d.mts +13 -0
  106. package/dist/src/identity/claim_verification.mjs +35 -0
  107. package/dist/src/identity/connect.d.mts +30 -0
  108. package/dist/src/identity/connect.mjs +111 -0
  109. package/dist/src/identity/provider_configuration.d.mts +37 -0
  110. package/dist/src/identity/provider_configuration.mjs +132 -0
  111. package/dist/src/identity/storage.d.mts +21 -0
  112. package/dist/src/identity/storage.mjs +122 -0
  113. package/dist/src/index.d.mts +4 -0
  114. package/dist/src/index.mjs +3 -0
  115. package/dist/src/project/configuration.d.mts +13 -0
  116. package/dist/src/project/configuration.mjs +9 -0
  117. package/dist/src/project/configuration_contract.d.mts +125 -0
  118. package/dist/src/project/configuration_contract.mjs +27 -0
  119. package/dist/src/project/configuration_discovery.d.mts +7 -0
  120. package/dist/src/project/configuration_discovery.mjs +37 -0
  121. package/dist/src/project/configuration_normalization.d.mts +107 -0
  122. package/dist/src/project/configuration_normalization.mjs +302 -0
  123. package/dist/src/project/configuration_rendering.d.mts +14 -0
  124. package/dist/src/project/configuration_rendering.mjs +228 -0
  125. package/dist/src/project/configuration_resolution.d.mts +125 -0
  126. package/dist/src/project/configuration_resolution.mjs +81 -0
  127. package/dist/src/project/configuration_schema.d.mts +22 -0
  128. package/dist/src/project/configuration_schema.mjs +160 -0
  129. package/dist/src/project/configuration_validation.d.mts +22 -0
  130. package/dist/src/project/configuration_validation.mjs +136 -0
  131. package/dist/src/project/install_scaffold.d.mts +42 -0
  132. package/dist/src/project/install_scaffold.mjs +263 -0
  133. package/dist/src/project/project_detection.d.mts +14 -0
  134. package/dist/src/project/project_detection.mjs +47 -0
  135. package/dist/src/project/setup.d.mts +87 -0
  136. package/dist/src/project/setup.mjs +414 -0
  137. package/dist/src/project/support_report.d.mts +88 -0
  138. package/dist/src/project/support_report.mjs +83 -0
  139. package/dist/src/runtime/cleanup.d.mts +85 -0
  140. package/dist/src/runtime/cleanup.mjs +311 -0
  141. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  142. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  143. package/dist/src/runtime/migration_history.d.mts +51 -0
  144. package/dist/src/runtime/migration_history.mjs +170 -0
  145. package/dist/src/runtime/plan.d.mts +134 -0
  146. package/dist/src/runtime/plan.mjs +640 -0
  147. package/dist/src/runtime/policy.d.mts +66 -0
  148. package/dist/src/runtime/policy.mjs +369 -0
  149. package/dist/src/runtime/restore.d.mts +22 -0
  150. package/dist/src/runtime/restore.mjs +276 -0
  151. package/dist/src/runtime/service_environment.d.mts +11 -0
  152. package/dist/src/runtime/service_environment.mjs +58 -0
  153. package/dist/src/runtime/topology.d.mts +51 -0
  154. package/dist/src/runtime/topology.mjs +128 -0
  155. package/dist/src/shared/diagnostics.d.mts +97 -0
  156. package/dist/src/shared/diagnostics.mjs +170 -0
  157. package/dist/src/shared/human_output.d.mts +2 -0
  158. package/{src → dist/src}/shared/human_output.mjs +1 -3
  159. package/dist/src/shared/node_runtime.d.mts +14 -0
  160. package/dist/src/shared/node_runtime.mjs +25 -0
  161. package/dist/src/shared/operation_guard.d.mts +22 -0
  162. package/dist/src/shared/operation_guard.mjs +150 -0
  163. package/dist/src/shared/process_environment.d.mts +15 -0
  164. package/dist/src/shared/process_environment.mjs +50 -0
  165. package/dist/src/source/access.d.mts +165 -0
  166. package/dist/src/source/access.mjs +602 -0
  167. package/dist/src/source/asset_transfer.d.mts +27 -0
  168. package/dist/src/source/asset_transfer.mjs +202 -0
  169. package/dist/src/source/baseline.d.mts +66 -0
  170. package/dist/src/source/baseline.mjs +372 -0
  171. package/dist/src/source/external_reader.d.mts +42 -0
  172. package/dist/src/source/external_reader.mjs +278 -0
  173. package/dist/src/source/postgresql_access.d.mts +86 -0
  174. package/dist/src/source/postgresql_access.mjs +454 -0
  175. package/dist/src/source/privacy_paths.d.mts +10 -0
  176. package/dist/src/source/privacy_paths.mjs +27 -0
  177. package/dist/src/targets/driver.d.mts +2 -0
  178. package/dist/src/targets/driver.mjs +13 -0
  179. package/dist/src/targets/postgresql.d.mts +5 -0
  180. package/dist/src/targets/postgresql.mjs +620 -0
  181. package/dist/src/targets/supabase.d.mts +8 -0
  182. package/dist/src/targets/supabase.mjs +629 -0
  183. package/dist/src/targets/supabase_environment.d.mts +54 -0
  184. package/dist/src/targets/supabase_environment.mjs +184 -0
  185. package/dist/src/targets/target.d.mts +33 -0
  186. package/dist/src/targets/target.mjs +39 -0
  187. package/docs/README.md +35 -16
  188. package/docs/architecture.md +135 -10
  189. package/docs/commands.md +53 -15
  190. package/docs/configuration.md +186 -31
  191. package/docs/getting-started.md +66 -9
  192. package/docs/index.md +67 -0
  193. package/docs/production-source.md +175 -17
  194. package/docs/releasing.md +24 -12
  195. package/docs/roadmap.md +3 -2
  196. package/docs/runtime-lifecycle.md +99 -0
  197. package/docs/runtime-policies.md +80 -7
  198. package/docs/sanitization.md +572 -4
  199. package/docs/security-model.md +8 -0
  200. package/docs/stability.md +70 -0
  201. package/docs/standalone-workflow.md +36 -13
  202. package/docs/troubleshooting.md +82 -10
  203. package/docs/tutorial.md +7 -1
  204. package/docs/typescript.md +134 -0
  205. package/package.json +44 -29
  206. package/types/configuration.d.mts +5 -0
  207. package/src/README.md +0 -17
  208. package/src/application/session.mjs +0 -313
  209. package/src/baseline/artifact.mjs +0 -661
  210. package/src/baseline/builder.mjs +0 -164
  211. package/src/baseline/input_discovery.mjs +0 -155
  212. package/src/baseline/policy_review.mjs +0 -92
  213. package/src/baseline/preparation.mjs +0 -323
  214. package/src/baseline/privacy_engine.mjs +0 -413
  215. package/src/baseline/sanitization_policy.mjs +0 -299
  216. package/src/baseline/schema_snapshot.mjs +0 -113
  217. package/src/cli/arguments.mjs +0 -120
  218. package/src/cli/guided.mjs +0 -807
  219. package/src/cli/rehearsal.mjs +0 -933
  220. package/src/cli/renderers.mjs +0 -584
  221. package/src/cli/runtime_commands.mjs +0 -553
  222. package/src/cli/source_commands.mjs +0 -326
  223. package/src/cli/terminal.mjs +0 -275
  224. package/src/identity/claim.mjs +0 -975
  225. package/src/identity/storage.mjs +0 -165
  226. package/src/project/configuration.d.mts +0 -153
  227. package/src/project/configuration.mjs +0 -1197
  228. package/src/project/setup.mjs +0 -379
  229. package/src/project/support_report.mjs +0 -105
  230. package/src/runtime/cleanup.mjs +0 -381
  231. package/src/runtime/migration_history.mjs +0 -220
  232. package/src/runtime/plan.mjs +0 -685
  233. package/src/runtime/policy.mjs +0 -438
  234. package/src/runtime/restore.mjs +0 -329
  235. package/src/runtime/service_environment.mjs +0 -82
  236. package/src/runtime/topology.mjs +0 -177
  237. package/src/shared/diagnostics.mjs +0 -199
  238. package/src/shared/operation_guard.mjs +0 -162
  239. package/src/shared/process_environment.mjs +0 -68
  240. package/src/source/access.mjs +0 -578
  241. package/src/source/asset_transfer.mjs +0 -177
  242. package/src/source/baseline.mjs +0 -446
  243. package/src/source/postgresql_access.mjs +0 -480
  244. package/src/targets/postgresql.mjs +0 -808
  245. package/src/targets/supabase.mjs +0 -830
  246. package/src/targets/supabase_environment.mjs +0 -229
  247. package/src/targets/target.mjs +0 -66
@@ -1,1197 +0,0 @@
1
- /**
2
- * Purpose: Load and strictly validate the versioned, project-owned Rehearsal
3
- * configuration contract. Do not run directly; this module is reusable script
4
- * infrastructure.
5
- */
6
-
7
- import { access, readFile, stat } from "node:fs/promises";
8
- import { basename, isAbsolute, join, relative, resolve } from "node:path";
9
- import { pathToFileURL } from "node:url";
10
- import { resolveRuntimeTarget } from "../targets/target.mjs";
11
-
12
- export {
13
- EXCLUDED_VALUE,
14
- SANITIZATION_ACTIONS,
15
- applySanitizationAction,
16
- normalizeSanitizationAction,
17
- readBoundRuntimeSanitizationPolicy,
18
- validateSanitizationCoverage,
19
- validateRuntimeSanitizationPolicy,
20
- } from "../baseline/sanitization_policy.mjs";
21
-
22
- export const REHEARSAL_CONFIG_VERSION = 1;
23
-
24
- export const REHEARSAL_DEFAULTS = Object.freeze({
25
- baseline: Object.freeze({ artifactDirectory: ".rehearsal" }),
26
- containerRuntime: Object.freeze({ autoStartColima: true }),
27
- cleanup: Object.freeze({ retainBaselineGenerations: 2 }),
28
- runtime: Object.freeze({
29
- applicationUrl: "http://localhost:5175",
30
- projectId: "rehearsal-local",
31
- }),
32
- safety: Object.freeze({
33
- allowedHosts: Object.freeze(["127.0.0.1", "::1", "localhost"]),
34
- blockedEnvironmentVariables: Object.freeze([
35
- "DATABASE_URL",
36
- "PGHOST",
37
- "PGPASSWORD",
38
- "PGPORT",
39
- "PGUSER",
40
- "SUPABASE_ACCESS_TOKEN",
41
- "SUPABASE_DB_PASSWORD",
42
- "SUPABASE_PROJECT_ID",
43
- ]),
44
- hostedAccess: "disabled",
45
- outboundNetwork: "deny",
46
- }),
47
- });
48
-
49
- const CONFIG_FILENAMES = Object.freeze([
50
- "rehearsal.config.mjs",
51
- "rehearsal.config.js",
52
- "rehearsal.config.ts",
53
- join("infrastructure", "rehearsal", "rehearsal.config.mjs"),
54
- join("infrastructure", "rehearsal", "rehearsal.config.ts"),
55
- ]);
56
-
57
- const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
58
- const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}$/u;
59
- const DATABASE_IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]{0,62}$/u;
60
- const POSTGRES_IMAGE_PATTERN = /^postgres:\d+(?:\.\d+)?-alpine$/u;
61
- const SAFE_COMMAND_PATTERN = /^[^\n\r\0]+$/u;
62
- const ENVIRONMENT_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
63
- const ENVIRONMENT_REFERENCE_PATTERN = /^[a-z0-9][a-z0-9-]*:[A-Z][A-Z0-9_]*$/u;
64
-
65
- const isPlainObject = (value) =>
66
- value !== null &&
67
- typeof value === "object" &&
68
- !Array.isArray(value) &&
69
- Object.getPrototypeOf(value) === Object.prototype;
70
-
71
- const assertPlainObject = (value, path) => {
72
- if (!isPlainObject(value)) {
73
- throw new Error(`${path} must be an object.`);
74
- }
75
- return value;
76
- };
77
-
78
- const assertKnownKeys = (value, keys, path) => {
79
- for (const key of Object.keys(value)) {
80
- if (!keys.includes(key)) {
81
- throw new Error(
82
- `Unknown Rehearsal configuration property: ${path}.${key}.`,
83
- );
84
- }
85
- }
86
- };
87
-
88
- const assertNonEmptyString = (value, path) => {
89
- if (typeof value !== "string" || !value.trim()) {
90
- throw new Error(`${path} must be a non-empty string.`);
91
- }
92
- return value.trim();
93
- };
94
-
95
- const assertRelativePath = (value, path) => {
96
- const normalized = assertNonEmptyString(value, path);
97
- if (isAbsolute(normalized) || normalized.split(/[\\/]/u).includes("..")) {
98
- throw new Error(`${path} must stay inside the project root.`);
99
- }
100
- return normalized;
101
- };
102
-
103
- const assertCommand = (value, path) => {
104
- const command = assertNonEmptyString(value, path);
105
- if (!SAFE_COMMAND_PATTERN.test(command)) {
106
- throw new Error(`${path} contains an unsafe control character.`);
107
- }
108
- return command;
109
- };
110
-
111
- const assertPort = (value, path) => {
112
- if (!Number.isSafeInteger(value) || value < 1024 || value > 65_535) {
113
- throw new Error(`${path} must be a non-privileged TCP port.`);
114
- }
115
- return value;
116
- };
117
-
118
- const assertBoolean = (value, path) => {
119
- if (typeof value !== "boolean") {
120
- throw new Error(`${path} must be true or false.`);
121
- }
122
- return value;
123
- };
124
-
125
- const assertPositiveInteger = (value, path) => {
126
- if (!Number.isSafeInteger(value) || value < 1) {
127
- throw new Error(`${path} must be a positive integer.`);
128
- }
129
- return value;
130
- };
131
-
132
- const assertStringArray = (value, path) => {
133
- if (
134
- !Array.isArray(value) ||
135
- value.length === 0 ||
136
- value.some((entry) => typeof entry !== "string" || !entry.trim())
137
- ) {
138
- throw new Error(`${path} must be a non-empty string array.`);
139
- }
140
- return [...new Set(value.map((entry) => entry.trim()))];
141
- };
142
-
143
- const normalizeApplicationEnvironmentMappings = (value = {}) => {
144
- const mappings = assertPlainObject(
145
- value,
146
- "config.application.environmentVariables",
147
- );
148
- return Object.freeze(
149
- Object.fromEntries(
150
- Object.entries(mappings).map(([key, reference]) => {
151
- if (!ENVIRONMENT_KEY_PATTERN.test(key)) {
152
- throw new Error(
153
- `config.application.environmentVariables.${key} is not a safe environment variable name.`,
154
- );
155
- }
156
- if (
157
- typeof reference !== "string" ||
158
- !ENVIRONMENT_REFERENCE_PATTERN.test(reference)
159
- ) {
160
- throw new Error(
161
- `config.application.environmentVariables.${key} must reference target:VARIABLE, such as primary:DATABASE_URL.`,
162
- );
163
- }
164
- return [key, reference];
165
- }),
166
- ),
167
- );
168
- };
169
-
170
- const assertLoopbackUrl = (value, path, allowedHosts) => {
171
- let parsed;
172
- try {
173
- parsed = new URL(assertNonEmptyString(value, path));
174
- } catch {
175
- throw new Error(`${path} must be a valid loopback URL.`);
176
- }
177
- if (
178
- !allowedHosts.includes(parsed.hostname) ||
179
- !LOOPBACK_HOSTS.has(parsed.hostname)
180
- ) {
181
- throw new Error(`${path} must use an explicitly allowed loopback host.`);
182
- }
183
- if (!["http:", "https:"].includes(parsed.protocol)) {
184
- throw new Error(`${path} must use HTTP or HTTPS.`);
185
- }
186
- return parsed.toString().replace(/\/$/u, "");
187
- };
188
-
189
- const normalizeConfig = (input) => {
190
- const root = assertPlainObject(input, "config");
191
- assertKnownKeys(
192
- root,
193
- [
194
- "schemaVersion",
195
- "project",
196
- "supabase",
197
- "postgresql",
198
- "baseline",
199
- "containerRuntime",
200
- "cleanup",
201
- "preparation",
202
- "runtimePolicy",
203
- "identityPolicy",
204
- "dependentTargets",
205
- "application",
206
- "runtime",
207
- "safety",
208
- "verification",
209
- ],
210
- "config",
211
- );
212
- if (root.schemaVersion !== REHEARSAL_CONFIG_VERSION) {
213
- throw new Error(
214
- `Unsupported Rehearsal configuration version: ${String(root.schemaVersion)}. Expected ${REHEARSAL_CONFIG_VERSION}.`,
215
- );
216
- }
217
-
218
- const project = assertPlainObject(root.project, "config.project");
219
- assertKnownKeys(project, ["name"], "config.project");
220
- const projectName = assertNonEmptyString(project.name, "config.project.name");
221
- if (!IDENTIFIER_PATTERN.test(projectName)) {
222
- throw new Error(
223
- "config.project.name must contain only lowercase letters, digits, and hyphens.",
224
- );
225
- }
226
-
227
- const runtimeTarget = resolveRuntimeTarget(root.runtime?.target).id;
228
- const supabase =
229
- runtimeTarget === "supabase"
230
- ? assertPlainObject(root.supabase, "config.supabase")
231
- : null;
232
- const postgresql =
233
- runtimeTarget === "postgresql"
234
- ? assertPlainObject(root.postgresql, "config.postgresql")
235
- : null;
236
- if (runtimeTarget === "supabase" && root.postgresql !== undefined) {
237
- throw new Error(
238
- "config.postgresql is not allowed when config.runtime.target is supabase.",
239
- );
240
- }
241
- if (runtimeTarget === "postgresql" && root.supabase !== undefined) {
242
- throw new Error(
243
- "config.supabase is not allowed when config.runtime.target is postgresql.",
244
- );
245
- }
246
- if (supabase) {
247
- assertKnownKeys(
248
- supabase,
249
- [
250
- "workdir",
251
- "migrationDirectory",
252
- "rehearsalConfig",
253
- "runtimeWorkdir",
254
- "serviceEnvironmentFile",
255
- "serviceEnvironmentVariables",
256
- ],
257
- "config.supabase",
258
- );
259
- }
260
- if (postgresql) {
261
- assertKnownKeys(
262
- postgresql,
263
- ["migrationDirectory", "runtimeWorkdir", "image", "database", "user"],
264
- "config.postgresql",
265
- );
266
- }
267
- const baseline = assertPlainObject(root.baseline, "config.baseline");
268
- assertKnownKeys(
269
- baseline,
270
- ["artifactDirectory", "sanitizationPolicy"],
271
- "config.baseline",
272
- );
273
- const containerRuntime = assertPlainObject(
274
- root.containerRuntime ?? {},
275
- "config.containerRuntime",
276
- );
277
- assertKnownKeys(
278
- containerRuntime,
279
- ["autoStartColima"],
280
- "config.containerRuntime",
281
- );
282
- const cleanup = assertPlainObject(root.cleanup ?? {}, "config.cleanup");
283
- assertKnownKeys(cleanup, ["retainBaselineGenerations"], "config.cleanup");
284
- const preparation =
285
- root.preparation === undefined
286
- ? null
287
- : assertPlainObject(root.preparation, "config.preparation");
288
- if (preparation) {
289
- assertKnownKeys(
290
- preparation,
291
- [
292
- "sourcePolicy",
293
- "privacyKey",
294
- "batchRows",
295
- "maximumRows",
296
- "maximumBytes",
297
- "diskHeadroomBytes",
298
- ],
299
- "config.preparation",
300
- );
301
- }
302
- const dependentTargets = root.dependentTargets ?? [];
303
- if (!Array.isArray(dependentTargets)) {
304
- throw new Error("config.dependentTargets must be an array.");
305
- }
306
- const normalizedDependentTargets = dependentTargets.map((entry, index) => {
307
- const path = `config.dependentTargets[${index}]`;
308
- const target = assertPlainObject(entry, path);
309
- assertKnownKeys(target, ["name", "configPath", "prepareCommand"], path);
310
- const name = assertNonEmptyString(target.name, `${path}.name`);
311
- if (!IDENTIFIER_PATTERN.test(name)) {
312
- throw new Error(
313
- `${path}.name must contain only lowercase letters, digits, and hyphens.`,
314
- );
315
- }
316
- return Object.freeze({
317
- name,
318
- configPath: assertRelativePath(target.configPath, `${path}.configPath`),
319
- prepareCommand:
320
- target.prepareCommand === undefined
321
- ? null
322
- : assertCommand(target.prepareCommand, `${path}.prepareCommand`),
323
- });
324
- });
325
- if (
326
- new Set(normalizedDependentTargets.map((target) => target.name)).size !==
327
- normalizedDependentTargets.length
328
- ) {
329
- throw new Error("config.dependentTargets names must be unique.");
330
- }
331
- const application = assertPlainObject(root.application, "config.application");
332
- assertKnownKeys(
333
- application,
334
- [
335
- "startCommand",
336
- "proofCommand",
337
- "environmentFile",
338
- "environmentVariables",
339
- "readiness",
340
- "httpProofs",
341
- "runtimeAdapter",
342
- ],
343
- "config.application",
344
- );
345
- const readiness =
346
- application.readiness === undefined
347
- ? null
348
- : assertPlainObject(
349
- application.readiness,
350
- "config.application.readiness",
351
- );
352
- if (readiness) {
353
- assertKnownKeys(
354
- readiness,
355
- ["url", "expectedStatus", "timeoutSeconds"],
356
- "config.application.readiness",
357
- );
358
- }
359
- const httpProofs = application.httpProofs ?? [];
360
- if (!Array.isArray(httpProofs)) {
361
- throw new Error("config.application.httpProofs must be an array.");
362
- }
363
- if (
364
- httpProofs.length > 0 &&
365
- (!httpProofs.some((proof) => proof?.kind === "positive") ||
366
- !httpProofs.some((proof) => proof?.kind === "negative"))
367
- ) {
368
- throw new Error(
369
- "config.application.httpProofs must include at least one positive and one negative check.",
370
- );
371
- }
372
- const runtime = assertPlainObject(root.runtime ?? {}, "config.runtime");
373
- assertKnownKeys(
374
- runtime,
375
- [
376
- "target",
377
- "applicationUrl",
378
- "projectId",
379
- "apiPort",
380
- "databasePort",
381
- "studioPort",
382
- ],
383
- "config.runtime",
384
- );
385
- const safety = assertPlainObject(root.safety ?? {}, "config.safety");
386
- assertKnownKeys(
387
- safety,
388
- [
389
- "allowedHosts",
390
- "blockedEnvironmentVariables",
391
- "authenticationProviders",
392
- "hostedAccess",
393
- "outboundNetwork",
394
- ],
395
- "config.safety",
396
- );
397
- const verification = assertPlainObject(
398
- root.verification ?? {},
399
- "config.verification",
400
- );
401
- assertKnownKeys(verification, ["commands"], "config.verification");
402
-
403
- const allowedHosts = assertStringArray(
404
- safety.allowedHosts ?? REHEARSAL_DEFAULTS.safety.allowedHosts,
405
- "config.safety.allowedHosts",
406
- );
407
- if (allowedHosts.some((host) => !LOOPBACK_HOSTS.has(host))) {
408
- throw new Error(
409
- "config.safety.allowedHosts may contain loopback hosts only in configuration version 1.",
410
- );
411
- }
412
- if ((safety.hostedAccess ?? "disabled") !== "disabled") {
413
- throw new Error(
414
- "config.safety.hostedAccess must remain disabled in configuration version 1.",
415
- );
416
- }
417
- if ((safety.outboundNetwork ?? "deny") !== "deny") {
418
- throw new Error(
419
- "config.safety.outboundNetwork must remain deny in configuration version 1.",
420
- );
421
- }
422
-
423
- const ports = {
424
- ...(runtimeTarget === "supabase"
425
- ? { api: assertPort(runtime.apiPort, "config.runtime.apiPort") }
426
- : {}),
427
- database: assertPort(runtime.databasePort, "config.runtime.databasePort"),
428
- ...(runtimeTarget === "supabase"
429
- ? { studio: assertPort(runtime.studioPort, "config.runtime.studioPort") }
430
- : {}),
431
- };
432
- if (new Set(Object.values(ports)).size !== Object.values(ports).length) {
433
- throw new Error("Rehearsal runtime ports must be unique.");
434
- }
435
-
436
- const commands = verification.commands ?? [];
437
- if (!Array.isArray(commands)) {
438
- throw new Error("config.verification.commands must be an array.");
439
- }
440
- const serviceEnvironmentVariables =
441
- supabase?.serviceEnvironmentVariables ?? [];
442
- if (!Array.isArray(serviceEnvironmentVariables)) {
443
- throw new Error(
444
- "config.supabase.serviceEnvironmentVariables must be an array.",
445
- );
446
- }
447
- const normalizedServiceEnvironmentVariables = serviceEnvironmentVariables.map(
448
- (value, index) =>
449
- assertNonEmptyString(
450
- value,
451
- `config.supabase.serviceEnvironmentVariables[${index}]`,
452
- ),
453
- );
454
- if (
455
- new Set(normalizedServiceEnvironmentVariables).size !==
456
- normalizedServiceEnvironmentVariables.length
457
- ) {
458
- throw new Error(
459
- "config.supabase.serviceEnvironmentVariables must not contain duplicates.",
460
- );
461
- }
462
- if (
463
- Boolean(supabase?.serviceEnvironmentFile) !==
464
- Boolean(normalizedServiceEnvironmentVariables.length)
465
- ) {
466
- throw new Error(
467
- "config.supabase.serviceEnvironmentFile and serviceEnvironmentVariables must be configured together.",
468
- );
469
- }
470
- const authenticationProviders = safety.authenticationProviders
471
- ? assertStringArray(
472
- safety.authenticationProviders,
473
- "config.safety.authenticationProviders",
474
- )
475
- : [];
476
- if (runtimeTarget === "postgresql" && authenticationProviders.length > 0) {
477
- throw new Error(
478
- "config.safety.authenticationProviders is available only for the Supabase target.",
479
- );
480
- }
481
-
482
- return Object.freeze({
483
- schemaVersion: REHEARSAL_CONFIG_VERSION,
484
- project: Object.freeze({ name: projectName }),
485
- supabase: supabase
486
- ? Object.freeze({
487
- workdir: assertRelativePath(
488
- supabase.workdir,
489
- "config.supabase.workdir",
490
- ),
491
- migrationDirectory: assertRelativePath(
492
- supabase.migrationDirectory,
493
- "config.supabase.migrationDirectory",
494
- ),
495
- rehearsalConfig: assertRelativePath(
496
- supabase.rehearsalConfig,
497
- "config.supabase.rehearsalConfig",
498
- ),
499
- runtimeWorkdir: assertRelativePath(
500
- supabase.runtimeWorkdir,
501
- "config.supabase.runtimeWorkdir",
502
- ),
503
- serviceEnvironmentFile: supabase.serviceEnvironmentFile
504
- ? assertRelativePath(
505
- supabase.serviceEnvironmentFile,
506
- "config.supabase.serviceEnvironmentFile",
507
- )
508
- : null,
509
- serviceEnvironmentVariables: Object.freeze(
510
- normalizedServiceEnvironmentVariables,
511
- ),
512
- })
513
- : null,
514
- postgresql: postgresql
515
- ? Object.freeze({
516
- migrationDirectory: assertRelativePath(
517
- postgresql.migrationDirectory,
518
- "config.postgresql.migrationDirectory",
519
- ),
520
- runtimeWorkdir: assertRelativePath(
521
- postgresql.runtimeWorkdir ?? ".rehearsal/runtime",
522
- "config.postgresql.runtimeWorkdir",
523
- ),
524
- image: (() => {
525
- const image = assertNonEmptyString(
526
- postgresql.image ?? "postgres:17-alpine",
527
- "config.postgresql.image",
528
- );
529
- if (!POSTGRES_IMAGE_PATTERN.test(image)) {
530
- throw new Error(
531
- "config.postgresql.image must name an official versioned Alpine PostgreSQL image, such as postgres:17-alpine.",
532
- );
533
- }
534
- return image;
535
- })(),
536
- database: (() => {
537
- const database = assertNonEmptyString(
538
- postgresql.database ?? "postgres",
539
- "config.postgresql.database",
540
- );
541
- if (!DATABASE_IDENTIFIER_PATTERN.test(database)) {
542
- throw new Error(
543
- "config.postgresql.database must be a safe lowercase PostgreSQL identifier.",
544
- );
545
- }
546
- return database;
547
- })(),
548
- user: (() => {
549
- const user = assertNonEmptyString(
550
- postgresql.user ?? "postgres",
551
- "config.postgresql.user",
552
- );
553
- if (!DATABASE_IDENTIFIER_PATTERN.test(user)) {
554
- throw new Error(
555
- "config.postgresql.user must be a safe lowercase PostgreSQL identifier.",
556
- );
557
- }
558
- return user;
559
- })(),
560
- })
561
- : null,
562
- baseline: Object.freeze({
563
- artifactDirectory: assertRelativePath(
564
- baseline.artifactDirectory ??
565
- REHEARSAL_DEFAULTS.baseline.artifactDirectory,
566
- "config.baseline.artifactDirectory",
567
- ),
568
- sanitizationPolicy: assertRelativePath(
569
- baseline.sanitizationPolicy,
570
- "config.baseline.sanitizationPolicy",
571
- ),
572
- }),
573
- containerRuntime: Object.freeze({
574
- autoStartColima: assertBoolean(
575
- containerRuntime.autoStartColima ??
576
- REHEARSAL_DEFAULTS.containerRuntime.autoStartColima,
577
- "config.containerRuntime.autoStartColima",
578
- ),
579
- }),
580
- cleanup: Object.freeze({
581
- retainBaselineGenerations: assertPositiveInteger(
582
- cleanup.retainBaselineGenerations ??
583
- REHEARSAL_DEFAULTS.cleanup.retainBaselineGenerations,
584
- "config.cleanup.retainBaselineGenerations",
585
- ),
586
- }),
587
- preparation: preparation
588
- ? Object.freeze({
589
- sourcePolicy: assertRelativePath(
590
- preparation.sourcePolicy,
591
- "config.preparation.sourcePolicy",
592
- ),
593
- privacyKey: assertRelativePath(
594
- preparation.privacyKey,
595
- "config.preparation.privacyKey",
596
- ),
597
- batchRows: assertPositiveInteger(
598
- preparation.batchRows ?? 500,
599
- "config.preparation.batchRows",
600
- ),
601
- maximumRows: assertPositiveInteger(
602
- preparation.maximumRows ?? 1_000_000,
603
- "config.preparation.maximumRows",
604
- ),
605
- maximumBytes: assertPositiveInteger(
606
- preparation.maximumBytes ?? 2 * 1024 * 1024 * 1024,
607
- "config.preparation.maximumBytes",
608
- ),
609
- diskHeadroomBytes: assertPositiveInteger(
610
- preparation.diskHeadroomBytes ?? 64 * 1024 * 1024,
611
- "config.preparation.diskHeadroomBytes",
612
- ),
613
- })
614
- : null,
615
- runtimePolicy:
616
- root.runtimePolicy === undefined
617
- ? null
618
- : assertRelativePath(root.runtimePolicy, "config.runtimePolicy"),
619
- identityPolicy:
620
- root.identityPolicy === undefined
621
- ? null
622
- : assertRelativePath(root.identityPolicy, "config.identityPolicy"),
623
- dependentTargets: Object.freeze(normalizedDependentTargets),
624
- application: Object.freeze({
625
- startCommand: assertCommand(
626
- application.startCommand,
627
- "config.application.startCommand",
628
- ),
629
- proofCommand: assertCommand(
630
- application.proofCommand,
631
- "config.application.proofCommand",
632
- ),
633
- environmentFile: assertRelativePath(
634
- application.environmentFile ?? ".rehearsal/runtime.env",
635
- "config.application.environmentFile",
636
- ),
637
- environmentVariables: normalizeApplicationEnvironmentMappings(
638
- application.environmentVariables,
639
- ),
640
- readiness: readiness
641
- ? Object.freeze({
642
- url: assertLoopbackUrl(
643
- readiness.url,
644
- "config.application.readiness.url",
645
- allowedHosts,
646
- ),
647
- expectedStatus: (() => {
648
- if (
649
- !Number.isSafeInteger(readiness.expectedStatus) ||
650
- readiness.expectedStatus < 100 ||
651
- readiness.expectedStatus > 599
652
- ) {
653
- throw new Error(
654
- "config.application.readiness.expectedStatus must be an HTTP status.",
655
- );
656
- }
657
- return readiness.expectedStatus;
658
- })(),
659
- timeoutSeconds: assertPositiveInteger(
660
- readiness.timeoutSeconds ?? 30,
661
- "config.application.readiness.timeoutSeconds",
662
- ),
663
- })
664
- : null,
665
- httpProofs: Object.freeze(
666
- httpProofs.map((proof, index) => {
667
- const path = `config.application.httpProofs[${index}]`;
668
- const value = assertPlainObject(proof, path);
669
- assertKnownKeys(
670
- value,
671
- ["name", "kind", "url", "method", "expectedStatus", "json"],
672
- path,
673
- );
674
- if (!["positive", "negative"].includes(value.kind)) {
675
- throw new Error(`${path}.kind must be positive or negative.`);
676
- }
677
- if (
678
- !Number.isSafeInteger(value.expectedStatus) ||
679
- value.expectedStatus < 100 ||
680
- value.expectedStatus > 599
681
- ) {
682
- throw new Error(`${path}.expectedStatus must be an HTTP status.`);
683
- }
684
- const method = assertNonEmptyString(
685
- value.method ?? "GET",
686
- `${path}.method`,
687
- ).toUpperCase();
688
- if (!["GET", "HEAD"].includes(method)) {
689
- throw new Error(`${path}.method must be GET or HEAD.`);
690
- }
691
- let json = null;
692
- if (value.json !== undefined) {
693
- const assertion = assertPlainObject(value.json, `${path}.json`);
694
- assertKnownKeys(
695
- assertion,
696
- ["path", "equals", "notEquals", "minimumItems"],
697
- `${path}.json`,
698
- );
699
- if (
700
- !Array.isArray(assertion.path) ||
701
- assertion.path.length === 0 ||
702
- assertion.path.some(
703
- (segment) => typeof segment !== "string" || !segment,
704
- )
705
- ) {
706
- throw new Error(
707
- `${path}.json.path must be a non-empty string array.`,
708
- );
709
- }
710
- const assertionCount = [
711
- Object.hasOwn(assertion, "equals"),
712
- Object.hasOwn(assertion, "notEquals"),
713
- assertion.minimumItems !== undefined,
714
- ].filter(Boolean).length;
715
- if (assertionCount !== 1) {
716
- throw new Error(
717
- `${path}.json must declare exactly one assertion.`,
718
- );
719
- }
720
- json = Object.freeze({
721
- path: Object.freeze([...assertion.path]),
722
- ...(Object.hasOwn(assertion, "equals")
723
- ? { equals: assertion.equals }
724
- : {}),
725
- ...(Object.hasOwn(assertion, "notEquals")
726
- ? { notEquals: assertion.notEquals }
727
- : {}),
728
- ...(assertion.minimumItems !== undefined
729
- ? {
730
- minimumItems: assertPositiveInteger(
731
- assertion.minimumItems,
732
- `${path}.json.minimumItems`,
733
- ),
734
- }
735
- : {}),
736
- });
737
- }
738
- return Object.freeze({
739
- name: assertNonEmptyString(value.name, `${path}.name`),
740
- kind: value.kind,
741
- url: assertLoopbackUrl(value.url, `${path}.url`, allowedHosts),
742
- method,
743
- expectedStatus: value.expectedStatus,
744
- json,
745
- });
746
- }),
747
- ),
748
- runtimeAdapter:
749
- application.runtimeAdapter === undefined
750
- ? null
751
- : assertRelativePath(
752
- application.runtimeAdapter,
753
- "config.application.runtimeAdapter",
754
- ),
755
- }),
756
- runtime: Object.freeze({
757
- target: runtimeTarget,
758
- applicationUrl: assertLoopbackUrl(
759
- runtime.applicationUrl ?? REHEARSAL_DEFAULTS.runtime.applicationUrl,
760
- "config.runtime.applicationUrl",
761
- allowedHosts,
762
- ),
763
- projectId: assertNonEmptyString(
764
- runtime.projectId ?? REHEARSAL_DEFAULTS.runtime.projectId,
765
- "config.runtime.projectId",
766
- ),
767
- ports: Object.freeze(ports),
768
- }),
769
- safety: Object.freeze({
770
- allowedHosts: Object.freeze(allowedHosts),
771
- blockedEnvironmentVariables: Object.freeze(
772
- assertStringArray(
773
- safety.blockedEnvironmentVariables ??
774
- REHEARSAL_DEFAULTS.safety.blockedEnvironmentVariables,
775
- "config.safety.blockedEnvironmentVariables",
776
- ),
777
- ),
778
- authenticationProviders: Object.freeze(authenticationProviders),
779
- hostedAccess: "disabled",
780
- outboundNetwork: "deny",
781
- }),
782
- verification: Object.freeze({
783
- commands: Object.freeze(
784
- commands.map((command, index) =>
785
- assertCommand(command, `config.verification.commands[${index}]`),
786
- ),
787
- ),
788
- }),
789
- });
790
- };
791
-
792
- export const defineRehearsalConfig = (config) => config;
793
-
794
- export const findRehearsalConfigPath = async ({ projectRoot, configPath }) => {
795
- const root = resolve(projectRoot);
796
- if (configPath) {
797
- const explicit = resolve(root, configPath);
798
- if (relative(root, explicit).startsWith("..")) {
799
- throw new Error(
800
- "The Rehearsal configuration must stay inside the project root.",
801
- );
802
- }
803
- await access(explicit);
804
- return explicit;
805
- }
806
- for (const filename of CONFIG_FILENAMES) {
807
- const candidate = join(root, filename);
808
- try {
809
- await access(candidate);
810
- return candidate;
811
- } catch (error) {
812
- if (error?.code !== "ENOENT") throw error;
813
- }
814
- }
815
- throw new Error(
816
- `No Rehearsal configuration was found. Checked: ${CONFIG_FILENAMES.join(", ")}.`,
817
- );
818
- };
819
-
820
- export const loadRehearsalConfig = async ({
821
- projectRoot = process.cwd(),
822
- configPath,
823
- } = {}) => {
824
- const root = resolve(projectRoot);
825
- const path = await findRehearsalConfigPath({ projectRoot: root, configPath });
826
- const module = await import(
827
- `${pathToFileURL(path).href}?loaded=${Date.now()}`
828
- );
829
- const config = normalizeConfig(module.default);
830
- const resolveOwnedPath = (value) => {
831
- const resolved = resolve(root, value);
832
- if (relative(root, resolved).startsWith("..")) {
833
- throw new Error(`A Rehearsal path escaped the project root: ${value}.`);
834
- }
835
- return resolved;
836
- };
837
- const paths = Object.freeze({
838
- artifactDirectory: resolveOwnedPath(config.baseline.artifactDirectory),
839
- applicationEnvironment: resolveOwnedPath(
840
- config.application.environmentFile,
841
- ),
842
- migrationDirectory: resolveOwnedPath(
843
- (config.supabase ?? config.postgresql).migrationDirectory,
844
- ),
845
- rehearsalConfig: config.supabase
846
- ? resolveOwnedPath(config.supabase.rehearsalConfig)
847
- : null,
848
- runtimeWorkdir: resolveOwnedPath(
849
- (config.supabase ?? config.postgresql).runtimeWorkdir,
850
- ),
851
- serviceEnvironment: config.supabase?.serviceEnvironmentFile
852
- ? resolveOwnedPath(config.supabase.serviceEnvironmentFile)
853
- : null,
854
- sanitizationPolicy: resolveOwnedPath(config.baseline.sanitizationPolicy),
855
- sourcePolicy: config.preparation
856
- ? resolveOwnedPath(config.preparation.sourcePolicy)
857
- : null,
858
- privacyKey: config.preparation
859
- ? resolveOwnedPath(config.preparation.privacyKey)
860
- : null,
861
- runtimePolicy: config.runtimePolicy
862
- ? resolveOwnedPath(config.runtimePolicy)
863
- : null,
864
- identityPolicy: config.identityPolicy
865
- ? resolveOwnedPath(config.identityPolicy)
866
- : null,
867
- supabaseWorkdir: config.supabase
868
- ? resolveOwnedPath(config.supabase.workdir)
869
- : null,
870
- runtimeAdapter: config.application.runtimeAdapter
871
- ? resolveOwnedPath(config.application.runtimeAdapter)
872
- : null,
873
- });
874
- if (basename(paths.artifactDirectory) !== ".rehearsal") {
875
- throw new Error(
876
- 'config.baseline.artifactDirectory must resolve to a directory named ".rehearsal" so destructive reset and cleanup operations have an explicit safety boundary.',
877
- );
878
- }
879
- const requiredRuntimeWorkdir = join(paths.artifactDirectory, "runtime");
880
- if (paths.runtimeWorkdir !== requiredRuntimeWorkdir) {
881
- throw new Error(
882
- `config.${config.runtime.target}.runtimeWorkdir must resolve to the runtime directory inside config.baseline.artifactDirectory.`,
883
- );
884
- }
885
- const environmentRelativePath = relative(
886
- paths.artifactDirectory,
887
- paths.applicationEnvironment,
888
- );
889
- if (
890
- !environmentRelativePath ||
891
- environmentRelativePath.startsWith("..") ||
892
- isAbsolute(environmentRelativePath)
893
- ) {
894
- throw new Error(
895
- "config.application.environmentFile must resolve to a file inside config.baseline.artifactDirectory.",
896
- );
897
- }
898
- if (paths.privacyKey) {
899
- const privacyKeyRelativePath = relative(
900
- paths.artifactDirectory,
901
- paths.privacyKey,
902
- );
903
- if (
904
- !privacyKeyRelativePath ||
905
- privacyKeyRelativePath.startsWith("..") ||
906
- isAbsolute(privacyKeyRelativePath)
907
- ) {
908
- throw new Error(
909
- "config.preparation.privacyKey must resolve to a secret file inside config.baseline.artifactDirectory.",
910
- );
911
- }
912
- }
913
- if (!IDENTIFIER_PATTERN.test(config.runtime.projectId)) {
914
- throw new Error(
915
- "config.runtime.projectId must contain only lowercase letters, digits, and hyphens.",
916
- );
917
- }
918
- return Object.freeze({
919
- config,
920
- configPath: path,
921
- projectRoot: root,
922
- paths,
923
- });
924
- };
925
-
926
- export const inspectDetectedProject = async ({
927
- projectRoot = process.cwd(),
928
- } = {}) => {
929
- const root = resolve(projectRoot);
930
- const packageJsonPath = join(root, "package.json");
931
- const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
932
- const hasPath = async (path) =>
933
- stat(join(root, path))
934
- .then(() => true)
935
- .catch((error) => {
936
- if (error?.code === "ENOENT") return false;
937
- throw error;
938
- });
939
- const packageManager = (await hasPath("pnpm-lock.yaml"))
940
- ? "pnpm"
941
- : (await hasPath("yarn.lock"))
942
- ? "yarn"
943
- : "npm";
944
- const scripts = packageJson.scripts ?? {};
945
- const postgresqlMigrationDirectory = (
946
- await Promise.all(
947
- ["database/migrations", "db/migrations", "migrations"].map(
948
- async (path) => ({ path, exists: await hasPath(path) }),
949
- ),
950
- )
951
- ).find(({ exists }) => exists)?.path;
952
- const normalizedProjectName = String(packageJson.name ?? basename(root))
953
- .toLowerCase()
954
- .replace(/[^a-z0-9-]+/gu, "-")
955
- .replace(/^-|-$/gu, "");
956
- const projectName =
957
- normalizedProjectName.slice(0, 52).replace(/-$/u, "") ||
958
- "rehearsal-project";
959
- return {
960
- projectName,
961
- packageManager,
962
- hasSupabaseConfig: await hasPath("supabase/config.toml"),
963
- hasMigrations: await hasPath("supabase/migrations"),
964
- hasPostgresqlMigrations: Boolean(postgresqlMigrationDirectory),
965
- postgresqlMigrationDirectory:
966
- postgresqlMigrationDirectory ?? "database/migrations",
967
- applicationCommand:
968
- ["dev:rehearsal", "dev", "start"]
969
- .find((name) => typeof scripts[name] === "string")
970
- ?.replace(/^/u, `${packageManager} run `) ??
971
- `${packageManager} run dev`,
972
- verificationCommand:
973
- ["verify:feature", "test", "check"]
974
- .find((name) => typeof scripts[name] === "string")
975
- ?.replace(/^/u, `${packageManager} run `) ?? `${packageManager} test`,
976
- };
977
- };
978
-
979
- export const renderDetectedConfig = (
980
- detected,
981
- {
982
- applicationUrl = "http://localhost:5175",
983
- ports = { api: 58321, database: 58322, studio: 58323 },
984
- } = {},
985
- ) =>
986
- `// @ts-check
987
- /**
988
- * Rehearsal configuration for ${detected.projectName}.
989
- * Generated from this project by \`npx rehearsal\` and safe to commit.
990
- * Review the CHECK comments. Keep passwords, tokens, and production URLs out of this file.
991
- * Docs: https://github.com/Ddupasquier/rehearsal-db/blob/main/docs/configuration.md
992
- */
993
- import { defineRehearsalConfig } from "@rehearsal-db/core";
994
-
995
- export default defineRehearsalConfig({
996
- // Configuration format. Rehearsal will explain if an upgrade is ever needed.
997
- schemaVersion: 1,
998
- // Stable local name used in Rehearsal labels and reports.
999
- project: { name: ${JSON.stringify(detected.projectName)} },
1000
-
1001
- // Project files Rehearsal reads. Every path stays inside this repository.
1002
- supabase: {
1003
- workdir: ".",
1004
- migrationDirectory: "supabase/migrations",
1005
- rehearsalConfig: "infrastructure/rehearsal/supabase/config.toml",
1006
- runtimeWorkdir: ".rehearsal/runtime",
1007
-
1008
- // Optional local-only identity-provider credentials. Enable both keys together.
1009
- // serviceEnvironmentFile: ".env.rehearsal-service.local",
1010
- // serviceEnvironmentVariables: ["LOCAL_IDP_CLIENT_ID", "LOCAL_IDP_SECRET"],
1011
- },
1012
-
1013
- // Immutable local baseline files. Rehearsal never reads production on your behalf.
1014
- baseline: {
1015
- artifactDirectory: ".rehearsal",
1016
- sanitizationPolicy: "infrastructure/rehearsal/sanitization-policy.json",
1017
- },
1018
-
1019
- // Optional source refresh. Enable only after reviewing docs/standalone-workflow.md.
1020
- // Credentials are read from the environment named by source-access-policy.json.
1021
- // preparation: {
1022
- // sourcePolicy: "infrastructure/rehearsal/source-access-policy.json",
1023
- // privacyKey: ".rehearsal/secrets/privacy.key",
1024
- // batchRows: 500,
1025
- // maximumRows: 1000000,
1026
- // maximumBytes: 2147483648,
1027
- // diskHeadroomBytes: 67108864,
1028
- // },
1029
- // Optional reviewed schemas/extensions/triggers/local-only rows.
1030
- // runtimePolicy: "infrastructure/rehearsal/runtime-policy.json",
1031
- // Optional signup defaults, role/reference transfer, Storage paths, and token checks.
1032
- // identityPolicy: "infrastructure/rehearsal/identity-policy.json",
1033
-
1034
- // Reuse any running Docker-compatible engine; start Colima only when needed.
1035
- containerRuntime: {
1036
- autoStartColima: true,
1037
- },
1038
-
1039
- // Keep this many baseline generations, always including the active one.
1040
- cleanup: {
1041
- retainBaselineGenerations: 2,
1042
- },
1043
-
1044
- // Optional: manage another isolated database as part of the same rehearsal.
1045
- // Give that database its own complete config, ports, project ID, and .rehearsal folder.
1046
- // dependentTargets: [
1047
- // {
1048
- // name: "publication-api",
1049
- // configPath: "rehearsal.publication.config.mjs",
1050
- // prepareCommand: "npm run rehearsal:prepare-publication",
1051
- // },
1052
- // ],
1053
-
1054
- application: {
1055
- // CHECK: commands detected from package.json. Change them if they are not correct.
1056
- startCommand: ${JSON.stringify(detected.applicationCommand)},
1057
- proofCommand: ${JSON.stringify(detected.verificationCommand)},
1058
- environmentFile: ".rehearsal/runtime.env",
1059
- // Map generated local runtime values directly into the normal app command.
1060
- // environmentVariables: { DATABASE_URL: "primary:DATABASE_URL" },
1061
- readiness: { url: ${JSON.stringify(applicationUrl)}, expectedStatus: 200, timeoutSeconds: 30 },
1062
- // Add at least one real positive and one negative before enabling HTTP proofs.
1063
- // httpProofs: [],
1064
- // Optional project-specific restore hook:
1065
- // runtimeAdapter: "infrastructure/rehearsal/runtime-adapter.mjs",
1066
- },
1067
-
1068
- // Disposable local runtime identity, URL, and dedicated ports.
1069
- runtime: {
1070
- target: "supabase",
1071
- applicationUrl: ${JSON.stringify(applicationUrl)},
1072
- projectId: "${detected.projectName}-rehearsal",
1073
- apiPort: ${ports.api},
1074
- databasePort: ${ports.database},
1075
- studioPort: ${ports.studio},
1076
- },
1077
-
1078
- // Only local runtime URLs are accepted; common hosted credentials are quarantined.
1079
- safety: {
1080
- allowedHosts: ["127.0.0.1", "::1", "localhost"],
1081
- blockedEnvironmentVariables: [
1082
- "SUPABASE_ACCESS_TOKEN",
1083
- "SUPABASE_DB_PASSWORD",
1084
- "SUPABASE_PROJECT_ID",
1085
- ],
1086
- hostedAccess: "disabled",
1087
- outboundNetwork: "deny",
1088
- },
1089
- });
1090
- `.replaceAll("\t", " ");
1091
-
1092
- export const renderDetectedPostgresqlConfig = (
1093
- detected,
1094
- { applicationUrl = "http://localhost:5175", databasePort = 58322 } = {},
1095
- ) =>
1096
- `// @ts-check
1097
- /**
1098
- * Rehearsal configuration for ${detected.projectName}.
1099
- * Generated from this project by \`npx rehearsal\` and safe to commit.
1100
- * Review the CHECK comments. Keep passwords, tokens, and production URLs out of this file.
1101
- * Docs: https://github.com/Ddupasquier/rehearsal-db/blob/main/docs/configuration.md
1102
- */
1103
- import { defineRehearsalConfig } from "@rehearsal-db/core";
1104
-
1105
- export default defineRehearsalConfig({
1106
- // Configuration format. Rehearsal will explain if an upgrade is ever needed.
1107
- schemaVersion: 1,
1108
- // Stable local name used in Rehearsal labels and reports.
1109
- project: { name: ${JSON.stringify(detected.projectName)} },
1110
-
1111
- // Disposable PostgreSQL settings. The image must already exist locally.
1112
- postgresql: {
1113
- migrationDirectory: ${JSON.stringify(detected.postgresqlMigrationDirectory)},
1114
- runtimeWorkdir: ".rehearsal/runtime",
1115
- image: "postgres:17-alpine",
1116
- database: "postgres",
1117
- user: "postgres",
1118
- },
1119
-
1120
- // Immutable local baseline files. Rehearsal never reads production on your behalf.
1121
- baseline: {
1122
- artifactDirectory: ".rehearsal",
1123
- sanitizationPolicy: "infrastructure/rehearsal/sanitization-policy.json",
1124
- },
1125
-
1126
- // Optional source refresh. Enable only after reviewing docs/standalone-workflow.md.
1127
- // Credentials are read from the environment named by source-access-policy.json.
1128
- // preparation: {
1129
- // sourcePolicy: "infrastructure/rehearsal/source-access-policy.json",
1130
- // privacyKey: ".rehearsal/secrets/privacy.key",
1131
- // batchRows: 500,
1132
- // maximumRows: 1000000,
1133
- // maximumBytes: 2147483648,
1134
- // diskHeadroomBytes: 67108864,
1135
- // },
1136
- // Optional reviewed schemas/extensions/triggers/local-only rows.
1137
- // runtimePolicy: "infrastructure/rehearsal/runtime-policy.json",
1138
- // Optional signup defaults, role/reference transfer, Storage paths, and token checks.
1139
- // identityPolicy: "infrastructure/rehearsal/identity-policy.json",
1140
-
1141
- // Reuse any running Docker-compatible engine; start Colima only when needed.
1142
- containerRuntime: {
1143
- autoStartColima: true,
1144
- },
1145
-
1146
- // Keep this many baseline generations, always including the active one.
1147
- cleanup: {
1148
- retainBaselineGenerations: 2,
1149
- },
1150
-
1151
- // Optional: manage another isolated database as part of the same rehearsal.
1152
- // Give that database its own complete config, ports, project ID, and .rehearsal folder.
1153
- // dependentTargets: [
1154
- // {
1155
- // name: "publication-api",
1156
- // configPath: "rehearsal.publication.config.mjs",
1157
- // prepareCommand: "npm run rehearsal:prepare-publication",
1158
- // },
1159
- // ],
1160
-
1161
- application: {
1162
- // CHECK: commands detected from package.json. Change them if they are not correct.
1163
- startCommand: ${JSON.stringify(detected.applicationCommand)},
1164
- proofCommand: ${JSON.stringify(detected.verificationCommand)},
1165
- environmentFile: ".rehearsal/runtime.env",
1166
- // Map generated local runtime values directly into the normal app command.
1167
- // environmentVariables: { DATABASE_URL: "primary:DATABASE_URL" },
1168
- readiness: { url: ${JSON.stringify(applicationUrl)}, expectedStatus: 200, timeoutSeconds: 30 },
1169
- // Add at least one real positive and one negative before enabling HTTP proofs.
1170
- // httpProofs: [],
1171
- // Optional project-specific restore hook:
1172
- // runtimeAdapter: "infrastructure/rehearsal/runtime-adapter.mjs",
1173
- },
1174
-
1175
- // Disposable local runtime identity, URL, and dedicated database port.
1176
- runtime: {
1177
- target: "postgresql",
1178
- applicationUrl: ${JSON.stringify(applicationUrl)},
1179
- projectId: "${detected.projectName}-rehearsal",
1180
- databasePort: ${databasePort},
1181
- },
1182
-
1183
- // Only local runtime URLs are accepted; common hosted credentials are quarantined.
1184
- safety: {
1185
- allowedHosts: ["127.0.0.1", "::1", "localhost"],
1186
- blockedEnvironmentVariables: [
1187
- "DATABASE_URL",
1188
- "PGHOST",
1189
- "PGPASSWORD",
1190
- "PGPORT",
1191
- "PGUSER",
1192
- ],
1193
- hostedAccess: "disabled",
1194
- outboundNetwork: "deny",
1195
- },
1196
- });
1197
- `.replaceAll("\t", " ");