@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
@@ -0,0 +1,111 @@
1
+ /** Read-only discovery and redacted review for the guided account connection. */
2
+ import pg from "pg";
3
+ import { findVerifiedLocalIdentity, isIdentityNotFoundError, } from "./claim_identity_lookup.mjs";
4
+ import { getIdentityPlanMatchValue } from "./claim_plan.mjs";
5
+ const { Client } = pg;
6
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
7
+ const assertLocalDatabase = (connectionString) => {
8
+ let target;
9
+ try {
10
+ target = new URL(connectionString);
11
+ }
12
+ catch {
13
+ throw new Error("Identity connection requires a valid local database URL.");
14
+ }
15
+ if (!["postgres:", "postgresql:"].includes(target.protocol) ||
16
+ !["127.0.0.1", "::1", "localhost"].includes(target.hostname)) {
17
+ throw new Error("Identity connection may inspect only the local Rehearsal database.");
18
+ }
19
+ };
20
+ const hasAssociationReceipt = async ({ client, name, placeholderUserId, localUserId, }) => {
21
+ const relation = await client.query("select to_regclass('rehearsal_internal.identity_claims')::text as relation");
22
+ if (relation.rows[0]?.relation !== "rehearsal_internal.identity_claims") {
23
+ return false;
24
+ }
25
+ const receipt = await client.query(`select 1
26
+ from rehearsal_internal.identity_claims
27
+ where identity_name = $1
28
+ and placeholder_user_id = $2::uuid
29
+ and local_user_id = $3::uuid
30
+ limit 1`, [name, placeholderUserId, localUserId]);
31
+ return receipt.rows.length === 1;
32
+ };
33
+ export const inspectIdentityConnection = async ({ plan, connectionString, clientFactory, }) => {
34
+ assertLocalDatabase(connectionString);
35
+ const matchValue = getIdentityPlanMatchValue(plan);
36
+ if (!matchValue) {
37
+ throw new Error("Identity connection plans must use the reviewed local matcher environment.");
38
+ }
39
+ const client = clientFactory
40
+ ? await clientFactory(connectionString)
41
+ : new Client({
42
+ connectionString,
43
+ application_name: "rehearsal-identity-connect",
44
+ });
45
+ await client.connect?.();
46
+ try {
47
+ const localIdentity = await findVerifiedLocalIdentity({
48
+ client,
49
+ plan,
50
+ matchValue,
51
+ lock: false,
52
+ });
53
+ if (!UUID.test(localIdentity.id)) {
54
+ throw new Error("Matched local identity is invalid.");
55
+ }
56
+ return Object.freeze({
57
+ name: plan.review.name,
58
+ provider: localIdentity.provider,
59
+ planDigest: plan.digest,
60
+ relationalGroups: plan.review.references.filter((reference) => reference.strategy === "transfer").length,
61
+ preservedAuditGroups: plan.review.references.filter((reference) => reference.strategy === "preserve-audit").length,
62
+ nestedJsonGroups: plan.review.jsonReferences.length,
63
+ signupDefaults: plan.review.signupDefaults.length,
64
+ storagePathGroups: plan.review.pathReferences.length,
65
+ assetScopes: plan.review.assets.length,
66
+ claimKeys: plan.review.claimKeys.length,
67
+ tokenHookConfigured: plan.review.tokenHook !== null,
68
+ associated: await hasAssociationReceipt({
69
+ client,
70
+ name: plan.identity.name,
71
+ placeholderUserId: plan.identity.placeholderUserId,
72
+ localUserId: localIdentity.id,
73
+ }),
74
+ });
75
+ }
76
+ finally {
77
+ await client.end?.();
78
+ }
79
+ };
80
+ const abortableDelay = (milliseconds, signal) => new Promise((resolve, reject) => {
81
+ if (signal?.aborted) {
82
+ reject(signal.reason ?? new Error("Identity connection was cancelled."));
83
+ return;
84
+ }
85
+ const timeout = setTimeout(resolve, milliseconds);
86
+ signal?.addEventListener("abort", () => {
87
+ clearTimeout(timeout);
88
+ reject(signal.reason ?? new Error("Identity connection was cancelled."));
89
+ }, { once: true });
90
+ });
91
+ export const waitForIdentityConnection = async ({ plan, connectionString, clientFactory, inspect = inspectIdentityConnection, signal, pollIntervalMs = 750, timeoutMs = 15 * 60 * 1_000, }) => {
92
+ const deadline = Date.now() + timeoutMs;
93
+ while (Date.now() < deadline) {
94
+ if (signal?.aborted) {
95
+ throw signal.reason ?? new Error("Identity connection was cancelled.");
96
+ }
97
+ try {
98
+ return await inspect({
99
+ plan,
100
+ connectionString,
101
+ ...(clientFactory === undefined ? {} : { clientFactory }),
102
+ });
103
+ }
104
+ catch (error) {
105
+ if (!isIdentityNotFoundError(error))
106
+ throw error;
107
+ }
108
+ await abortableDelay(pollIntervalMs, signal);
109
+ }
110
+ throw new Error("Timed out waiting for the approved verified local identity. Nothing was changed.");
111
+ };
@@ -0,0 +1,37 @@
1
+ /** Render reviewed local OAuth declarations into the disposable Supabase runtime. */
2
+ export interface SupabaseAuthenticationProvider {
3
+ name: "google" | "github";
4
+ clientIdEnvironmentVariable: string;
5
+ clientSecretEnvironmentVariable: string;
6
+ skipNonceCheck?: boolean;
7
+ emailOptional?: boolean;
8
+ }
9
+ export interface SupabaseAuthentication {
10
+ providers: readonly SupabaseAuthenticationProvider[];
11
+ }
12
+ interface TomlSection {
13
+ name: string;
14
+ start: number;
15
+ end: number;
16
+ }
17
+ interface ProviderReplacementSection extends TomlSection {
18
+ provider: string;
19
+ }
20
+ interface ProviderTransition {
21
+ readonly replacedProviders: readonly string[];
22
+ readonly replacementSections: readonly ProviderReplacementSection[];
23
+ }
24
+ export declare const inspectSupabaseAuthenticationProviderTransition: ({ source, authentication, }: {
25
+ source: string;
26
+ authentication: SupabaseAuthentication | null;
27
+ }) => Readonly<ProviderTransition>;
28
+ export declare const renderSupabaseAuthenticationProviders: ({ authentication, apiPort, }: {
29
+ authentication: SupabaseAuthentication | null;
30
+ apiPort: number;
31
+ }) => string;
32
+ export declare const applySupabaseAuthenticationProviders: ({ source, authentication, apiPort, }: {
33
+ source: string;
34
+ authentication: SupabaseAuthentication | null;
35
+ apiPort: number;
36
+ }) => string;
37
+ export {};
@@ -0,0 +1,132 @@
1
+ /** Render reviewed local OAuth declarations into the disposable Supabase runtime. */
2
+ const escapePattern = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
3
+ const LEGACY_PROVIDER_KEYS = new Set([
4
+ "client_id",
5
+ "email_optional",
6
+ "enabled",
7
+ "redirect_uri",
8
+ "secret",
9
+ "skip_nonce_check",
10
+ ]);
11
+ const SAFE_BOOLEAN_PROVIDER_KEYS = new Set([
12
+ "email_optional",
13
+ "skip_nonce_check",
14
+ ]);
15
+ const readTomlSections = (source) => {
16
+ const lines = source.split("\n");
17
+ const sections = [];
18
+ for (let index = 0; index < lines.length; index += 1) {
19
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/u.exec(lines[index]);
20
+ if (!match)
21
+ continue;
22
+ if (sections.length)
23
+ sections.at(-1).end = index;
24
+ sections.push({ name: match[1], start: index, end: lines.length });
25
+ }
26
+ return { lines, sections };
27
+ };
28
+ const validateLegacyProviderSection = ({ lines, section, provider, }) => {
29
+ const seen = new Set();
30
+ for (const line of lines.slice(section.start + 1, section.end)) {
31
+ if (!line.trim() || /^\s*#/u.test(line))
32
+ continue;
33
+ const assignment = /^\s*([A-Za-z0-9_]+)\s*=/u.exec(line);
34
+ if (!assignment || !LEGACY_PROVIDER_KEYS.has(assignment[1])) {
35
+ throw new Error(`The local Supabase template auth.external.${provider.name} section contains an unsupported setting. Rehearsal left the tracked template unchanged.`);
36
+ }
37
+ const key = assignment[1];
38
+ if (seen.has(key)) {
39
+ throw new Error(`The local Supabase template auth.external.${provider.name} section contains duplicate ${key} settings.`);
40
+ }
41
+ seen.add(key);
42
+ if (SAFE_BOOLEAN_PROVIDER_KEYS.has(key) &&
43
+ !new RegExp(`^\\s*${escapePattern(key)}\\s*=\\s*${key === "skip_nonce_check"
44
+ ? (provider.skipNonceCheck ?? false)
45
+ : (provider.emailOptional ?? false)}\\s*(?:#.*)?$`, "u").test(line)) {
46
+ throw new Error(`The local Supabase template auth.external.${provider.name}.${key} setting must match its explicit declarative authentication option before Rehearsal can replace the legacy provider in its disposable runtime copy.`);
47
+ }
48
+ }
49
+ };
50
+ export const inspectSupabaseAuthenticationProviderTransition = ({ source, authentication, }) => {
51
+ if (!authentication)
52
+ return Object.freeze({ replacedProviders: [], replacementSections: [] });
53
+ const { lines, sections } = readTomlSections(source);
54
+ const replacementSections = [];
55
+ for (const provider of authentication.providers) {
56
+ const sectionName = `auth.external.${provider.name}`;
57
+ const nested = sections.filter((section) => section.name.startsWith(`${sectionName}.`));
58
+ if (nested.length) {
59
+ throw new Error(`The local Supabase template contains unsupported nested ${sectionName} configuration. Rehearsal left the tracked template unchanged.`);
60
+ }
61
+ const matching = sections.filter((section) => section.name === sectionName);
62
+ if (matching.length > 1) {
63
+ throw new Error(`The local Supabase template declares ${sectionName} more than once.`);
64
+ }
65
+ if (matching.length === 1) {
66
+ validateLegacyProviderSection({
67
+ lines,
68
+ section: matching[0],
69
+ provider,
70
+ });
71
+ replacementSections.push({ ...matching[0], provider: provider.name });
72
+ }
73
+ }
74
+ return Object.freeze({
75
+ replacedProviders: Object.freeze(replacementSections.map(({ provider }) => provider)),
76
+ replacementSections: Object.freeze(replacementSections),
77
+ });
78
+ };
79
+ export const renderSupabaseAuthenticationProviders = ({ authentication, apiPort, }) => {
80
+ if (!authentication)
81
+ return "";
82
+ return authentication.providers
83
+ .map((provider) => `# Generated by Rehearsal from config.supabase.authentication.
84
+ [auth.external.${provider.name}]
85
+ enabled = true
86
+ client_id = "env(${provider.clientIdEnvironmentVariable})"
87
+ secret = "env(${provider.clientSecretEnvironmentVariable})"
88
+ redirect_uri = "http://127.0.0.1:${apiPort}/auth/v1/callback"
89
+ ${provider.skipNonceCheck ? "skip_nonce_check = true\n" : ""}${provider.emailOptional ? "email_optional = true\n" : ""}
90
+ `)
91
+ .join("\n");
92
+ };
93
+ const enableLocalSignup = (source) => {
94
+ const lines = source.split("\n");
95
+ let inAuth = false;
96
+ let authSections = 0;
97
+ let signupSettings = 0;
98
+ const updated = lines.map((line) => {
99
+ const section = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/u.exec(line);
100
+ if (section) {
101
+ inAuth = section[1] === "auth";
102
+ if (inAuth)
103
+ authSections += 1;
104
+ return line;
105
+ }
106
+ if (!inAuth || !/^\s*enable_signup\s*=/u.test(line))
107
+ return line;
108
+ signupSettings += 1;
109
+ const comment = line.match(/\s+#.*$/u)?.[0] ?? "";
110
+ return `enable_signup = true${comment}`;
111
+ });
112
+ if (authSections !== 1 || signupSettings !== 1) {
113
+ throw new Error("The local Supabase template must declare exactly one [auth] section with one enable_signup setting before Rehearsal can configure OAuth.");
114
+ }
115
+ return updated.join("\n");
116
+ };
117
+ export const applySupabaseAuthenticationProviders = ({ source, authentication, apiPort, }) => {
118
+ if (!authentication)
119
+ return source;
120
+ const transition = inspectSupabaseAuthenticationProviderTransition({
121
+ source,
122
+ authentication,
123
+ });
124
+ const { lines } = readTomlSections(source);
125
+ const replacedIndexes = new Set(transition.replacementSections.flatMap((section) => Array.from({ length: section.end - section.start }, (_, offset) => section.start + offset)));
126
+ const transitionedSource = lines
127
+ .filter((_, index) => !replacedIndexes.has(index))
128
+ .join("\n");
129
+ const runtimeSource = enableLocalSignup(transitionedSource);
130
+ const separator = runtimeSource.endsWith("\n") ? "\n" : "\n\n";
131
+ return `${runtimeSource}${separator}${renderSupabaseAuthenticationProviders({ authentication, apiPort })}`;
132
+ };
@@ -0,0 +1,21 @@
1
+ /** Copy and verify local Supabase Storage objects during an identity claim. */
2
+ export interface StorageObjectReceipt {
3
+ readonly bytes: number;
4
+ readonly sha256: string;
5
+ }
6
+ export interface SupabaseStorageTransfer {
7
+ copyAndVerify(input: {
8
+ bucket: string;
9
+ source: string;
10
+ destination: string;
11
+ }): Promise<StorageObjectReceipt>;
12
+ remove(input: {
13
+ bucket: string;
14
+ paths: readonly string[];
15
+ }): Promise<void>;
16
+ }
17
+ export declare const createSupabaseStorageTransfer: ({ url, serviceRoleKey, fetchImpl, }: {
18
+ url: string;
19
+ serviceRoleKey: string;
20
+ fetchImpl?: typeof fetch;
21
+ }) => SupabaseStorageTransfer;
@@ -0,0 +1,122 @@
1
+ /** Copy and verify local Supabase Storage objects during an identity claim. */
2
+ import { createHash } from "node:crypto";
3
+ import { assertLoopbackUrl } from "../shared/process_environment.mjs";
4
+ const BUCKET = /^[a-z0-9][a-z0-9.-]{0,99}$/u;
5
+ const MAXIMUM_VERIFICATION_BYTES = 100 * 1024 * 1024;
6
+ const REQUEST_TIMEOUT_MS = 30_000;
7
+ const objectPath = (value) => {
8
+ if (typeof value !== "string" ||
9
+ !value ||
10
+ value.startsWith("/") ||
11
+ value.split("/").some((part) => !part || part === "." || part === "..")) {
12
+ throw new Error("A Storage identity-claim path is unsafe.");
13
+ }
14
+ return value.split("/").map(encodeURIComponent).join("/");
15
+ };
16
+ const readVerifiedObject = async ({ fetchImpl, baseUrl, headers, bucket, path, }) => {
17
+ const response = await fetchImpl(`${baseUrl}/storage/v1/object/${encodeURIComponent(bucket)}/${objectPath(path)}?rehearsal=${Date.now()}`, {
18
+ headers,
19
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
20
+ });
21
+ if (!response.ok) {
22
+ throw new Error(`Supabase Storage could not read an identity-claim object (HTTP ${response.status}).`);
23
+ }
24
+ const declaredBytes = Number(response.headers.get("content-length"));
25
+ if (Number.isFinite(declaredBytes) &&
26
+ declaredBytes > MAXIMUM_VERIFICATION_BYTES) {
27
+ throw new Error("A Storage identity-claim object exceeds the 100 MiB verification limit.");
28
+ }
29
+ const bytes = Buffer.from(await response.arrayBuffer());
30
+ if (bytes.length > MAXIMUM_VERIFICATION_BYTES) {
31
+ throw new Error("A Storage identity-claim object exceeds the 100 MiB verification limit.");
32
+ }
33
+ return Object.freeze({
34
+ bytes: bytes.length,
35
+ sha256: createHash("sha256").update(bytes).digest("hex"),
36
+ });
37
+ };
38
+ export const createSupabaseStorageTransfer = ({ url, serviceRoleKey, fetchImpl = globalThis.fetch, }) => {
39
+ assertLoopbackUrl("Rehearsal Supabase Storage", url);
40
+ if (typeof serviceRoleKey !== "string" || !serviceRoleKey) {
41
+ throw new Error("The local runtime environment has no Storage service key.");
42
+ }
43
+ if (typeof fetchImpl !== "function") {
44
+ throw new Error("A Fetch implementation is required for Storage transfer.");
45
+ }
46
+ const baseUrl = url.replace(/\/$/u, "");
47
+ const headers = Object.freeze({
48
+ apikey: serviceRoleKey,
49
+ authorization: `Bearer ${serviceRoleKey}`,
50
+ });
51
+ const removePaths = async ({ bucket, paths, }) => {
52
+ if (!BUCKET.test(bucket ?? "") || !Array.isArray(paths) || !paths.length) {
53
+ throw new Error("A Storage identity-claim removal is invalid.");
54
+ }
55
+ paths.forEach(objectPath);
56
+ const response = await fetchImpl(`${baseUrl}/storage/v1/object/${encodeURIComponent(bucket)}`, {
57
+ method: "DELETE",
58
+ headers: { ...headers, "content-type": "application/json" },
59
+ body: JSON.stringify({ prefixes: paths }),
60
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
61
+ });
62
+ if (!response.ok) {
63
+ throw new Error(`Supabase Storage could not remove identity-claim objects (HTTP ${response.status}).`);
64
+ }
65
+ };
66
+ return Object.freeze({
67
+ async copyAndVerify({ bucket, source, destination, }) {
68
+ if (!BUCKET.test(bucket ?? "")) {
69
+ throw new Error("A Storage identity-claim bucket is invalid.");
70
+ }
71
+ objectPath(source);
72
+ objectPath(destination);
73
+ if (source === destination) {
74
+ throw new Error("A Storage identity-claim copy must change the path.");
75
+ }
76
+ const sourceReceipt = await readVerifiedObject({
77
+ fetchImpl,
78
+ baseUrl,
79
+ headers,
80
+ bucket,
81
+ path: source,
82
+ });
83
+ try {
84
+ const response = await fetchImpl(`${baseUrl}/storage/v1/object/copy`, {
85
+ method: "POST",
86
+ headers: { ...headers, "content-type": "application/json" },
87
+ body: JSON.stringify({
88
+ bucketId: bucket,
89
+ sourceKey: source,
90
+ destinationKey: destination,
91
+ }),
92
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
93
+ });
94
+ if (!response.ok) {
95
+ throw new Error(`Supabase Storage could not copy an identity-claim object (HTTP ${response.status}).`);
96
+ }
97
+ const destinationReceipt = await readVerifiedObject({
98
+ fetchImpl,
99
+ baseUrl,
100
+ headers,
101
+ bucket,
102
+ path: destination,
103
+ });
104
+ if (sourceReceipt.bytes !== destinationReceipt.bytes ||
105
+ sourceReceipt.sha256 !== destinationReceipt.sha256) {
106
+ throw new Error("Supabase Storage copied an identity object with a different byte digest.");
107
+ }
108
+ return destinationReceipt;
109
+ }
110
+ catch (error) {
111
+ try {
112
+ await removePaths({ bucket, paths: [destination] });
113
+ }
114
+ catch (cleanupError) {
115
+ throw new AggregateError([error, cleanupError], "Storage identity-copy verification failed and destination cleanup needs attention.");
116
+ }
117
+ throw error;
118
+ }
119
+ },
120
+ remove: removePaths,
121
+ });
122
+ };
@@ -0,0 +1,4 @@
1
+ /** Public Rehearsal package entry point. */
2
+ export { REHEARSAL_CONFIG_VERSION, defineRehearsalConfig, findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./project/configuration.mjs";
3
+ export type { DetectedProject, NormalizedRehearsalConfig, RehearsalConfig, RehearsalConfigPathOptions, RehearsalConfigVersion, } from "./project/configuration.mjs";
4
+ export { EXCLUDED_VALUE, SANITIZATION_ACTIONS, applySanitizationAction, normalizeSanitizationAction, readBoundRuntimeSanitizationPolicy, validateSanitizationCoverage, validateRuntimeSanitizationPolicy, } from "./baseline/sanitization_policy.mjs";
@@ -0,0 +1,3 @@
1
+ /** Public Rehearsal package entry point. */
2
+ export { REHEARSAL_CONFIG_VERSION, defineRehearsalConfig, findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./project/configuration.mjs";
3
+ export { EXCLUDED_VALUE, SANITIZATION_ACTIONS, applySanitizationAction, normalizeSanitizationAction, readBoundRuntimeSanitizationPolicy, validateSanitizationCoverage, validateRuntimeSanitizationPolicy, } from "./baseline/sanitization_policy.mjs";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Stable public facade for Rehearsal configuration. Implementation concerns
3
+ * live in focused modules so callers keep one compatible import path.
4
+ */
5
+ export { defineRehearsalConfig, REHEARSAL_CONFIG_VERSION, REHEARSAL_DEFAULTS, } from "./configuration_contract.mjs";
6
+ export type { RehearsalConfig, RehearsalConfigVersion, } from "./configuration_contract.mjs";
7
+ export { findRehearsalConfigPath } from "./configuration_discovery.mjs";
8
+ export type { RehearsalConfigPathOptions } from "./configuration_discovery.mjs";
9
+ export { loadRehearsalConfig } from "./configuration_resolution.mjs";
10
+ export type { NormalizedRehearsalConfig } from "./configuration_resolution.mjs";
11
+ export { inspectDetectedProject } from "./project_detection.mjs";
12
+ export type { DetectedProject } from "./project_detection.mjs";
13
+ export { renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration_rendering.mjs";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Stable public facade for Rehearsal configuration. Implementation concerns
3
+ * live in focused modules so callers keep one compatible import path.
4
+ */
5
+ export { defineRehearsalConfig, REHEARSAL_CONFIG_VERSION, REHEARSAL_DEFAULTS, } from "./configuration_contract.mjs";
6
+ export { findRehearsalConfigPath } from "./configuration_discovery.mjs";
7
+ export { loadRehearsalConfig } from "./configuration_resolution.mjs";
8
+ export { inspectDetectedProject } from "./project_detection.mjs";
9
+ export { renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration_rendering.mjs";
@@ -0,0 +1,125 @@
1
+ /** Public configuration types, format version, and stable defaults. */
2
+ export type RehearsalConfigVersion = 1;
3
+ export interface RehearsalConfig {
4
+ schemaVersion: RehearsalConfigVersion;
5
+ project: {
6
+ name: string;
7
+ };
8
+ supabase?: {
9
+ workdir: string;
10
+ migrationDirectory: string;
11
+ rehearsalConfig: string;
12
+ runtimeWorkdir: string;
13
+ authentication?: {
14
+ enableLocalSignup: true;
15
+ environmentFile: string;
16
+ providers: Array<{
17
+ name: "google" | "github";
18
+ clientIdEnvironmentVariable: string;
19
+ clientSecretEnvironmentVariable: string;
20
+ skipNonceCheck?: boolean;
21
+ emailOptional?: boolean;
22
+ }>;
23
+ };
24
+ serviceEnvironmentFile?: string;
25
+ serviceEnvironmentVariables?: string[];
26
+ };
27
+ postgresql?: {
28
+ migrationDirectory: string;
29
+ runtimeWorkdir?: string;
30
+ image?: string;
31
+ database?: string;
32
+ user?: string;
33
+ };
34
+ baseline: {
35
+ artifactDirectory?: string;
36
+ sanitizationPolicy: string;
37
+ };
38
+ containerRuntime?: {
39
+ autoStartColima?: boolean;
40
+ };
41
+ cleanup?: {
42
+ retainBaselineGenerations?: number;
43
+ };
44
+ preparation?: {
45
+ sourcePolicy: string;
46
+ privacyKey: string;
47
+ batchRows?: number;
48
+ maximumRows?: number;
49
+ maximumBytes?: number;
50
+ diskHeadroomBytes?: number;
51
+ };
52
+ runtimePolicy?: string;
53
+ identityPolicy?: string;
54
+ dependentTargets?: Array<{
55
+ name: string;
56
+ configPath: string;
57
+ prepareCommand?: string;
58
+ }>;
59
+ application: {
60
+ startCommand: string;
61
+ proofCommand: string;
62
+ environmentFile?: string;
63
+ environmentVariables?: Record<string, string>;
64
+ readiness?: {
65
+ url: string;
66
+ expectedStatus: number;
67
+ timeoutSeconds?: number;
68
+ };
69
+ httpProofs?: Array<{
70
+ name: string;
71
+ kind: "positive" | "negative";
72
+ url: string;
73
+ method?: string;
74
+ expectedStatus: number;
75
+ json?: {
76
+ path: string[];
77
+ equals?: unknown;
78
+ notEquals?: unknown;
79
+ minimumItems?: number;
80
+ };
81
+ }>;
82
+ runtimeAdapter?: string;
83
+ };
84
+ runtime: {
85
+ target?: "supabase" | "postgresql";
86
+ applicationUrl?: string;
87
+ projectId?: string;
88
+ apiPort?: number;
89
+ databasePort: number;
90
+ studioPort?: number;
91
+ };
92
+ safety?: {
93
+ allowedHosts?: string[];
94
+ blockedEnvironmentVariables?: string[];
95
+ authenticationProviders?: string[];
96
+ hostedAccess?: "disabled";
97
+ outboundNetwork?: "deny";
98
+ };
99
+ verification?: {
100
+ commands?: string[];
101
+ };
102
+ }
103
+ export declare const REHEARSAL_CONFIG_VERSION = 1;
104
+ export declare const REHEARSAL_DEFAULTS: Readonly<{
105
+ baseline: Readonly<{
106
+ artifactDirectory: ".rehearsal";
107
+ }>;
108
+ containerRuntime: Readonly<{
109
+ autoStartColima: false;
110
+ }>;
111
+ cleanup: Readonly<{
112
+ retainBaselineGenerations: 2;
113
+ }>;
114
+ runtime: Readonly<{
115
+ applicationUrl: "http://localhost:5175";
116
+ projectId: "rehearsal-local";
117
+ }>;
118
+ safety: Readonly<{
119
+ allowedHosts: readonly string[];
120
+ blockedEnvironmentVariables: readonly string[];
121
+ hostedAccess: "disabled";
122
+ outboundNetwork: "deny";
123
+ }>;
124
+ }>;
125
+ export declare const defineRehearsalConfig: <const Config extends RehearsalConfig>(config: Config) => Config;
@@ -0,0 +1,27 @@
1
+ /** Public configuration types, format version, and stable defaults. */
2
+ export const REHEARSAL_CONFIG_VERSION = 1;
3
+ export const REHEARSAL_DEFAULTS = Object.freeze({
4
+ baseline: Object.freeze({ artifactDirectory: ".rehearsal" }),
5
+ containerRuntime: Object.freeze({ autoStartColima: false }),
6
+ cleanup: Object.freeze({ retainBaselineGenerations: 2 }),
7
+ runtime: Object.freeze({
8
+ applicationUrl: "http://localhost:5175",
9
+ projectId: "rehearsal-local",
10
+ }),
11
+ safety: Object.freeze({
12
+ allowedHosts: Object.freeze(["127.0.0.1", "::1", "localhost"]),
13
+ blockedEnvironmentVariables: Object.freeze([
14
+ "DATABASE_URL",
15
+ "PGHOST",
16
+ "PGPASSWORD",
17
+ "PGPORT",
18
+ "PGUSER",
19
+ "SUPABASE_ACCESS_TOKEN",
20
+ "SUPABASE_DB_PASSWORD",
21
+ "SUPABASE_PROJECT_ID",
22
+ ]),
23
+ hostedAccess: "disabled",
24
+ outboundNetwork: "deny",
25
+ }),
26
+ });
27
+ export const defineRehearsalConfig = (config) => config;
@@ -0,0 +1,7 @@
1
+ /** Locate the one supported Rehearsal configuration owned by a project. */
2
+ export declare const CONFIG_FILENAMES: readonly string[];
3
+ export interface RehearsalConfigPathOptions {
4
+ projectRoot?: string;
5
+ configPath?: string;
6
+ }
7
+ export declare const findRehearsalConfigPath: ({ projectRoot, configPath, }?: RehearsalConfigPathOptions) => Promise<string>;
@@ -0,0 +1,37 @@
1
+ /** Locate the one supported Rehearsal configuration owned by a project. */
2
+ import { access } from "node:fs/promises";
3
+ import { join, relative, resolve } from "node:path";
4
+ export const CONFIG_FILENAMES = Object.freeze([
5
+ "rehearsal.config.mjs",
6
+ "rehearsal.config.js",
7
+ "rehearsal.config.ts",
8
+ join("infrastructure", "rehearsal", "rehearsal.config.mjs"),
9
+ join("infrastructure", "rehearsal", "rehearsal.config.ts"),
10
+ ]);
11
+ const hasErrorCode = (error, code) => typeof error === "object" &&
12
+ error !== null &&
13
+ "code" in error &&
14
+ error.code === code;
15
+ export const findRehearsalConfigPath = async ({ projectRoot = process.cwd(), configPath, } = {}) => {
16
+ const root = resolve(projectRoot);
17
+ if (configPath) {
18
+ const explicit = resolve(root, configPath);
19
+ if (relative(root, explicit).startsWith("..")) {
20
+ throw new Error("The Rehearsal configuration must stay inside the project root.");
21
+ }
22
+ await access(explicit);
23
+ return explicit;
24
+ }
25
+ for (const filename of CONFIG_FILENAMES) {
26
+ const candidate = join(root, filename);
27
+ try {
28
+ await access(candidate);
29
+ return candidate;
30
+ }
31
+ catch (error) {
32
+ if (!hasErrorCode(error, "ENOENT"))
33
+ throw error;
34
+ }
35
+ }
36
+ throw new Error(`No Rehearsal configuration was found. Checked: ${CONFIG_FILENAMES.join(", ")}.`);
37
+ };