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

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 (162) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -0
  3. package/dist/package.json +100 -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_engine.d.mts +162 -0
  21. package/dist/src/baseline/privacy_engine.mjs +1162 -0
  22. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  23. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  24. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  25. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  26. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  27. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  28. package/dist/src/cli/arguments.d.mts +43 -0
  29. package/dist/src/cli/arguments.mjs +135 -0
  30. package/dist/src/cli/guided.d.mts +51 -0
  31. package/dist/src/cli/guided.mjs +713 -0
  32. package/dist/src/cli/init_command.d.mts +21 -0
  33. package/dist/src/cli/init_command.mjs +90 -0
  34. package/dist/src/cli/project_commands.d.mts +157 -0
  35. package/dist/src/cli/project_commands.mjs +183 -0
  36. package/dist/src/cli/rehearsal.d.mts +7 -0
  37. package/dist/src/cli/rehearsal.mjs +706 -0
  38. package/dist/src/cli/renderers.d.mts +299 -0
  39. package/dist/src/cli/renderers.mjs +554 -0
  40. package/dist/src/cli/runtime_commands.d.mts +265 -0
  41. package/dist/src/cli/runtime_commands.mjs +599 -0
  42. package/dist/src/cli/source_commands.d.mts +432 -0
  43. package/dist/src/cli/source_commands.mjs +299 -0
  44. package/dist/src/cli/terminal.d.mts +59 -0
  45. package/dist/src/cli/terminal.mjs +230 -0
  46. package/dist/src/identity/claim.d.mts +144 -0
  47. package/dist/src/identity/claim.mjs +947 -0
  48. package/dist/src/identity/provider_configuration.d.mts +37 -0
  49. package/dist/src/identity/provider_configuration.mjs +132 -0
  50. package/dist/src/identity/storage.d.mts +21 -0
  51. package/dist/src/identity/storage.mjs +122 -0
  52. package/dist/src/index.d.mts +4 -0
  53. package/dist/src/index.mjs +3 -0
  54. package/dist/src/project/configuration.d.mts +181 -0
  55. package/dist/src/project/configuration.mjs +958 -0
  56. package/dist/src/project/install_scaffold.d.mts +42 -0
  57. package/dist/src/project/install_scaffold.mjs +263 -0
  58. package/dist/src/project/setup.d.mts +87 -0
  59. package/dist/src/project/setup.mjs +414 -0
  60. package/dist/src/project/support_report.d.mts +88 -0
  61. package/dist/src/project/support_report.mjs +83 -0
  62. package/dist/src/runtime/cleanup.d.mts +85 -0
  63. package/dist/src/runtime/cleanup.mjs +311 -0
  64. package/dist/src/runtime/migration_history.d.mts +51 -0
  65. package/dist/src/runtime/migration_history.mjs +170 -0
  66. package/dist/src/runtime/plan.d.mts +134 -0
  67. package/dist/src/runtime/plan.mjs +636 -0
  68. package/dist/src/runtime/policy.d.mts +66 -0
  69. package/dist/src/runtime/policy.mjs +369 -0
  70. package/dist/src/runtime/restore.d.mts +22 -0
  71. package/dist/src/runtime/restore.mjs +276 -0
  72. package/dist/src/runtime/service_environment.d.mts +11 -0
  73. package/dist/src/runtime/service_environment.mjs +58 -0
  74. package/dist/src/runtime/topology.d.mts +51 -0
  75. package/dist/src/runtime/topology.mjs +128 -0
  76. package/dist/src/shared/diagnostics.d.mts +97 -0
  77. package/dist/src/shared/diagnostics.mjs +170 -0
  78. package/dist/src/shared/human_output.d.mts +2 -0
  79. package/{src → dist/src}/shared/human_output.mjs +1 -3
  80. package/dist/src/shared/node_runtime.d.mts +14 -0
  81. package/dist/src/shared/node_runtime.mjs +25 -0
  82. package/dist/src/shared/operation_guard.d.mts +22 -0
  83. package/dist/src/shared/operation_guard.mjs +150 -0
  84. package/dist/src/shared/process_environment.d.mts +15 -0
  85. package/dist/src/shared/process_environment.mjs +50 -0
  86. package/dist/src/source/access.d.mts +165 -0
  87. package/dist/src/source/access.mjs +602 -0
  88. package/dist/src/source/asset_transfer.d.mts +27 -0
  89. package/dist/src/source/asset_transfer.mjs +202 -0
  90. package/dist/src/source/baseline.d.mts +66 -0
  91. package/dist/src/source/baseline.mjs +372 -0
  92. package/dist/src/source/external_reader.d.mts +42 -0
  93. package/dist/src/source/external_reader.mjs +278 -0
  94. package/dist/src/source/postgresql_access.d.mts +86 -0
  95. package/dist/src/source/postgresql_access.mjs +454 -0
  96. package/dist/src/source/privacy_paths.d.mts +10 -0
  97. package/dist/src/source/privacy_paths.mjs +27 -0
  98. package/dist/src/targets/driver.d.mts +2 -0
  99. package/dist/src/targets/driver.mjs +13 -0
  100. package/dist/src/targets/postgresql.d.mts +5 -0
  101. package/dist/src/targets/postgresql.mjs +641 -0
  102. package/dist/src/targets/supabase.d.mts +8 -0
  103. package/dist/src/targets/supabase.mjs +659 -0
  104. package/dist/src/targets/supabase_environment.d.mts +49 -0
  105. package/dist/src/targets/supabase_environment.mjs +162 -0
  106. package/dist/src/targets/target.d.mts +33 -0
  107. package/dist/src/targets/target.mjs +39 -0
  108. package/docs/README.md +1 -0
  109. package/docs/architecture.md +17 -9
  110. package/docs/configuration.md +9 -3
  111. package/docs/index.md +1 -0
  112. package/docs/typescript.md +87 -0
  113. package/package.json +35 -29
  114. package/scripts/install/create_root_config.mjs +0 -19
  115. package/src/README.md +0 -17
  116. package/src/application/session.mjs +0 -435
  117. package/src/baseline/artifact.mjs +0 -684
  118. package/src/baseline/builder.mjs +0 -164
  119. package/src/baseline/input_discovery.mjs +0 -155
  120. package/src/baseline/policy_review.mjs +0 -92
  121. package/src/baseline/preparation.mjs +0 -323
  122. package/src/baseline/privacy_engine.mjs +0 -1399
  123. package/src/baseline/privacy_structured_json.mjs +0 -554
  124. package/src/baseline/sanitization_policy.mjs +0 -299
  125. package/src/baseline/schema_snapshot.mjs +0 -113
  126. package/src/cli/arguments.mjs +0 -126
  127. package/src/cli/guided.mjs +0 -814
  128. package/src/cli/init_command.mjs +0 -94
  129. package/src/cli/rehearsal.mjs +0 -972
  130. package/src/cli/renderers.mjs +0 -638
  131. package/src/cli/runtime_commands.mjs +0 -707
  132. package/src/cli/source_commands.mjs +0 -355
  133. package/src/cli/terminal.mjs +0 -275
  134. package/src/identity/claim.mjs +0 -1165
  135. package/src/identity/provider_configuration.mjs +0 -177
  136. package/src/identity/storage.mjs +0 -165
  137. package/src/project/configuration.mjs +0 -1337
  138. package/src/project/install_scaffold.mjs +0 -325
  139. package/src/project/setup.mjs +0 -474
  140. package/src/project/support_report.mjs +0 -105
  141. package/src/runtime/cleanup.mjs +0 -381
  142. package/src/runtime/migration_history.mjs +0 -220
  143. package/src/runtime/plan.mjs +0 -722
  144. package/src/runtime/policy.mjs +0 -438
  145. package/src/runtime/restore.mjs +0 -337
  146. package/src/runtime/service_environment.mjs +0 -82
  147. package/src/runtime/topology.mjs +0 -177
  148. package/src/shared/diagnostics.mjs +0 -207
  149. package/src/shared/node_runtime.mjs +0 -31
  150. package/src/shared/operation_guard.mjs +0 -162
  151. package/src/shared/process_environment.mjs +0 -68
  152. package/src/source/access.mjs +0 -798
  153. package/src/source/asset_transfer.mjs +0 -259
  154. package/src/source/baseline.mjs +0 -482
  155. package/src/source/external_reader.mjs +0 -368
  156. package/src/source/postgresql_access.mjs +0 -638
  157. package/src/source/privacy_paths.mjs +0 -38
  158. package/src/targets/postgresql.mjs +0 -808
  159. package/src/targets/supabase.mjs +0 -840
  160. package/src/targets/supabase_environment.mjs +0 -229
  161. package/src/targets/target.mjs +0 -66
  162. /package/{src/project → types}/configuration.d.mts +0 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ### Added
9
+
10
+ - A strict, no-emit TypeScript gate now checks the first shared runtime utilities and
11
+ verifies that consumers can compile against Rehearsal's public configuration types.
12
+ - The documentation site now explains the supported TypeScript surface with examples
13
+ that are compiled and checked against their rendered documentation source.
14
+ - All shipped implementation and executable-helper source now uses TypeScript. Clean
15
+ builds emit Node-compatible ESM and declarations into the reviewed package boundary,
16
+ and tests exercise that compiled output rather than an alternate source path.
17
+ - The published build omits repository-only JavaScript source maps while retaining
18
+ runtime JavaScript and declarations, keeping the installed package compact.
19
+
20
+ ### Fixed
21
+
22
+ - Packed onboarding chooses an available local port block for preserved configuration
23
+ fixtures, so unrelated running Rehearsal projects cannot make the isolation test fail.
24
+
8
25
  ## [0.1.0-beta.14] - 2026-10-05
9
26
 
10
27
  ### Added
package/README.md CHANGED
@@ -145,6 +145,7 @@ Reference:
145
145
 
146
146
  - [CLI commands](docs/commands.md)
147
147
  - [Configuration](docs/configuration.md)
148
+ - [TypeScript types](docs/typescript.md)
148
149
  - [Baselines](docs/baselines.md)
149
150
  - [Sanitization](docs/sanitization.md)
150
151
  - [Security model](docs/security-model.md)
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@rehearsal-db/core",
3
+ "version": "0.1.0-beta.15",
4
+ "private": false,
5
+ "description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Ddupasquier/rehearsal-db.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/Ddupasquier/rehearsal-db/issues"
12
+ },
13
+ "homepage": "https://ddupasquier.github.io/rehearsal-db/",
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "tag": "beta",
17
+ "provenance": true
18
+ },
19
+ "type": "module",
20
+ "engines": {
21
+ "node": "^22.0.0 || ^24.0.0 || ^26.0.0"
22
+ },
23
+ "bin": {
24
+ "rehearsal": "dist/src/cli/rehearsal.mjs"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./types/configuration.d.mts",
29
+ "import": "./dist/src/index.mjs"
30
+ },
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
+ },
41
+ "files": [
42
+ "dist",
43
+ "types",
44
+ "docs/*.md",
45
+ "README.md",
46
+ "BENCHMARKS.md",
47
+ "CHANGELOG.md",
48
+ "SECURITY.md",
49
+ "COMPATIBILITY.md",
50
+ "SUPPORT.md",
51
+ "LICENSE"
52
+ ],
53
+ "scripts": {
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",
57
+ "docs:build": "node scripts/docs/build.mjs",
58
+ "docs:preview": "node scripts/docs/serve.mjs",
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/fixtures/supabase.mjs",
64
+ "test:fixture:postgresql": "npm run build && node scripts/verification/fixtures/postgresql.mjs",
65
+ "test:fixture:dependent": "npm run build && node scripts/verification/fixtures/dependent.mjs",
66
+ "test:fixture:onboarding": "npm run build && node scripts/verification/fixtures/onboarding.mjs",
67
+ "test:fixture:typescript": "npm run build && node scripts/verification/fixtures/typescript.mjs",
68
+ "test:fixture:standalone": "npm run build && node scripts/verification/fixtures/standalone.mjs",
69
+ "test:fixture:privacy": "npm run build && node scripts/verification/fixtures/privacy.mjs",
70
+ "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
71
+ "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
72
+ "typecheck": "tsc --project tsconfig.json",
73
+ "format": "prettier --write .",
74
+ "format:check": "prettier --check .",
75
+ "package:audit": "node scripts/verification/audit_package.mjs"
76
+ },
77
+ "keywords": [
78
+ "postgresql",
79
+ "supabase",
80
+ "migration",
81
+ "testing",
82
+ "database"
83
+ ],
84
+ "license": "MIT",
85
+ "dependencies": {
86
+ "@clack/prompts": "^1.8.1",
87
+ "pg": "^8.23.1"
88
+ },
89
+ "devDependencies": {
90
+ "@fontsource-variable/dm-sans": "^5.3.0",
91
+ "@fontsource-variable/fraunces": "^5.3.0",
92
+ "@lydell/node-pty": "^1.2.0-beta.15",
93
+ "@types/node": "22.20.5",
94
+ "@types/pg": "^8.23.1",
95
+ "marked": "^18.0.14",
96
+ "prettier": "^3.9.6",
97
+ "typescript": "7.0.2",
98
+ "vitest": "^5.0.2"
99
+ }
100
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ /** Create safe project-root Rehearsal scaffolding during a local install. */
3
+ export {};
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /** Create safe project-root Rehearsal scaffolding during a local install. */
3
+ import { fileURLToPath } from "node:url";
4
+ import { createInstalledRehearsalScaffold, renderInstalledRehearsalScaffold, } from "../../src/project/install_scaffold.mjs";
5
+ const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
6
+ try {
7
+ const result = await createInstalledRehearsalScaffold({ packageRoot });
8
+ console.log(renderInstalledRehearsalScaffold(result));
9
+ }
10
+ catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ console.warn(`Rehearsal could not safely create project files during installation: ${message}\nNext: run npx rehearsal from the application directory.`);
13
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Purpose: Select the configured database runtime and forward the requested
4
+ * lifecycle action to its isolated driver.
5
+ */
6
+ export {};
@@ -3,16 +3,14 @@
3
3
  * Purpose: Select the configured database runtime and forward the requested
4
4
  * lifecycle action to its isolated driver.
5
5
  */
6
-
7
6
  import { loadRehearsalConfig } from "../../src/project/configuration.mjs";
8
- import {
9
- parseRuntimeInvocation,
10
- runRuntimeTarget,
11
- } from "../../src/targets/target.mjs";
12
-
7
+ import { parseRuntimeInvocation } from "../../src/targets/target.mjs";
8
+ import { runRuntimeTarget } from "../../src/targets/driver.mjs";
13
9
  const invocation = parseRuntimeInvocation();
14
10
  const { config } = await loadRehearsalConfig({
15
- projectRoot: process.cwd(),
16
- configPath: invocation.configPath,
11
+ projectRoot: process.cwd(),
12
+ ...(invocation.configPath === undefined
13
+ ? {}
14
+ : { configPath: invocation.configPath }),
17
15
  });
18
16
  await runRuntimeTarget(config.runtime.target);
@@ -0,0 +1,96 @@
1
+ /** Launch ordinary application commands against verified local runtime files. */
2
+ import { spawn } from "node:child_process";
3
+ import type { EventEmitter } from "node:events";
4
+ declare const SESSION_SIGNALS: string[];
5
+ type SessionSignal = (typeof SESSION_SIGNALS)[number];
6
+ export interface ApplicationReadiness {
7
+ readonly url: string;
8
+ readonly expectedStatus?: number;
9
+ readonly timeoutSeconds?: number;
10
+ }
11
+ export interface ApplicationSession {
12
+ readonly pid: number | undefined;
13
+ readonly ready: Readonly<{
14
+ url: string;
15
+ status: number;
16
+ }>;
17
+ readonly stop: () => Promise<void>;
18
+ readonly wait: () => Promise<ChildExit>;
19
+ readonly diagnostics: () => ApplicationDiagnostics;
20
+ }
21
+ export interface ApplicationDiagnostics {
22
+ readonly stdoutBytes: number;
23
+ readonly stderrBytes: number;
24
+ }
25
+ interface ChildExit {
26
+ readonly status: number | null;
27
+ readonly signal: NodeJS.Signals | null;
28
+ }
29
+ interface SessionInput {
30
+ readonly isTTY?: boolean;
31
+ prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
32
+ removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
33
+ }
34
+ interface EnvironmentInput {
35
+ readonly files: Readonly<Record<string, string>> & {
36
+ readonly primary: string;
37
+ };
38
+ readonly mappings: Readonly<Record<string, string>>;
39
+ }
40
+ interface HttpJsonAssertion {
41
+ readonly path: readonly string[];
42
+ readonly equals?: unknown;
43
+ readonly notEquals?: unknown;
44
+ readonly minimumItems?: number;
45
+ }
46
+ export interface HttpProofCheck {
47
+ readonly name: string;
48
+ readonly kind: "positive" | "negative";
49
+ readonly url: string;
50
+ readonly method?: string;
51
+ readonly expectedStatus: number;
52
+ readonly timeoutMs?: number;
53
+ readonly json?: HttpJsonAssertion | null;
54
+ }
55
+ type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
56
+ export declare const buildApplicationEnvironment: ({ files, mappings, }: EnvironmentInput) => Promise<Record<string, string>>;
57
+ export declare const summarizeProjectCommandFailure: ({ error, status, signal, stdout, stderr, stdoutByteCount, stderrByteCount, }: {
58
+ error?: NodeJS.ErrnoException;
59
+ status: number | null;
60
+ signal: NodeJS.Signals | null;
61
+ stdout?: string;
62
+ stderr?: string;
63
+ stdoutByteCount?: number;
64
+ stderrByteCount?: number;
65
+ }) => string;
66
+ export declare const startApplicationSession: ({ command, cwd, files, mappings, readiness, inheritedEnvironment, spawnImplementation, fetchImplementation, signalTarget, }: EnvironmentInput & {
67
+ command: string;
68
+ cwd: string;
69
+ readiness: ApplicationReadiness;
70
+ inheritedEnvironment?: NodeJS.ProcessEnv;
71
+ spawnImplementation?: typeof spawn;
72
+ fetchImplementation?: typeof fetch;
73
+ signalTarget?: SignalTarget;
74
+ }) => Promise<ApplicationSession>;
75
+ /**
76
+ * Keep a ready application session attached until the user interrupts it.
77
+ * The child process group is always stopped before this function returns.
78
+ */
79
+ export declare const holdApplicationSession: ({ session, signalTarget, input, onReady, }: {
80
+ session: ApplicationSession;
81
+ signalTarget?: SignalTarget;
82
+ input?: SessionInput;
83
+ onReady?: (ready: ApplicationSession["ready"]) => void;
84
+ }) => Promise<Readonly<{
85
+ signal: SessionSignal;
86
+ diagnostics: ApplicationDiagnostics;
87
+ }>>;
88
+ export declare const runHttpProofs: ({ checks, fetchImplementation, }: {
89
+ checks: readonly HttpProofCheck[];
90
+ fetchImplementation?: typeof fetch;
91
+ }) => Promise<readonly Readonly<{
92
+ name: string;
93
+ kind: "positive" | "negative";
94
+ status: number;
95
+ }>[]>;
96
+ export {};
@@ -0,0 +1,380 @@
1
+ /** Launch ordinary application commands against verified local runtime files. */
2
+ import { spawn } from "node:child_process";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { parseEnv } from "node:util";
5
+ import { createCleanProcessEnvironment, isLoopbackUrl, } from "../shared/process_environment.mjs";
6
+ const ENV_KEY = /^[A-Z][A-Z0-9_]*$/u;
7
+ const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
8
+ const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
9
+ const parseCommand = (source) => {
10
+ if (typeof source !== "string" || !source.trim())
11
+ throw new Error("Application command is empty.");
12
+ const values = [];
13
+ let value = "";
14
+ let quote = null;
15
+ for (const character of source) {
16
+ if (quote) {
17
+ if (character === quote)
18
+ quote = null;
19
+ else
20
+ value += character;
21
+ }
22
+ else if (character === '"' || character === "'")
23
+ quote = character;
24
+ else if (/\s/u.test(character)) {
25
+ if (value)
26
+ values.push(value);
27
+ value = "";
28
+ }
29
+ else
30
+ value += character;
31
+ }
32
+ if (quote)
33
+ throw new Error("Application command contains an unmatched quote.");
34
+ if (value)
35
+ values.push(value);
36
+ const [program, ...args] = values;
37
+ if (!program)
38
+ throw new Error("Application command is empty.");
39
+ return [program, ...args];
40
+ };
41
+ const readOwnedEnvironment = async (path) => {
42
+ const details = await stat(path);
43
+ if (!details.isFile() || (details.mode & 0o077) !== 0) {
44
+ throw new Error("Runtime environment file must be an owner-only regular file.");
45
+ }
46
+ const parsed = parseEnv(await readFile(path, "utf8"));
47
+ return Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === "string"));
48
+ };
49
+ const assertLocalValue = (key, value) => {
50
+ if (/(_URL|_URI|_ENDPOINT)$/u.test(key)) {
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(value);
54
+ }
55
+ catch {
56
+ throw new Error(`Mapped runtime variable ${key} is not a URL.`);
57
+ }
58
+ if (!isLoopbackUrl(value) &&
59
+ !["postgres:", "postgresql:"].includes(parsed.protocol)) {
60
+ throw new Error(`Mapped runtime variable ${key} is not local.`);
61
+ }
62
+ if (["postgres:", "postgresql:"].includes(parsed.protocol) &&
63
+ !["127.0.0.1", "::1", "localhost"].includes(parsed.hostname)) {
64
+ throw new Error(`Mapped runtime variable ${key} is not local.`);
65
+ }
66
+ }
67
+ };
68
+ export const buildApplicationEnvironment = async ({ files, mappings, }) => {
69
+ if (!files?.primary)
70
+ throw new Error("Primary runtime environment file is required.");
71
+ const environments = {};
72
+ for (const [name, path] of Object.entries(files)) {
73
+ environments[name] = await readOwnedEnvironment(path);
74
+ }
75
+ if (!mappings || typeof mappings !== "object" || Array.isArray(mappings)) {
76
+ throw new Error("Application environment mappings must be an object.");
77
+ }
78
+ const result = {};
79
+ for (const [destination, reference] of Object.entries(mappings)) {
80
+ if (!ENV_KEY.test(destination) || typeof reference !== "string") {
81
+ throw new Error("Application environment mapping is invalid.");
82
+ }
83
+ const separator = reference.indexOf(":");
84
+ if (separator < 1)
85
+ throw new Error(`Application mapping ${destination} has an invalid reference.`);
86
+ const target = reference.slice(0, separator);
87
+ const source = reference.slice(separator + 1);
88
+ const sourceValue = environments[target]?.[source];
89
+ if (!ENV_KEY.test(source) || !sourceValue) {
90
+ throw new Error(`Application mapping ${destination} references a missing runtime value.`);
91
+ }
92
+ assertLocalValue(source, sourceValue);
93
+ result[destination] = sourceValue;
94
+ }
95
+ return result;
96
+ };
97
+ const boundedAppend = (current, chunk) => `${current}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES);
98
+ const childHasExited = (child) => child.exitCode !== null || child.signalCode !== null;
99
+ const waitForChildExit = (child) => {
100
+ if (childHasExited(child)) {
101
+ return Promise.resolve({
102
+ status: child.exitCode,
103
+ signal: child.signalCode,
104
+ });
105
+ }
106
+ return new Promise((resolve) => {
107
+ child.once("exit", (status, signal) => resolve({ status, signal }));
108
+ });
109
+ };
110
+ export const summarizeProjectCommandFailure = ({ error, status, signal, stdout = "", stderr = "", stdoutByteCount, stderrByteCount, }) => {
111
+ const stdoutBytes = stdoutByteCount ?? Buffer.byteLength(String(stdout));
112
+ const stderrBytes = stderrByteCount ?? Buffer.byteLength(String(stderr));
113
+ if (error) {
114
+ const code = /^[A-Z0-9_]+$/u.test(error.code ?? "")
115
+ ? ` (${error.code})`
116
+ : "";
117
+ return `command could not start${code}; output withheld`;
118
+ }
119
+ const outcome = signal
120
+ ? `signal ${signal}`
121
+ : `exit ${Number.isInteger(status) ? status : "unknown"}`;
122
+ return `${outcome}; stdout ${stdoutBytes} bytes; stderr ${stderrBytes} bytes; output withheld`;
123
+ };
124
+ const waitForReadiness = async ({ readiness, fetchImplementation, child, }) => {
125
+ if (!readiness || !isLoopbackUrl(readiness.url)) {
126
+ throw new Error("Application readiness must use a loopback URL.");
127
+ }
128
+ const expected = readiness.expectedStatus ?? 200;
129
+ const deadline = Date.now() + (readiness.timeoutSeconds ?? 30) * 1_000;
130
+ let lastStatus = null;
131
+ while (Date.now() < deadline) {
132
+ if (childHasExited(child))
133
+ throw new Error("Application exited before becoming ready.");
134
+ try {
135
+ const response = await fetchImplementation(readiness.url, {
136
+ redirect: "manual",
137
+ signal: AbortSignal.timeout(2_000),
138
+ });
139
+ lastStatus = response.status;
140
+ if (response.status === expected)
141
+ return { url: readiness.url, status: response.status };
142
+ }
143
+ catch {
144
+ // A bounded retry is expected while the local process starts.
145
+ }
146
+ await new Promise((resolve) => setTimeout(resolve, 200));
147
+ }
148
+ throw new Error(`Application readiness timed out${lastStatus === null ? "" : ` after HTTP ${lastStatus}`}.`);
149
+ };
150
+ const stopOwnedChild = async (child) => {
151
+ if (childHasExited(child))
152
+ return;
153
+ const exit = waitForChildExit(child);
154
+ const kill = (signal) => {
155
+ try {
156
+ if (process.platform === "win32")
157
+ child.kill(signal);
158
+ else {
159
+ if (child.pid === undefined) {
160
+ throw new Error("Application child process has no process identifier.");
161
+ }
162
+ process.kill(-child.pid, signal);
163
+ }
164
+ }
165
+ catch (error) {
166
+ if (typeof error !== "object" ||
167
+ error === null ||
168
+ !("code" in error) ||
169
+ error.code !== "ESRCH")
170
+ throw error;
171
+ }
172
+ };
173
+ kill("SIGTERM");
174
+ await Promise.race([
175
+ exit,
176
+ new Promise((resolve) => setTimeout(resolve, 5_000)),
177
+ ]);
178
+ if (!childHasExited(child)) {
179
+ kill("SIGKILL");
180
+ await exit;
181
+ }
182
+ };
183
+ export const startApplicationSession = async ({ command, cwd, files, mappings, readiness, inheritedEnvironment = process.env, spawnImplementation = spawn, fetchImplementation = fetch, signalTarget = process, }) => {
184
+ const mapped = await buildApplicationEnvironment({ files, mappings });
185
+ const [program, ...args] = parseCommand(command);
186
+ const child = spawnImplementation(program, args, {
187
+ cwd,
188
+ env: createCleanProcessEnvironment({
189
+ inheritedEnvironment,
190
+ overrides: mapped,
191
+ }),
192
+ detached: process.platform !== "win32",
193
+ stdio: ["ignore", "pipe", "pipe"],
194
+ });
195
+ let stdout = "";
196
+ let stderr = "";
197
+ child.stdout?.on("data", (chunk) => {
198
+ stdout = boundedAppend(stdout, chunk.toString("utf8"));
199
+ });
200
+ child.stderr?.on("data", (chunk) => {
201
+ stderr = boundedAppend(stderr, chunk.toString("utf8"));
202
+ });
203
+ await new Promise((resolve, reject) => {
204
+ child.once("spawn", resolve);
205
+ child.once("error", reject);
206
+ });
207
+ let interruptStartup = () => undefined;
208
+ const interruption = new Promise((_, reject) => {
209
+ interruptStartup = (signal) => reject(new Error(`Application startup was interrupted by ${signal}.`));
210
+ });
211
+ const startupHandlers = new Map(SESSION_SIGNALS.map((signal) => [signal, () => interruptStartup(signal)]));
212
+ for (const [signal, handler] of startupHandlers) {
213
+ signalTarget.prependListener(signal, handler);
214
+ }
215
+ try {
216
+ const ready = await Promise.race([
217
+ waitForReadiness({
218
+ readiness,
219
+ fetchImplementation,
220
+ child,
221
+ }),
222
+ interruption,
223
+ ]);
224
+ return Object.freeze({
225
+ pid: child.pid,
226
+ ready,
227
+ stop: () => stopOwnedChild(child),
228
+ wait: () => waitForChildExit(child),
229
+ diagnostics: () => ({
230
+ stdoutBytes: Buffer.byteLength(stdout),
231
+ stderrBytes: Buffer.byteLength(stderr),
232
+ }),
233
+ });
234
+ }
235
+ catch (error) {
236
+ await stopOwnedChild(child);
237
+ throw error;
238
+ }
239
+ finally {
240
+ for (const [signal, handler] of startupHandlers) {
241
+ signalTarget.removeListener(signal, handler);
242
+ }
243
+ }
244
+ };
245
+ /**
246
+ * Keep a ready application session attached until the user interrupts it.
247
+ * The child process group is always stopped before this function returns.
248
+ */
249
+ export const holdApplicationSession = async ({ session, signalTarget = process, input = process.stdin, onReady = () => undefined, }) => {
250
+ if (!session ||
251
+ typeof session.wait !== "function" ||
252
+ typeof session.stop !== "function") {
253
+ throw new Error("A started application session is required.");
254
+ }
255
+ let finishInterruption = () => undefined;
256
+ const interruption = new Promise((resolve) => {
257
+ finishInterruption = resolve;
258
+ });
259
+ const handlers = new Map(SESSION_SIGNALS.map((signal) => [
260
+ signal,
261
+ () => finishInterruption({ kind: "interrupted", signal }),
262
+ ]));
263
+ for (const [signal, handler] of handlers) {
264
+ signalTarget.prependListener(signal, handler);
265
+ }
266
+ const onInput = (chunk) => {
267
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
268
+ if (bytes.includes(0x03)) {
269
+ finishInterruption({ kind: "interrupted", signal: "SIGINT" });
270
+ }
271
+ else if (bytes.includes(0x1a)) {
272
+ finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
273
+ }
274
+ };
275
+ if (input?.isTTY)
276
+ input.prependListener("data", onInput);
277
+ try {
278
+ onReady(session.ready);
279
+ const outcome = await Promise.race([
280
+ interruption,
281
+ session.wait().then((exit) => ({ kind: "exited", ...exit })),
282
+ ]);
283
+ if (outcome.kind === "exited") {
284
+ const diagnostics = session.diagnostics?.() ?? {};
285
+ throw new Error(`Application exited before the sandbox was closed (${summarizeProjectCommandFailure({
286
+ status: outcome.status,
287
+ signal: outcome.signal,
288
+ stdoutByteCount: diagnostics.stdoutBytes ?? 0,
289
+ stderrByteCount: diagnostics.stderrBytes ?? 0,
290
+ })}).`);
291
+ }
292
+ return Object.freeze({
293
+ signal: outcome.signal,
294
+ diagnostics: session.diagnostics?.() ?? {
295
+ stdoutBytes: 0,
296
+ stderrBytes: 0,
297
+ },
298
+ });
299
+ }
300
+ finally {
301
+ try {
302
+ // npm can forward the terminal's SIGINT after the CLI receives it.
303
+ // Keep our handlers installed until the owned process group is fully
304
+ // stopped so a duplicate signal cannot terminate Rehearsal mid-cleanup.
305
+ await session.stop();
306
+ }
307
+ finally {
308
+ for (const [signal, handler] of handlers) {
309
+ signalTarget.removeListener(signal, handler);
310
+ }
311
+ if (input?.isTTY)
312
+ input.removeListener("data", onInput);
313
+ }
314
+ }
315
+ };
316
+ const valueAtPath = (value, path) => {
317
+ let current = value;
318
+ for (const segment of path) {
319
+ if (current === null ||
320
+ typeof current !== "object" ||
321
+ !Object.hasOwn(current, segment)) {
322
+ return undefined;
323
+ }
324
+ current = current[segment];
325
+ }
326
+ return current;
327
+ };
328
+ export const runHttpProofs = async ({ checks, fetchImplementation = fetch, }) => {
329
+ if (!Array.isArray(checks) || checks.length < 2) {
330
+ throw new Error("HTTP proof requires explicit positive and negative checks.");
331
+ }
332
+ if (!checks.some((check) => check.kind === "positive") ||
333
+ !checks.some((check) => check.kind === "negative")) {
334
+ throw new Error("HTTP proof requires at least one positive and one negative check.");
335
+ }
336
+ const results = [];
337
+ for (const check of checks) {
338
+ if (!isLoopbackUrl(check.url))
339
+ throw new Error(`HTTP proof ${check.name} is not local.`);
340
+ const response = await fetchImplementation(check.url, {
341
+ method: check.method ?? "GET",
342
+ redirect: "manual",
343
+ signal: AbortSignal.timeout(check.timeoutMs ?? 5_000),
344
+ });
345
+ if (response.status !== check.expectedStatus) {
346
+ throw new Error(`HTTP proof ${check.name} expected status ${check.expectedStatus} but received ${response.status}.`);
347
+ }
348
+ const bytes = Buffer.from(await response.arrayBuffer());
349
+ if (bytes.length > 1024 * 1024)
350
+ throw new Error(`HTTP proof ${check.name} returned too much data.`);
351
+ if (check.json) {
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse(bytes.toString("utf8"));
355
+ }
356
+ catch {
357
+ throw new Error(`HTTP proof ${check.name} did not return JSON.`);
358
+ }
359
+ const actual = valueAtPath(parsed, check.json.path);
360
+ if (Object.hasOwn(check.json, "equals") &&
361
+ !Object.is(actual, check.json.equals)) {
362
+ throw new Error(`HTTP proof ${check.name} did not return its required value.`);
363
+ }
364
+ if (Object.hasOwn(check.json, "notEquals") &&
365
+ Object.is(actual, check.json.notEquals)) {
366
+ throw new Error(`HTTP proof ${check.name} returned a forbidden value.`);
367
+ }
368
+ if (check.json.minimumItems !== undefined &&
369
+ (!Array.isArray(actual) || actual.length < check.json.minimumItems)) {
370
+ throw new Error(`HTTP proof ${check.name} returned too few matching items.`);
371
+ }
372
+ }
373
+ results.push({
374
+ name: check.name,
375
+ kind: check.kind,
376
+ status: response.status,
377
+ });
378
+ }
379
+ return Object.freeze(results);
380
+ };