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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -0
  3. package/dist/package.json +101 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  21. package/dist/src/baseline/privacy_contract.mjs +2 -0
  22. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  23. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  24. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  25. package/dist/src/baseline/privacy_engine.mjs +4 -0
  26. package/dist/src/baseline/privacy_key.d.mts +7 -0
  27. package/dist/src/baseline/privacy_key.mjs +42 -0
  28. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  29. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  30. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  31. package/dist/src/baseline/privacy_policy.mjs +237 -0
  32. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  33. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  34. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  35. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  36. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  37. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  38. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  39. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  40. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  41. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  42. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  43. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  44. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  45. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  46. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  47. package/dist/src/cli/application_command_handlers.mjs +52 -0
  48. package/dist/src/cli/arguments.d.mts +38 -0
  49. package/dist/src/cli/arguments.mjs +128 -0
  50. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  51. package/dist/src/cli/basic_command_handlers.mjs +56 -0
  52. package/dist/src/cli/cli_commands.d.mts +14 -0
  53. package/dist/src/cli/cli_commands.mjs +32 -0
  54. package/dist/src/cli/command_contract.d.mts +57 -0
  55. package/dist/src/cli/command_contract.mjs +39 -0
  56. package/dist/src/cli/command_output.d.mts +12 -0
  57. package/dist/src/cli/command_output.mjs +20 -0
  58. package/dist/src/cli/command_registry.d.mts +7 -0
  59. package/dist/src/cli/command_registry.mjs +14 -0
  60. package/dist/src/cli/guided.d.mts +53 -0
  61. package/dist/src/cli/guided.mjs +719 -0
  62. package/dist/src/cli/identity_command_handlers.d.mts +8 -0
  63. package/dist/src/cli/identity_command_handlers.mjs +43 -0
  64. package/dist/src/cli/init_command.d.mts +21 -0
  65. package/dist/src/cli/init_command.mjs +90 -0
  66. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  67. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  68. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  69. package/dist/src/cli/project_command_handlers.mjs +92 -0
  70. package/dist/src/cli/project_commands.d.mts +159 -0
  71. package/dist/src/cli/project_commands.mjs +183 -0
  72. package/dist/src/cli/rehearsal.d.mts +7 -0
  73. package/dist/src/cli/rehearsal.mjs +190 -0
  74. package/dist/src/cli/renderers.d.mts +299 -0
  75. package/dist/src/cli/renderers.mjs +554 -0
  76. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  77. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  78. package/dist/src/cli/runtime_commands.d.mts +269 -0
  79. package/dist/src/cli/runtime_commands.mjs +601 -0
  80. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  81. package/dist/src/cli/source_command_handlers.mjs +62 -0
  82. package/dist/src/cli/source_commands.d.mts +436 -0
  83. package/dist/src/cli/source_commands.mjs +301 -0
  84. package/dist/src/cli/terminal.d.mts +59 -0
  85. package/dist/src/cli/terminal.mjs +230 -0
  86. package/dist/src/identity/claim.d.mts +5 -0
  87. package/dist/src/identity/claim.mjs +4 -0
  88. package/dist/src/identity/claim_contract.d.mts +135 -0
  89. package/dist/src/identity/claim_contract.mjs +6 -0
  90. package/dist/src/identity/claim_executor.d.mts +10 -0
  91. package/dist/src/identity/claim_executor.mjs +358 -0
  92. package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
  93. package/dist/src/identity/claim_identity_lookup.mjs +125 -0
  94. package/dist/src/identity/claim_plan.d.mts +8 -0
  95. package/dist/src/identity/claim_plan.mjs +61 -0
  96. package/dist/src/identity/claim_policy.d.mts +3 -0
  97. package/dist/src/identity/claim_policy.mjs +377 -0
  98. package/dist/src/identity/claim_sql.d.mts +10 -0
  99. package/dist/src/identity/claim_sql.mjs +46 -0
  100. package/dist/src/identity/claim_verification.d.mts +13 -0
  101. package/dist/src/identity/claim_verification.mjs +35 -0
  102. package/dist/src/identity/provider_configuration.d.mts +37 -0
  103. package/dist/src/identity/provider_configuration.mjs +132 -0
  104. package/dist/src/identity/storage.d.mts +21 -0
  105. package/dist/src/identity/storage.mjs +122 -0
  106. package/dist/src/index.d.mts +4 -0
  107. package/dist/src/index.mjs +3 -0
  108. package/dist/src/project/configuration.d.mts +13 -0
  109. package/dist/src/project/configuration.mjs +9 -0
  110. package/dist/src/project/configuration_contract.d.mts +125 -0
  111. package/dist/src/project/configuration_contract.mjs +27 -0
  112. package/dist/src/project/configuration_discovery.d.mts +7 -0
  113. package/dist/src/project/configuration_discovery.mjs +37 -0
  114. package/dist/src/project/configuration_normalization.d.mts +107 -0
  115. package/dist/src/project/configuration_normalization.mjs +302 -0
  116. package/dist/src/project/configuration_rendering.d.mts +14 -0
  117. package/dist/src/project/configuration_rendering.mjs +224 -0
  118. package/dist/src/project/configuration_resolution.d.mts +125 -0
  119. package/dist/src/project/configuration_resolution.mjs +81 -0
  120. package/dist/src/project/configuration_schema.d.mts +22 -0
  121. package/dist/src/project/configuration_schema.mjs +160 -0
  122. package/dist/src/project/configuration_validation.d.mts +22 -0
  123. package/dist/src/project/configuration_validation.mjs +136 -0
  124. package/dist/src/project/install_scaffold.d.mts +42 -0
  125. package/dist/src/project/install_scaffold.mjs +263 -0
  126. package/dist/src/project/project_detection.d.mts +14 -0
  127. package/dist/src/project/project_detection.mjs +47 -0
  128. package/dist/src/project/setup.d.mts +87 -0
  129. package/dist/src/project/setup.mjs +414 -0
  130. package/dist/src/project/support_report.d.mts +88 -0
  131. package/dist/src/project/support_report.mjs +83 -0
  132. package/dist/src/runtime/cleanup.d.mts +85 -0
  133. package/dist/src/runtime/cleanup.mjs +311 -0
  134. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  135. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  136. package/dist/src/runtime/migration_history.d.mts +51 -0
  137. package/dist/src/runtime/migration_history.mjs +170 -0
  138. package/dist/src/runtime/plan.d.mts +134 -0
  139. package/dist/src/runtime/plan.mjs +637 -0
  140. package/dist/src/runtime/policy.d.mts +66 -0
  141. package/dist/src/runtime/policy.mjs +369 -0
  142. package/dist/src/runtime/restore.d.mts +22 -0
  143. package/dist/src/runtime/restore.mjs +276 -0
  144. package/dist/src/runtime/service_environment.d.mts +11 -0
  145. package/dist/src/runtime/service_environment.mjs +58 -0
  146. package/dist/src/runtime/topology.d.mts +51 -0
  147. package/dist/src/runtime/topology.mjs +128 -0
  148. package/dist/src/shared/diagnostics.d.mts +97 -0
  149. package/dist/src/shared/diagnostics.mjs +170 -0
  150. package/dist/src/shared/human_output.d.mts +2 -0
  151. package/{src → dist/src}/shared/human_output.mjs +1 -3
  152. package/dist/src/shared/node_runtime.d.mts +14 -0
  153. package/dist/src/shared/node_runtime.mjs +25 -0
  154. package/dist/src/shared/operation_guard.d.mts +22 -0
  155. package/dist/src/shared/operation_guard.mjs +150 -0
  156. package/dist/src/shared/process_environment.d.mts +15 -0
  157. package/dist/src/shared/process_environment.mjs +50 -0
  158. package/dist/src/source/access.d.mts +165 -0
  159. package/dist/src/source/access.mjs +602 -0
  160. package/dist/src/source/asset_transfer.d.mts +27 -0
  161. package/dist/src/source/asset_transfer.mjs +202 -0
  162. package/dist/src/source/baseline.d.mts +66 -0
  163. package/dist/src/source/baseline.mjs +372 -0
  164. package/dist/src/source/external_reader.d.mts +42 -0
  165. package/dist/src/source/external_reader.mjs +278 -0
  166. package/dist/src/source/postgresql_access.d.mts +86 -0
  167. package/dist/src/source/postgresql_access.mjs +454 -0
  168. package/dist/src/source/privacy_paths.d.mts +10 -0
  169. package/dist/src/source/privacy_paths.mjs +27 -0
  170. package/dist/src/targets/driver.d.mts +2 -0
  171. package/dist/src/targets/driver.mjs +13 -0
  172. package/dist/src/targets/postgresql.d.mts +5 -0
  173. package/dist/src/targets/postgresql.mjs +620 -0
  174. package/dist/src/targets/supabase.d.mts +8 -0
  175. package/dist/src/targets/supabase.mjs +627 -0
  176. package/dist/src/targets/supabase_environment.d.mts +49 -0
  177. package/dist/src/targets/supabase_environment.mjs +162 -0
  178. package/dist/src/targets/target.d.mts +33 -0
  179. package/dist/src/targets/target.mjs +39 -0
  180. package/docs/README.md +1 -0
  181. package/docs/architecture.md +133 -10
  182. package/docs/configuration.md +9 -3
  183. package/docs/index.md +1 -0
  184. package/docs/releasing.md +8 -3
  185. package/docs/typescript.md +134 -0
  186. package/package.json +36 -29
  187. package/types/configuration.d.mts +5 -0
  188. package/scripts/install/create_root_config.mjs +0 -19
  189. package/src/README.md +0 -17
  190. package/src/application/session.mjs +0 -435
  191. package/src/baseline/artifact.mjs +0 -684
  192. package/src/baseline/builder.mjs +0 -164
  193. package/src/baseline/input_discovery.mjs +0 -155
  194. package/src/baseline/policy_review.mjs +0 -92
  195. package/src/baseline/preparation.mjs +0 -323
  196. package/src/baseline/privacy_engine.mjs +0 -1399
  197. package/src/baseline/privacy_structured_json.mjs +0 -554
  198. package/src/baseline/sanitization_policy.mjs +0 -299
  199. package/src/baseline/schema_snapshot.mjs +0 -113
  200. package/src/cli/arguments.mjs +0 -126
  201. package/src/cli/guided.mjs +0 -814
  202. package/src/cli/init_command.mjs +0 -94
  203. package/src/cli/rehearsal.mjs +0 -972
  204. package/src/cli/renderers.mjs +0 -638
  205. package/src/cli/runtime_commands.mjs +0 -707
  206. package/src/cli/source_commands.mjs +0 -355
  207. package/src/cli/terminal.mjs +0 -275
  208. package/src/identity/claim.mjs +0 -1165
  209. package/src/identity/provider_configuration.mjs +0 -177
  210. package/src/identity/storage.mjs +0 -165
  211. package/src/project/configuration.d.mts +0 -164
  212. package/src/project/configuration.mjs +0 -1337
  213. package/src/project/install_scaffold.mjs +0 -325
  214. package/src/project/setup.mjs +0 -474
  215. package/src/project/support_report.mjs +0 -105
  216. package/src/runtime/cleanup.mjs +0 -381
  217. package/src/runtime/migration_history.mjs +0 -220
  218. package/src/runtime/plan.mjs +0 -722
  219. package/src/runtime/policy.mjs +0 -438
  220. package/src/runtime/restore.mjs +0 -337
  221. package/src/runtime/service_environment.mjs +0 -82
  222. package/src/runtime/topology.mjs +0 -177
  223. package/src/shared/diagnostics.mjs +0 -207
  224. package/src/shared/node_runtime.mjs +0 -31
  225. package/src/shared/operation_guard.mjs +0 -162
  226. package/src/shared/process_environment.mjs +0 -68
  227. package/src/source/access.mjs +0 -798
  228. package/src/source/asset_transfer.mjs +0 -259
  229. package/src/source/baseline.mjs +0 -482
  230. package/src/source/external_reader.mjs +0 -368
  231. package/src/source/postgresql_access.mjs +0 -638
  232. package/src/source/privacy_paths.mjs +0 -38
  233. package/src/targets/postgresql.mjs +0 -808
  234. package/src/targets/supabase.mjs +0 -840
  235. package/src/targets/supabase_environment.mjs +0 -229
  236. package/src/targets/target.mjs +0 -66
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-beta.14",
3
+ "version": "0.1.0-beta.16",
4
4
  "private": false,
5
5
  "description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
6
6
  "repository": {
@@ -21,27 +21,26 @@
21
21
  "node": "^22.0.0 || ^24.0.0 || ^26.0.0"
22
22
  },
23
23
  "bin": {
24
- "rehearsal": "src/cli/rehearsal.mjs"
24
+ "rehearsal": "dist/src/cli/rehearsal.mjs"
25
25
  },
26
26
  "exports": {
27
27
  ".": {
28
- "types": "./src/project/configuration.d.mts",
29
- "import": "./src/project/configuration.mjs"
28
+ "types": "./types/configuration.d.mts",
29
+ "import": "./dist/src/index.mjs"
30
30
  },
31
- "./baseline": "./src/baseline/artifact.mjs",
32
- "./baseline-builder": "./src/baseline/builder.mjs",
33
- "./diagnostics": "./src/shared/diagnostics.mjs",
34
- "./migrations": "./src/runtime/migration_history.mjs",
35
- "./process-environment": "./src/shared/process_environment.mjs",
36
- "./privacy": "./src/baseline/privacy_engine.mjs",
37
- "./schema": "./src/baseline/schema_snapshot.mjs",
38
- "./service-environment": "./src/runtime/service_environment.mjs",
39
- "./source-access": "./src/source/access.mjs"
31
+ "./baseline": "./dist/src/baseline/artifact.mjs",
32
+ "./baseline-builder": "./dist/src/baseline/builder.mjs",
33
+ "./diagnostics": "./dist/src/shared/diagnostics.mjs",
34
+ "./migrations": "./dist/src/runtime/migration_history.mjs",
35
+ "./process-environment": "./dist/src/shared/process_environment.mjs",
36
+ "./privacy": "./dist/src/baseline/privacy_engine.mjs",
37
+ "./schema": "./dist/src/baseline/schema_snapshot.mjs",
38
+ "./service-environment": "./dist/src/runtime/service_environment.mjs",
39
+ "./source-access": "./dist/src/source/access.mjs"
40
40
  },
41
41
  "files": [
42
- "src",
43
- "scripts/install/create_root_config.mjs",
44
- "scripts/runtime/manage_database.mjs",
42
+ "dist",
43
+ "types",
45
44
  "docs/*.md",
46
45
  "README.md",
47
46
  "BENCHMARKS.md",
@@ -52,21 +51,26 @@
52
51
  "LICENSE"
53
52
  ],
54
53
  "scripts": {
55
- "postinstall": "node scripts/install/create_root_config.mjs",
54
+ "postinstall": "node dist/scripts/install/create_root_config.mjs",
55
+ "prepack": "npm run build",
56
+ "build": "node scripts/build/clean.mjs && tsc --project tsconfig.build.json",
56
57
  "docs:build": "node scripts/docs/build.mjs",
57
58
  "docs:preview": "node scripts/docs/serve.mjs",
58
- "test": "vitest run",
59
- "test:unit": "vitest run tests/unit",
60
- "test:integration": "vitest run tests/integration tests/contracts",
61
- "test:docs": "vitest run tests/contracts/documentationCommands.test.mjs",
62
- "test:fixture": "node scripts/verification/fixtures/supabase.mjs",
63
- "test:fixture:postgresql": "node scripts/verification/fixtures/postgresql.mjs",
64
- "test:fixture:dependent": "node scripts/verification/fixtures/dependent.mjs",
65
- "test:fixture:onboarding": "node scripts/verification/fixtures/onboarding.mjs",
66
- "test:fixture:standalone": "node scripts/verification/fixtures/standalone.mjs",
67
- "test:fixture:privacy": "node scripts/verification/fixtures/privacy.mjs",
68
- "check": "npm run check:syntax && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
69
- "check:syntax": "find src scripts tests -type f -name '*.mjs' -exec node --check {} +",
59
+ "test": "npm run build && vitest run",
60
+ "test:unit": "npm run build && vitest run tests/unit",
61
+ "test:integration": "npm run build && vitest run tests/integration tests/contracts",
62
+ "test:docs": "npm run build && vitest run tests/contracts/documentationCommands.test.mjs",
63
+ "test:fixture": "npm run build && node scripts/verification/run_scenario.mjs supabase",
64
+ "test:fixture:postgresql": "npm run build && node scripts/verification/run_scenario.mjs postgresql",
65
+ "test:fixture:dependent": "npm run build && node scripts/verification/run_scenario.mjs dependent-target",
66
+ "test:fixture:onboarding": "npm run build && node scripts/verification/run_scenario.mjs onboarding",
67
+ "test:fixture:typescript": "npm run build && node scripts/verification/run_scenario.mjs typescript",
68
+ "test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
69
+ "test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
70
+ "release:verify": "node scripts/verification/release_gate.mjs",
71
+ "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
72
+ "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
73
+ "typecheck": "npm run build && tsc --project tsconfig.json",
70
74
  "format": "prettier --write .",
71
75
  "format:check": "prettier --check .",
72
76
  "package:audit": "node scripts/verification/audit_package.mjs"
@@ -87,8 +91,11 @@
87
91
  "@fontsource-variable/dm-sans": "^5.3.0",
88
92
  "@fontsource-variable/fraunces": "^5.3.0",
89
93
  "@lydell/node-pty": "^1.2.0-beta.15",
94
+ "@types/node": "22.20.5",
95
+ "@types/pg": "^8.23.1",
90
96
  "marked": "^18.0.14",
91
97
  "prettier": "^3.9.6",
98
+ "typescript": "7.0.2",
92
99
  "vitest": "^5.0.2"
93
100
  }
94
101
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Compatibility declaration entry point.
3
+ * Public types are emitted from the TypeScript implementation in dist/src.
4
+ */
5
+ export * from "../dist/src/index.mjs";
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- /** Create safe project-root Rehearsal scaffolding during a local install. */
3
-
4
- import { fileURLToPath } from "node:url";
5
- import {
6
- createInstalledRehearsalScaffold,
7
- renderInstalledRehearsalScaffold,
8
- } from "../../src/project/install_scaffold.mjs";
9
-
10
- const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
11
-
12
- try {
13
- const result = await createInstalledRehearsalScaffold({ packageRoot });
14
- console.log(renderInstalledRehearsalScaffold(result));
15
- } catch (error) {
16
- console.warn(
17
- `Rehearsal could not safely create project files during installation: ${String(error?.message ?? error)}\nNext: run npx rehearsal from the application directory.`,
18
- );
19
- }
package/src/README.md DELETED
@@ -1,17 +0,0 @@
1
- # Source layout
2
-
3
- Production code is grouped by the responsibility it owns:
4
-
5
- - `application/` starts and proves the project application.
6
- - `baseline/` creates, validates, and stores safe baseline artifacts.
7
- - `cli/` is the human and automation entry point.
8
- - `identity/` associates copied data with verified local users.
9
- - `project/` owns configuration, setup, and support reporting.
10
- - `runtime/` plans, verifies, resets, migrates, and cleans local runtimes.
11
- - `shared/` contains small target-neutral infrastructure.
12
- - `source/` controls approved source access and baseline refresh.
13
- - `targets/` contains the PostgreSQL and Supabase runtime adapters.
14
-
15
- Place a new module in the domain that owns its policy. Keep database-specific behavior
16
- behind `targets/`, repository-only checks under `scripts/verification/`, and public
17
- imports behind the export map in `package.json`.
@@ -1,435 +0,0 @@
1
- /** Launch ordinary application commands against verified local runtime files. */
2
-
3
- import { spawn } from "node:child_process";
4
- import { readFile, stat } from "node:fs/promises";
5
- import { parseEnv } from "node:util";
6
- import {
7
- createCleanProcessEnvironment,
8
- isLoopbackUrl,
9
- } from "../shared/process_environment.mjs";
10
-
11
- const ENV_KEY = /^[A-Z][A-Z0-9_]*$/u;
12
- const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
13
- const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
14
-
15
- const parseCommand = (source) => {
16
- if (typeof source !== "string" || !source.trim())
17
- throw new Error("Application command is empty.");
18
- const values = [];
19
- let value = "";
20
- let quote = null;
21
- for (const character of source) {
22
- if (quote) {
23
- if (character === quote) quote = null;
24
- else value += character;
25
- } else if (character === '"' || character === "'") quote = character;
26
- else if (/\s/u.test(character)) {
27
- if (value) values.push(value);
28
- value = "";
29
- } else value += character;
30
- }
31
- if (quote)
32
- throw new Error("Application command contains an unmatched quote.");
33
- if (value) values.push(value);
34
- return values;
35
- };
36
-
37
- const readOwnedEnvironment = async (path) => {
38
- const details = await stat(path);
39
- if (!details.isFile() || (details.mode & 0o077) !== 0) {
40
- throw new Error(
41
- "Runtime environment file must be an owner-only regular file.",
42
- );
43
- }
44
- return parseEnv(await readFile(path, "utf8"));
45
- };
46
-
47
- const assertLocalValue = (key, value) => {
48
- if (/(_URL|_URI|_ENDPOINT)$/u.test(key)) {
49
- let parsed;
50
- try {
51
- parsed = new URL(value);
52
- } catch {
53
- throw new Error(`Mapped runtime variable ${key} is not a URL.`);
54
- }
55
- if (
56
- !isLoopbackUrl(value) &&
57
- !["postgres:", "postgresql:"].includes(parsed.protocol)
58
- ) {
59
- throw new Error(`Mapped runtime variable ${key} is not local.`);
60
- }
61
- if (
62
- ["postgres:", "postgresql:"].includes(parsed.protocol) &&
63
- !["127.0.0.1", "::1", "localhost"].includes(parsed.hostname)
64
- ) {
65
- throw new Error(`Mapped runtime variable ${key} is not local.`);
66
- }
67
- }
68
- };
69
-
70
- export const buildApplicationEnvironment = async ({ files, mappings }) => {
71
- if (!files?.primary)
72
- throw new Error("Primary runtime environment file is required.");
73
- const environments = {};
74
- for (const [name, path] of Object.entries(files)) {
75
- environments[name] = await readOwnedEnvironment(path);
76
- }
77
- if (!mappings || typeof mappings !== "object" || Array.isArray(mappings)) {
78
- throw new Error("Application environment mappings must be an object.");
79
- }
80
- const result = {};
81
- for (const [destination, reference] of Object.entries(mappings)) {
82
- if (!ENV_KEY.test(destination) || typeof reference !== "string") {
83
- throw new Error("Application environment mapping is invalid.");
84
- }
85
- const separator = reference.indexOf(":");
86
- if (separator < 1)
87
- throw new Error(
88
- `Application mapping ${destination} has an invalid reference.`,
89
- );
90
- const target = reference.slice(0, separator);
91
- const source = reference.slice(separator + 1);
92
- if (!ENV_KEY.test(source) || !environments[target]?.[source]) {
93
- throw new Error(
94
- `Application mapping ${destination} references a missing runtime value.`,
95
- );
96
- }
97
- assertLocalValue(source, environments[target][source]);
98
- result[destination] = environments[target][source];
99
- }
100
- return result;
101
- };
102
-
103
- const boundedAppend = (current, chunk) =>
104
- `${current}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES);
105
-
106
- const childHasExited = (child) =>
107
- child.exitCode !== null || child.signalCode !== null;
108
-
109
- const waitForChildExit = (child) => {
110
- if (childHasExited(child)) {
111
- return Promise.resolve({
112
- status: child.exitCode,
113
- signal: child.signalCode,
114
- });
115
- }
116
- return new Promise((resolve) => {
117
- child.once("exit", (status, signal) => resolve({ status, signal }));
118
- });
119
- };
120
-
121
- export const summarizeProjectCommandFailure = ({
122
- error,
123
- status,
124
- signal,
125
- stdout = "",
126
- stderr = "",
127
- stdoutByteCount,
128
- stderrByteCount,
129
- }) => {
130
- const stdoutBytes = stdoutByteCount ?? Buffer.byteLength(String(stdout));
131
- const stderrBytes = stderrByteCount ?? Buffer.byteLength(String(stderr));
132
- if (error) {
133
- const code = /^[A-Z0-9_]+$/u.test(error.code ?? "")
134
- ? ` (${error.code})`
135
- : "";
136
- return `command could not start${code}; output withheld`;
137
- }
138
- const outcome = signal
139
- ? `signal ${signal}`
140
- : `exit ${Number.isInteger(status) ? status : "unknown"}`;
141
- return `${outcome}; stdout ${stdoutBytes} bytes; stderr ${stderrBytes} bytes; output withheld`;
142
- };
143
-
144
- const waitForReadiness = async ({ readiness, fetchImplementation, child }) => {
145
- if (!readiness || !isLoopbackUrl(readiness.url)) {
146
- throw new Error("Application readiness must use a loopback URL.");
147
- }
148
- const expected = readiness.expectedStatus ?? 200;
149
- const deadline = Date.now() + (readiness.timeoutSeconds ?? 30) * 1_000;
150
- let lastStatus = null;
151
- while (Date.now() < deadline) {
152
- if (childHasExited(child))
153
- throw new Error("Application exited before becoming ready.");
154
- try {
155
- const response = await fetchImplementation(readiness.url, {
156
- redirect: "manual",
157
- signal: AbortSignal.timeout(2_000),
158
- });
159
- lastStatus = response.status;
160
- if (response.status === expected)
161
- return { url: readiness.url, status: response.status };
162
- } catch {
163
- // A bounded retry is expected while the local process starts.
164
- }
165
- await new Promise((resolve) => setTimeout(resolve, 200));
166
- }
167
- throw new Error(
168
- `Application readiness timed out${lastStatus === null ? "" : ` after HTTP ${lastStatus}`}.`,
169
- );
170
- };
171
-
172
- const stopOwnedChild = async (child) => {
173
- if (childHasExited(child)) return;
174
- const exit = waitForChildExit(child);
175
- const kill = (signal) => {
176
- try {
177
- if (process.platform === "win32") child.kill(signal);
178
- else process.kill(-child.pid, signal);
179
- } catch (error) {
180
- if (error?.code !== "ESRCH") throw error;
181
- }
182
- };
183
- kill("SIGTERM");
184
- await Promise.race([
185
- exit,
186
- new Promise((resolve) => setTimeout(resolve, 5_000)),
187
- ]);
188
- if (!childHasExited(child)) {
189
- kill("SIGKILL");
190
- await exit;
191
- }
192
- };
193
-
194
- export const startApplicationSession = async ({
195
- command,
196
- cwd,
197
- files,
198
- mappings,
199
- readiness,
200
- inheritedEnvironment = process.env,
201
- spawnImplementation = spawn,
202
- fetchImplementation = fetch,
203
- signalTarget = process,
204
- }) => {
205
- const mapped = await buildApplicationEnvironment({ files, mappings });
206
- const [program, ...args] = parseCommand(command);
207
- const child = spawnImplementation(program, args, {
208
- cwd,
209
- env: createCleanProcessEnvironment({
210
- inheritedEnvironment,
211
- overrides: mapped,
212
- }),
213
- detached: process.platform !== "win32",
214
- stdio: ["ignore", "pipe", "pipe"],
215
- });
216
- let stdout = "";
217
- let stderr = "";
218
- child.stdout?.on("data", (chunk) => {
219
- stdout = boundedAppend(stdout, chunk.toString("utf8"));
220
- });
221
- child.stderr?.on("data", (chunk) => {
222
- stderr = boundedAppend(stderr, chunk.toString("utf8"));
223
- });
224
- await new Promise((resolve, reject) => {
225
- child.once("spawn", resolve);
226
- child.once("error", reject);
227
- });
228
- let interruptStartup;
229
- const interruption = new Promise((_, reject) => {
230
- interruptStartup = (signal) =>
231
- reject(new Error(`Application startup was interrupted by ${signal}.`));
232
- });
233
- const startupHandlers = new Map(
234
- SESSION_SIGNALS.map((signal) => [signal, () => interruptStartup(signal)]),
235
- );
236
- for (const [signal, handler] of startupHandlers) {
237
- signalTarget.prependListener(signal, handler);
238
- }
239
- try {
240
- const ready = await Promise.race([
241
- waitForReadiness({
242
- readiness,
243
- fetchImplementation,
244
- child,
245
- }),
246
- interruption,
247
- ]);
248
- return Object.freeze({
249
- pid: child.pid,
250
- ready,
251
- stop: () => stopOwnedChild(child),
252
- wait: () => waitForChildExit(child),
253
- diagnostics: () => ({
254
- stdoutBytes: Buffer.byteLength(stdout),
255
- stderrBytes: Buffer.byteLength(stderr),
256
- }),
257
- });
258
- } catch (error) {
259
- await stopOwnedChild(child);
260
- throw error;
261
- } finally {
262
- for (const [signal, handler] of startupHandlers) {
263
- signalTarget.removeListener(signal, handler);
264
- }
265
- }
266
- };
267
-
268
- /**
269
- * Keep a ready application session attached until the user interrupts it.
270
- * The child process group is always stopped before this function returns.
271
- */
272
- export const holdApplicationSession = async ({
273
- session,
274
- signalTarget = process,
275
- input = process.stdin,
276
- onReady = () => undefined,
277
- }) => {
278
- if (
279
- !session ||
280
- typeof session.wait !== "function" ||
281
- typeof session.stop !== "function"
282
- ) {
283
- throw new Error("A started application session is required.");
284
- }
285
- let finishInterruption;
286
- const interruption = new Promise((resolve) => {
287
- finishInterruption = resolve;
288
- });
289
- const handlers = new Map(
290
- SESSION_SIGNALS.map((signal) => [
291
- signal,
292
- () => finishInterruption({ kind: "interrupted", signal }),
293
- ]),
294
- );
295
- for (const [signal, handler] of handlers) {
296
- signalTarget.prependListener(signal, handler);
297
- }
298
- const onInput = (chunk) => {
299
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
300
- if (bytes.includes(0x03)) {
301
- finishInterruption({ kind: "interrupted", signal: "SIGINT" });
302
- } else if (bytes.includes(0x1a)) {
303
- finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
304
- }
305
- };
306
- if (input?.isTTY) input.prependListener("data", onInput);
307
- try {
308
- onReady(session.ready);
309
- const outcome = await Promise.race([
310
- interruption,
311
- session.wait().then((exit) => ({ kind: "exited", ...exit })),
312
- ]);
313
- if (outcome.kind === "exited") {
314
- const diagnostics = session.diagnostics?.() ?? {};
315
- throw new Error(
316
- `Application exited before the sandbox was closed (${summarizeProjectCommandFailure(
317
- {
318
- status: outcome.status,
319
- signal: outcome.signal,
320
- stdoutByteCount: diagnostics.stdoutBytes ?? 0,
321
- stderrByteCount: diagnostics.stderrBytes ?? 0,
322
- },
323
- )}).`,
324
- );
325
- }
326
- return Object.freeze({
327
- signal: outcome.signal,
328
- diagnostics: session.diagnostics?.() ?? {
329
- stdoutBytes: 0,
330
- stderrBytes: 0,
331
- },
332
- });
333
- } finally {
334
- try {
335
- // npm can forward the terminal's SIGINT after the CLI receives it.
336
- // Keep our handlers installed until the owned process group is fully
337
- // stopped so a duplicate signal cannot terminate Rehearsal mid-cleanup.
338
- await session.stop();
339
- } finally {
340
- for (const [signal, handler] of handlers) {
341
- signalTarget.removeListener(signal, handler);
342
- }
343
- if (input?.isTTY) input.removeListener("data", onInput);
344
- }
345
- }
346
- };
347
-
348
- const valueAtPath = (value, path) => {
349
- let current = value;
350
- for (const segment of path) {
351
- if (
352
- current === null ||
353
- typeof current !== "object" ||
354
- !Object.hasOwn(current, segment)
355
- ) {
356
- return undefined;
357
- }
358
- current = current[segment];
359
- }
360
- return current;
361
- };
362
-
363
- export const runHttpProofs = async ({
364
- checks,
365
- fetchImplementation = fetch,
366
- }) => {
367
- if (!Array.isArray(checks) || checks.length < 2) {
368
- throw new Error(
369
- "HTTP proof requires explicit positive and negative checks.",
370
- );
371
- }
372
- if (
373
- !checks.some((check) => check.kind === "positive") ||
374
- !checks.some((check) => check.kind === "negative")
375
- ) {
376
- throw new Error(
377
- "HTTP proof requires at least one positive and one negative check.",
378
- );
379
- }
380
- const results = [];
381
- for (const check of checks) {
382
- if (!isLoopbackUrl(check.url))
383
- throw new Error(`HTTP proof ${check.name} is not local.`);
384
- const response = await fetchImplementation(check.url, {
385
- method: check.method ?? "GET",
386
- redirect: "manual",
387
- signal: AbortSignal.timeout(check.timeoutMs ?? 5_000),
388
- });
389
- if (response.status !== check.expectedStatus) {
390
- throw new Error(
391
- `HTTP proof ${check.name} expected status ${check.expectedStatus} but received ${response.status}.`,
392
- );
393
- }
394
- const bytes = Buffer.from(await response.arrayBuffer());
395
- if (bytes.length > 1024 * 1024)
396
- throw new Error(`HTTP proof ${check.name} returned too much data.`);
397
- if (check.json) {
398
- let parsed;
399
- try {
400
- parsed = JSON.parse(bytes.toString("utf8"));
401
- } catch {
402
- throw new Error(`HTTP proof ${check.name} did not return JSON.`);
403
- }
404
- const actual = valueAtPath(parsed, check.json.path);
405
- if (
406
- Object.hasOwn(check.json, "equals") &&
407
- !Object.is(actual, check.json.equals)
408
- ) {
409
- throw new Error(
410
- `HTTP proof ${check.name} did not return its required value.`,
411
- );
412
- }
413
- if (
414
- Object.hasOwn(check.json, "notEquals") &&
415
- Object.is(actual, check.json.notEquals)
416
- ) {
417
- throw new Error(`HTTP proof ${check.name} returned a forbidden value.`);
418
- }
419
- if (
420
- check.json.minimumItems !== undefined &&
421
- (!Array.isArray(actual) || actual.length < check.json.minimumItems)
422
- ) {
423
- throw new Error(
424
- `HTTP proof ${check.name} returned too few matching items.`,
425
- );
426
- }
427
- }
428
- results.push({
429
- name: check.name,
430
- kind: check.kind,
431
- status: response.status,
432
- });
433
- }
434
- return Object.freeze(results);
435
- };