@rehearsal-db/core 0.1.0-beta.11 → 0.1.0-beta.13

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 (38) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/COMPATIBILITY.md +6 -0
  3. package/README.md +11 -2
  4. package/docs/commands.md +18 -13
  5. package/docs/configuration.md +78 -12
  6. package/docs/getting-started.md +14 -4
  7. package/docs/production-source.md +138 -16
  8. package/docs/runtime-policies.md +4 -2
  9. package/docs/sanitization.md +73 -7
  10. package/docs/security-model.md +8 -0
  11. package/docs/standalone-workflow.md +24 -9
  12. package/docs/troubleshooting.md +26 -8
  13. package/docs/tutorial.md +2 -1
  14. package/package.json +4 -3
  15. package/src/baseline/artifact.mjs +27 -4
  16. package/src/baseline/privacy_engine.mjs +220 -9
  17. package/src/cli/arguments.mjs +3 -0
  18. package/src/cli/guided.mjs +8 -6
  19. package/src/cli/init_command.mjs +17 -0
  20. package/src/cli/rehearsal.mjs +20 -2
  21. package/src/cli/renderers.mjs +41 -4
  22. package/src/cli/runtime_commands.mjs +92 -9
  23. package/src/cli/source_commands.mjs +26 -5
  24. package/src/identity/provider_configuration.mjs +177 -0
  25. package/src/project/configuration.d.mts +11 -0
  26. package/src/project/configuration.mjs +155 -19
  27. package/src/project/setup.mjs +5 -23
  28. package/src/runtime/plan.mjs +40 -3
  29. package/src/runtime/restore.mjs +13 -5
  30. package/src/shared/diagnostics.mjs +7 -0
  31. package/src/shared/node_runtime.mjs +31 -0
  32. package/src/source/access.mjs +224 -49
  33. package/src/source/asset_transfer.mjs +35 -5
  34. package/src/source/baseline.mjs +42 -6
  35. package/src/source/external_reader.mjs +255 -0
  36. package/src/source/postgresql_access.mjs +143 -42
  37. package/src/source/privacy_paths.mjs +38 -0
  38. package/src/targets/supabase.mjs +11 -1
@@ -40,10 +40,10 @@ Two policy versions exist:
40
40
  Rehearsal can sanitize without project callbacks.
41
41
 
42
42
  Version 2 supports keyed UUID, email, text, and integer pseudonyms; explicit constant
43
- replacement; bounded date shifting; approved-owner conditionals; exhaustively classified
44
- JSON objects; and bounded JSON arrays. Unknown tables, columns, nested keys, formats,
45
- recipes, or missing required fields fail closed. It never evaluates JavaScript or SQL
46
- from the policy.
43
+ replacement; bounded and grouped date shifting; identity-aware path mapping;
44
+ approved-owner conditionals; exhaustively classified JSON objects; and bounded JSON
45
+ arrays. Unknown tables, columns, nested keys, formats, recipes, or missing required
46
+ fields fail closed. It never evaluates JavaScript or SQL from the policy.
47
47
 
48
48
  ## Approved-owner values
49
49
 
@@ -69,7 +69,7 @@ environment-variable name and SHA-256 receipt:
69
69
  "action": "PSEUDONYMIZE",
70
70
  "recipe": { "format": "uuid", "namespace": "account-id" },
71
71
  "generated": "NEVER",
72
- "identity": "YES",
72
+ "identity": "NO",
73
73
  "foreignKey": null
74
74
  },
75
75
  {
@@ -101,12 +101,78 @@ node -e 'const {createHash}=require("node:crypto"); process.stdout.write(createH
101
101
 
102
102
  Every conditional table names its owner column. Missing bindings, hash mismatches,
103
103
  missing owner columns, null/unsupported owner values, and conditionals without an owner
104
- binding are refused. The owner column must be marked `identity: "YES"` and use
105
- `PSEUDONYMIZE`, preventing a conditional policy from retaining raw account IDs. Other
104
+ binding are refused. The owner column must use `PSEUDONYMIZE`, preventing a conditional
105
+ policy from retaining raw account IDs. `identity` describes PostgreSQL's generated SQL
106
+ identity metadata, not whether a value identifies a person: ordinary UUID primary keys
107
+ use `"NO"`, while a genuine `GENERATED ... AS IDENTITY` column uses `"YES"`. Rehearsal
108
+ checks the restored PostgreSQL schema for a real backing sequence before attempting a
109
+ sequence reset, so UUID identity values never receive numeric sequence operations. Other
106
110
  owners take the declared `otherwise` path; every other identity-bearing field still
107
111
  needs its own pseudonymization declaration. Supply the binding only to the local
108
112
  server-side Rehearsal process; do not put it in the policy, baseline, or tracked files.
109
113
 
114
+ ## Identity-aware Storage paths
115
+
116
+ Use one named mapping when a database column and a physical Storage object both begin
117
+ with the approved owner's ID. Rehearsal pseudonymizes only that first segment and keeps
118
+ the remaining path unchanged:
119
+
120
+ ```json
121
+ {
122
+ "pathMappings": {
123
+ "owner-storage": {
124
+ "binding": "approved-owner",
125
+ "format": "uuid",
126
+ "namespace": "account-id",
127
+ "maximumBytes": 512,
128
+ "maximumSegments": 8
129
+ }
130
+ },
131
+ "tables": [
132
+ {
133
+ "name": "profiles",
134
+ "sourceRows": "STREAM AND SANITIZE",
135
+ "columns": [
136
+ {
137
+ "name": "avatar_path",
138
+ "action": "DERIVE",
139
+ "recipe": { "kind": "path-map", "mapping": "owner-storage" },
140
+ "generated": "NEVER",
141
+ "identity": "NO",
142
+ "foreignKey": null
143
+ }
144
+ ]
145
+ }
146
+ ]
147
+ }
148
+ ```
149
+
150
+ The source policy uses the same mapping name and names the binding's environment
151
+ variable as `prefixEnvironmentVariable`; it never contains the raw owner ID. During
152
+ `rehearsal refresh`, the package uses the mapping for both database values and physical
153
+ object destinations. A missing mapping, a different environment variable, a path whose
154
+ first segment is not the exact reviewed owner, traversal, or a size/segment limit breach
155
+ stops the refresh.
156
+
157
+ ## Related dates
158
+
159
+ Give related timestamps the same `group` and `days` value to shift them by one stable
160
+ offset. This preserves their order and duration:
161
+
162
+ ```json
163
+ {
164
+ "name": "starts_at",
165
+ "action": "DERIVE",
166
+ "recipe": { "kind": "date-shift", "days": 30, "group": "event-window" },
167
+ "generated": "NEVER",
168
+ "identity": "NO",
169
+ "foreignKey": null
170
+ }
171
+ ```
172
+
173
+ Apply the same recipe to `ends_at`. Rehearsal refuses a group that declares conflicting
174
+ `days` values. Leave out `group` when a date should shift independently.
175
+
110
176
  ## Structured JSON
111
177
 
112
178
  `json-object` fields are required by default. Set `"required": false` on an explicitly
@@ -26,6 +26,9 @@ runtime.
26
26
  - successful receipts written only after verification.
27
27
  - separate exact-digest approval for source setup, source retirement, and identity claim;
28
28
  - short-lived source readers limited to reviewed views/columns and deny checks;
29
+ - externally provisioned readers require canonical view-definition receipts,
30
+ `security_barrier`, independent ownership, an exact non-system readable-column surface,
31
+ no write or role-escalation paths, and database-enforced expiration;
29
32
  - bounded read-only extraction with no raw dump or row-value logging;
30
33
  - exhaustive privacy policy version 2 with an owner-only local pseudonym key;
31
34
  - application children receive only explicit local mappings and stay in an owned process
@@ -39,6 +42,11 @@ No single environment variable or config edit can redirect runtime commands to
39
42
  production. Optional source preparation is a separate boundary and never provides a
40
43
  hosted execution mode.
41
44
 
45
+ External source mode verifies access but does not own it. Rehearsal makes no source-side
46
+ changes and removes only its local credential and receipt. The provider owns early
47
+ revocation and the database enforces the reviewed short expiration; Rehearsal never
48
+ reports local cleanup as proof that a provider-owned role was deleted.
49
+
42
50
  ## Identity providers
43
51
 
44
52
  An optional external identity handshake is distinct from database access. Only declared
@@ -8,16 +8,18 @@ The optional preparation workflow is a separate security boundary. It has
8
8
  three deliberately separate phases:
9
9
 
10
10
  1. `source plan` reads declarations and produces a redacted, exact plan.
11
- 2. `source apply` requires the plan digest and source-owner credentials from a
12
- named environment variable. It creates only the declared, time-limited
13
- reader and export surface.
11
+ 2. `source apply` requires the plan digest. Managed mode uses source-owner credentials
12
+ from a named environment variable and creates only the declared, time-limited reader
13
+ and export surface. External mode verifies an already restricted reader without an
14
+ administrator credential or source-side writes.
14
15
  3. `refresh` uses that reader in a read-only repeatable-read transaction, builds
15
16
  privately, activates only after verification, resets the local runtime, and applies
16
17
  reviewed retention. `baseline refresh` is the lower-level baseline-only form.
17
18
 
18
- `source retire` has its own preview and digest. It removes only the exact
19
- reader and export objects Rehearsal recorded. It never searches for similarly
20
- named roles or removes unrelated grants.
19
+ `source retire` has its own preview and digest. Managed mode removes only the exact
20
+ reader and export objects Rehearsal recorded. External mode removes only local Rehearsal
21
+ credentials and receipts, preserving provider-owned objects. It never searches for
22
+ similarly named roles or removes unrelated grants.
21
23
 
22
24
  ## What belongs in declarations
23
25
 
@@ -46,9 +48,12 @@ separate. A declaration contains only the name of an environment variable or
46
48
  owner-only secret file. Rehearsal never puts credential values in command-line
47
49
  arguments, reports, baselines, receipts, or a launched application.
48
50
 
49
- The current workflow creates and verifies its own temporary PostgreSQL reader. An
50
- externally provisioned reader is not yet accepted because Rehearsal cannot currently
51
- prove that reader's complete scope and retirement behavior.
51
+ The default managed workflow creates and verifies its own temporary PostgreSQL reader.
52
+ External mode accepts a provider-managed reader only when its complete non-system
53
+ readable-column surface exactly matches the reviewed export views and migration ledger.
54
+ It also verifies canonical view-definition fingerprints, `security_barrier`, ownership,
55
+ write denial, role isolation, security-definer denial, and a short database-enforced
56
+ expiration. Rehearsal never claims ownership or revocation of provider-managed objects.
52
57
 
53
58
  ## Snapshot and privacy guarantees
54
59
 
@@ -57,6 +62,16 @@ read-only repeatable-read PostgreSQL transaction. Rehearsal rechecks the source
57
62
  identity and migration evidence before commit. Rows stream in bounded batches;
58
63
  record bodies never appear in progress or error output.
59
64
 
65
+ The snapshot boundary is deliberate: row commits made after extraction starts are left
66
+ for the next baseline, while concurrent schema or migration-ledger changes block the
67
+ current activation. Invalid estimates, malformed batches, connection loss, interruption,
68
+ and disk or configured-limit failures remove private staging and preserve the previously
69
+ active baseline.
70
+
71
+ Cleanup recursively makes only Rehearsal's private staging tree removable. A failed
72
+ Storage transfer therefore reports the original safe transfer error, removes incomplete
73
+ files, and does not alter the active generation.
74
+
60
75
  Storage objects do not share the database transaction guarantee. Their inventory records
61
76
  size and version metadata; transfer requires that version, verifies the exact byte count,
62
77
  and records a SHA-256 in the immutable baseline. Transfer refuses an object that changes,
@@ -34,14 +34,15 @@ Choose **Set the stage** and approve the preview. For pnpm and Yarn, use this gu
34
34
  automatic root generation is currently tested with npm. An existing supported config is
35
35
  kept in place, including `infrastructure/rehearsal/rehearsal.config.mjs`.
36
36
 
37
- ## Setup says Node.js 24 is required
37
+ ## Setup says this Node.js version is unsupported
38
38
 
39
- Rehearsal intentionally supports one maintained Node.js major in its first beta. Switch
40
- the current shell before installing or running it. With nvm:
39
+ Rehearsal supports the maintained Node.js 22, 24, and 26 release lines. Node.js 20 and
40
+ odd-numbered releases are end-of-life. Switch the current shell before installing or
41
+ running Rehearsal. The latest LTS is the recommended default:
41
42
 
42
43
  ```bash
43
- nvm install 24
44
- nvm use 24
44
+ nvm install --lts
45
+ nvm use --lts
45
46
  node --version
46
47
  ```
47
48
 
@@ -53,6 +54,12 @@ writes setup files.
53
54
  Start Docker Desktop or Colima, confirm `docker info`, then rerun `rehearsal doctor`.
54
55
  Restarting the computer is rarely necessary.
55
56
 
57
+ When a container stays in `starting` or becomes `unhealthy`, Rehearsal reports a runtime
58
+ dependency failure rather than a migration failure. The safe diagnostic includes Docker
59
+ CPU, memory, and container counts; Colima also includes available VM memory when it can
60
+ be read. Stop unrelated local stacks or increase the engine's resources before retrying.
61
+ Rehearsal never changes engine settings or bypasses service health checks.
62
+
56
63
  ## Docker or Colima is out of disk space
57
64
 
58
65
  Preview what Rehearsal can safely remove:
@@ -146,9 +153,20 @@ changes.
146
153
 
147
154
  ## Local authentication fails
148
155
 
149
- Confirm the provider is enabled in the dedicated local Supabase config, the allowlisted
150
- service environment file exists with owner-only permissions, and the provider callback
151
- is the local Auth callback. Do not paste provider secrets into config or terminal output.
156
+ For Google or GitHub, confirm `supabase.authentication` is declared in
157
+ `rehearsal.config.mjs`, its ignored environment file exists with `chmod 600`, and the
158
+ provider has the printed local Auth callback registered. Rehearsal generates the
159
+ disposable Supabase provider block.
160
+
161
+ When migrating from the legacy service environment fields, replace those fields together
162
+ with `supabase.authentication` and rerun `rehearsal doctor`. A compatible Google or
163
+ GitHub provider section may remain in the tracked template: Rehearsal supersedes it only
164
+ inside the generated runtime copy. Doctor refuses unknown provider settings, duplicate
165
+ or nested provider sections, and nonce or optional-email settings that do not match the
166
+ new declaration. Keep both options false unless there is a reviewed provider-specific
167
+ need; skipping nonce validation reduces replay protection. Otherwise keep using the
168
+ documented legacy configuration.
169
+ Never paste provider secrets into tracked config or terminal output.
152
170
 
153
171
  If `identity plan` refuses the matcher, read the message literally:
154
172
 
package/docs/tutorial.md CHANGED
@@ -3,7 +3,8 @@
3
3
  This tutorial runs a complete PostgreSQL rehearsal in a temporary fictional project. It
4
4
  does not use your application, production data, or a hosted database.
5
5
 
6
- Allow about five minutes. You need Node.js 24 and a running Docker-compatible engine.
6
+ Allow about five minutes. You need Node.js 22, 24, or 26 and a running
7
+ Docker-compatible engine. Node.js 24 LTS is recommended.
7
8
 
8
9
  ## 1. Prepare Rehearsal
9
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-beta.11",
3
+ "version": "0.1.0-beta.13",
4
4
  "private": false,
5
5
  "description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
6
6
  "repository": {
@@ -10,7 +10,7 @@
10
10
  "bugs": {
11
11
  "url": "https://github.com/Ddupasquier/rehearsal-db/issues"
12
12
  },
13
- "homepage": "https://github.com/Ddupasquier/rehearsal-db#readme",
13
+ "homepage": "https://ddupasquier.github.io/rehearsal-db/",
14
14
  "publishConfig": {
15
15
  "access": "public",
16
16
  "tag": "beta",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "type": "module",
20
20
  "engines": {
21
- "node": ">=24 <25"
21
+ "node": "^22.0.0 || ^24.0.0 || ^26.0.0"
22
22
  },
23
23
  "bin": {
24
24
  "rehearsal": "src/cli/rehearsal.mjs"
@@ -62,6 +62,7 @@
62
62
  "test:fixture": "node scripts/verification/fixtures/supabase.mjs",
63
63
  "test:fixture:postgresql": "node scripts/verification/fixtures/postgresql.mjs",
64
64
  "test:fixture:dependent": "node scripts/verification/fixtures/dependent.mjs",
65
+ "test:fixture:onboarding": "node scripts/verification/fixtures/onboarding.mjs",
65
66
  "test:fixture:standalone": "node scripts/verification/fixtures/standalone.mjs",
66
67
  "test:fixture:privacy": "node scripts/verification/fixtures/privacy.mjs",
67
68
  "check": "npm run check:syntax && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
@@ -372,12 +372,35 @@ export const createAndActivateBaseline = async ({
372
372
  }
373
373
  return manifest;
374
374
  } catch (error) {
375
+ let cleanupFailure;
375
376
  if (dataHandle) await dataHandle.close().catch(() => undefined);
376
- await chmod(buildDirectory, 0o700).catch(() => undefined);
377
- await rm(buildDirectory, { recursive: true, force: true });
377
+ await makeArtifactTreeWritable(buildDirectory).catch(() => undefined);
378
+ await rm(buildDirectory, { recursive: true, force: true }).catch(
379
+ (cleanupError) => {
380
+ cleanupFailure = cleanupError;
381
+ },
382
+ );
378
383
  if (finalCreated) {
379
- await chmod(finalDirectory, 0o700).catch(() => undefined);
380
- await rm(finalDirectory, { recursive: true, force: true });
384
+ await makeArtifactTreeWritable(finalDirectory).catch(() => undefined);
385
+ await rm(finalDirectory, { recursive: true, force: true }).catch(
386
+ (cleanupError) => {
387
+ cleanupFailure ??= cleanupError;
388
+ },
389
+ );
390
+ }
391
+ if (
392
+ cleanupFailure &&
393
+ error &&
394
+ (typeof error === "object" || typeof error === "function")
395
+ ) {
396
+ try {
397
+ Object.defineProperty(error, "cleanupError", {
398
+ value: cleanupFailure,
399
+ enumerable: false,
400
+ });
401
+ } catch {
402
+ // Preserve the preparation failure even when its Error is immutable.
403
+ }
381
404
  }
382
405
  throw error;
383
406
  }