@rehearsal-db/core 0.1.0-beta.7 → 0.1.0-beta.9

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 (64) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/COMPATIBILITY.md +15 -3
  3. package/README.md +31 -13
  4. package/docs/README.md +29 -0
  5. package/docs/adapters.md +15 -6
  6. package/docs/architecture.md +58 -0
  7. package/docs/baselines.md +22 -1
  8. package/docs/commands.md +81 -14
  9. package/docs/configuration.md +144 -11
  10. package/docs/getting-started.md +12 -2
  11. package/docs/glossary.md +4 -0
  12. package/docs/production-source.md +176 -33
  13. package/docs/roadmap.md +30 -28
  14. package/docs/runtime-policies.md +191 -0
  15. package/docs/sanitization.md +24 -8
  16. package/docs/security-model.md +29 -8
  17. package/docs/standalone-workflow.md +107 -0
  18. package/docs/troubleshooting.md +8 -0
  19. package/package.json +25 -22
  20. package/scripts/runtime/manage_database.mjs +18 -0
  21. package/src/README.md +17 -0
  22. package/src/application/session.mjs +313 -0
  23. package/{scripts/lib/rehearsal/baseline_artifact.mjs → src/baseline/artifact.mjs} +110 -14
  24. package/{scripts/lib/rehearsal/baseline_builder.mjs → src/baseline/builder.mjs} +8 -4
  25. package/{scripts/lib/rehearsal/baseline_preparation.mjs → src/baseline/preparation.mjs} +11 -4
  26. package/src/baseline/privacy_engine.mjs +413 -0
  27. package/{scripts/lib/rehearsal → src/baseline}/sanitization_policy.mjs +27 -7
  28. package/{scripts/lib/rehearsal → src/baseline}/schema_snapshot.mjs +1 -1
  29. package/src/cli/arguments.mjs +120 -0
  30. package/src/cli/guided.mjs +807 -0
  31. package/src/cli/rehearsal.mjs +933 -0
  32. package/src/cli/renderers.mjs +584 -0
  33. package/src/cli/runtime_commands.mjs +553 -0
  34. package/src/cli/source_commands.mjs +326 -0
  35. package/src/cli/terminal.mjs +275 -0
  36. package/src/identity/claim.mjs +975 -0
  37. package/src/identity/storage.mjs +165 -0
  38. package/{scripts/lib/rehearsal → src/project}/configuration.d.mts +34 -0
  39. package/{scripts/lib/rehearsal → src/project}/configuration.mjs +353 -3
  40. package/{scripts/lib/rehearsal → src/project}/setup.mjs +1 -1
  41. package/{scripts/lib/rehearsal → src/project}/support_report.mjs +5 -2
  42. package/{scripts/lib/rehearsal → src/runtime}/cleanup.mjs +3 -3
  43. package/{scripts/lib/rehearsal → src/runtime}/plan.mjs +5 -5
  44. package/src/runtime/policy.mjs +438 -0
  45. package/{scripts/lib/rehearsal/runtime_restore.mjs → src/runtime/restore.mjs} +40 -25
  46. package/src/runtime/topology.mjs +177 -0
  47. package/{scripts/lib/rehearsal → src/shared}/diagnostics.mjs +1 -1
  48. package/src/shared/operation_guard.mjs +162 -0
  49. package/{scripts/lib/rehearsal → src/shared}/process_environment.mjs +5 -1
  50. package/src/source/access.mjs +578 -0
  51. package/src/source/asset_transfer.mjs +177 -0
  52. package/src/source/baseline.mjs +446 -0
  53. package/src/source/postgresql_access.mjs +480 -0
  54. package/{scripts/lib/runtime/postgresql_runtime.mjs → src/targets/postgresql.mjs} +49 -13
  55. package/{scripts/lib/runtime/supabase_runtime.mjs → src/targets/supabase.mjs} +54 -16
  56. package/{scripts/lib/environment/local_supabase.mjs → src/targets/supabase_environment.mjs} +52 -20
  57. package/{scripts/lib/runtime/runtime_target.mjs → src/targets/target.mjs} +27 -2
  58. package/scripts/operations/database/manage_rehearsal_database.mjs +0 -11
  59. package/scripts/operations/rehearsal/rehearsal_cli.mjs +0 -2166
  60. /package/{scripts/lib/rehearsal → src/baseline}/input_discovery.mjs +0 -0
  61. /package/{scripts/lib/rehearsal → src/baseline}/policy_review.mjs +0 -0
  62. /package/{scripts/lib/rehearsal → src/runtime}/migration_history.mjs +0 -0
  63. /package/{scripts/lib/rehearsal → src/runtime}/service_environment.mjs +0 -0
  64. /package/{scripts/lib/rehearsal → src/shared}/human_output.mjs +0 -0
@@ -16,11 +16,11 @@ import { pathToFileURL } from "node:url";
16
16
  import {
17
17
  resolveActiveBaselinePaths,
18
18
  verifyActiveBaseline,
19
- } from "../rehearsal/baseline_artifact.mjs";
19
+ } from "../baseline/artifact.mjs";
20
20
  import {
21
21
  createCleanProcessEnvironment,
22
22
  assertLoopbackUrl,
23
- } from "../rehearsal/process_environment.mjs";
23
+ } from "../shared/process_environment.mjs";
24
24
  import {
25
25
  localCommandSucceeds,
26
26
  readLocalSupabaseEnvironment,
@@ -28,32 +28,42 @@ import {
28
28
  runLocalCommand,
29
29
  startLocalSupabase,
30
30
  stopLocalSupabase,
31
- } from "../environment/local_supabase.mjs";
31
+ } from "./supabase_environment.mjs";
32
32
  import {
33
33
  buildRestoreSqlPrefix,
34
34
  buildRestoreSqlSuffix,
35
35
  createCandidateMigrationReceipt,
36
36
  encodeBaselineRecordForCopy,
37
37
  summarizeRestoreError,
38
- } from "../rehearsal/runtime_restore.mjs";
38
+ } from "../runtime/restore.mjs";
39
39
  import {
40
40
  buildMigrationLedgerInventory,
41
41
  compareSourceToReplay,
42
42
  createMigrationReplayReceipt,
43
43
  readMigrationFileInventory,
44
- } from "../rehearsal/migration_history.mjs";
45
- import { loadRehearsalConfig } from "../rehearsal/configuration.mjs";
46
- import { readRehearsalServiceEnvironment } from "../rehearsal/service_environment.mjs";
47
- import { readBoundRuntimeSanitizationPolicy } from "../rehearsal/sanitization_policy.mjs";
44
+ } from "../runtime/migration_history.mjs";
45
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
46
+ import { readRehearsalServiceEnvironment } from "../runtime/service_environment.mjs";
47
+ import { readBoundRuntimeSanitizationPolicy } from "../baseline/sanitization_policy.mjs";
48
48
  import {
49
49
  captureLocalPublicSchema,
50
50
  compareProductionSchemaDumps,
51
- } from "../rehearsal/schema_snapshot.mjs";
52
- import { formatCount } from "../rehearsal/human_output.mjs";
51
+ } from "../baseline/schema_snapshot.mjs";
52
+ import { formatCount } from "../shared/human_output.mjs";
53
+ import { parseRuntimeInvocation } from "./target.mjs";
54
+ import {
55
+ buildRuntimeFinalizationSql,
56
+ buildRuntimePostSchemaSql,
57
+ buildRuntimePrerequisiteSql,
58
+ buildRuntimeVerificationSql,
59
+ validateRuntimePolicy,
60
+ } from "../runtime/policy.mjs";
53
61
 
54
62
  const repositoryRoot = process.cwd();
63
+ const invocation = parseRuntimeInvocation();
55
64
  const loadedConfiguration = await loadRehearsalConfig({
56
65
  projectRoot: repositoryRoot,
66
+ configPath: invocation.configPath,
57
67
  });
58
68
  const { config, paths: configuredPaths } = loadedConfiguration;
59
69
  const artifactRoot = configuredPaths.artifactDirectory;
@@ -75,8 +85,13 @@ const projectId = config.runtime.projectId;
75
85
  const runtimeAdapter = configuredPaths.runtimeAdapter
76
86
  ? await import(pathToFileURL(configuredPaths.runtimeAdapter).href)
77
87
  : null;
88
+ const runtimePolicy = configuredPaths.runtimePolicy
89
+ ? validateRuntimePolicy(
90
+ JSON.parse(await readFile(configuredPaths.runtimePolicy, "utf8")),
91
+ )
92
+ : null;
78
93
  const environmentKeyPattern = /^[A-Z][A-Z0-9_]*$/u;
79
- const action = process.argv[2] ?? "status";
94
+ const action = invocation.action;
80
95
 
81
96
  const readServiceEnvironment = () =>
82
97
  readRehearsalServiceEnvironment({
@@ -331,6 +346,12 @@ const writeRuntimeEnvironment = async ({
331
346
  const variables = {
332
347
  REHEARSAL_RUNTIME_ENVIRONMENT: "rehearsal",
333
348
  REHEARSAL_BASELINE_ID: baseline.generationId,
349
+ DATABASE_URL: environment.databaseUrl,
350
+ PGHOST: environment.databaseHost,
351
+ PGPORT: environment.databasePort,
352
+ PGDATABASE: environment.databaseName,
353
+ PGUSER: environment.databaseUser,
354
+ PGPASSWORD: environment.databasePassword,
334
355
  SUPABASE_URL: environment.apiUrl,
335
356
  SUPABASE_PUBLISHABLE_KEY: environment.publishableKey,
336
357
  SUPABASE_SERVICE_ROLE_KEY: environment.serviceRoleKey,
@@ -372,7 +393,13 @@ const runDatabaseSql = (sql) => {
372
393
 
373
394
  const restoreProductionSchema = async ({ baseline, paths }) => {
374
395
  const receipt = baseline.files?.["production-schema.sql"];
375
- if (baseline.formatVersion === 1 && receipt === undefined) return null;
396
+ if (baseline.formatVersion === 1 && receipt === undefined) {
397
+ if (runtimePolicy) {
398
+ runDatabaseSql(buildRuntimePrerequisiteSql(runtimePolicy));
399
+ runDatabaseSql(buildRuntimePostSchemaSql(runtimePolicy));
400
+ }
401
+ return null;
402
+ }
376
403
  if (
377
404
  baseline.formatVersion !== 2 ||
378
405
  !/^[a-f0-9]{64}$/u.test(receipt?.sha256 ?? "") ||
@@ -383,7 +410,12 @@ const restoreProductionSchema = async ({ baseline, paths }) => {
383
410
  );
384
411
  }
385
412
  const schemaSql = await readFile(paths.schemaPath, "utf8");
386
- runDatabaseSql("drop schema if exists public cascade;");
413
+ runDatabaseSql(`drop schema if exists public cascade;
414
+ create schema public authorization pg_database_owner;
415
+ grant usage on schema public to public;`);
416
+ if (runtimePolicy) {
417
+ runDatabaseSql(buildRuntimePrerequisiteSql(runtimePolicy));
418
+ }
387
419
  if (runtimeAdapter?.prepareSchema) {
388
420
  const preparation = runtimeAdapter.prepareSchema({
389
421
  runSql: runDatabaseSql,
@@ -395,6 +427,9 @@ const restoreProductionSchema = async ({ baseline, paths }) => {
395
427
  }
396
428
  }
397
429
  runDatabaseSql(schemaSql);
430
+ if (runtimePolicy) {
431
+ runDatabaseSql(buildRuntimePostSchemaSql(runtimePolicy));
432
+ }
398
433
  const restoredSchema = await captureLocalPublicSchema({
399
434
  repositoryRoot,
400
435
  artifactRoot,
@@ -477,6 +512,10 @@ const resetRuntime = async () => {
477
512
  paths: active.paths,
478
513
  environment,
479
514
  });
515
+ if (runtimePolicy && runtimePolicy.localRows.length > 0) {
516
+ runDatabaseSql(buildRuntimeFinalizationSql(runtimePolicy));
517
+ console.log("Applied reviewed local-only recipes.");
518
+ }
480
519
  const projectRuntime = configureProjectRuntime({
481
520
  environment,
482
521
  baseline: active.baseline,
@@ -613,6 +652,7 @@ const verifyRuntime = async () => {
613
652
  currentFiles: candidateReceipt.currentFiles,
614
653
  });
615
654
  let projectVerification;
655
+ if (runtimePolicy) runDatabaseSql(buildRuntimeVerificationSql(runtimePolicy));
616
656
  if (runtimeAdapter?.verifyRuntime) {
617
657
  projectVerification = runtimeAdapter.verifyRuntime({
618
658
  baseline,
@@ -662,9 +702,7 @@ const migrateRuntime = async () => {
662
702
  console.log(
663
703
  `${receipt.candidates.length === 1 ? "Candidate migration" : "Candidate migrations"} (${receipt.candidateSha256}): ${receipt.candidates.map((candidate) => candidate.filename).join(", ")}`,
664
704
  );
665
- const confirmation = process.argv
666
- .find((argument) => argument.startsWith("--confirm-candidates="))
667
- ?.slice("--confirm-candidates=".length);
705
+ const confirmation = invocation.confirmation;
668
706
  if (confirmation !== receipt.candidateSha256) {
669
707
  throw new Error(
670
708
  "Candidate migration confirmation is missing or does not match the exact SHA-256.",
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { spawnSync } from "node:child_process";
7
- import { createCleanProcessEnvironment } from "../rehearsal/process_environment.mjs";
7
+ import { createCleanProcessEnvironment } from "../shared/process_environment.mjs";
8
8
 
9
9
  export const parseSupabaseStatusEnvironment = (output) => {
10
10
  const values = {};
@@ -18,6 +18,56 @@ export const parseSupabaseStatusEnvironment = (output) => {
18
18
  return values;
19
19
  };
20
20
 
21
+ export const parseLocalSupabaseEnvironment = (output) => {
22
+ const values = parseSupabaseStatusEnvironment(output);
23
+ const localEnvironment = {
24
+ apiUrl: values.API_URL,
25
+ databaseUrl: values.DB_URL,
26
+ publishableKey: values.PUBLISHABLE_KEY ?? values.ANON_KEY,
27
+ serviceRoleKey: values.SERVICE_ROLE_KEY ?? values.SECRET_KEY,
28
+ studioUrl: values.STUDIO_URL,
29
+ };
30
+ for (const [label, value] of [
31
+ ["API URL", localEnvironment.apiUrl],
32
+ ["database URL", localEnvironment.databaseUrl],
33
+ ["publishable key", localEnvironment.publishableKey],
34
+ ["service-role key", localEnvironment.serviceRoleKey],
35
+ ]) {
36
+ if (!value) throw new Error(`Local Supabase status omitted its ${label}.`);
37
+ }
38
+ const api = new URL(localEnvironment.apiUrl);
39
+ const database = new URL(localEnvironment.databaseUrl);
40
+ const loopbackHosts = ["127.0.0.1", "::1", "localhost"];
41
+ if (
42
+ !loopbackHosts.includes(api.hostname) ||
43
+ !loopbackHosts.includes(database.hostname) ||
44
+ !["postgres:", "postgresql:"].includes(database.protocol)
45
+ ) {
46
+ throw new Error("Refusing to use a non-local Supabase status target.");
47
+ }
48
+ const databaseName = decodeURIComponent(
49
+ database.pathname.replace(/^\//u, ""),
50
+ );
51
+ if (
52
+ !database.port ||
53
+ !databaseName ||
54
+ !database.username ||
55
+ !database.password
56
+ ) {
57
+ throw new Error(
58
+ "Local Supabase status returned an incomplete database URL.",
59
+ );
60
+ }
61
+ return {
62
+ ...localEnvironment,
63
+ databaseHost: database.hostname,
64
+ databasePort: database.port,
65
+ databaseName,
66
+ databaseUser: decodeURIComponent(database.username),
67
+ databasePassword: decodeURIComponent(database.password),
68
+ };
69
+ };
70
+
21
71
  export const runLocalCommand = (
22
72
  command,
23
73
  args,
@@ -90,25 +140,7 @@ export const readLocalSupabaseEnvironment = ({
90
140
  withWorkdir(["status", "-o", "env"], workdir),
91
141
  { capture: true, cwd, environment },
92
142
  );
93
- const values = parseSupabaseStatusEnvironment(output);
94
- const localEnvironment = {
95
- apiUrl: values.API_URL,
96
- publishableKey: values.PUBLISHABLE_KEY ?? values.ANON_KEY,
97
- serviceRoleKey: values.SERVICE_ROLE_KEY ?? values.SECRET_KEY,
98
- studioUrl: values.STUDIO_URL,
99
- };
100
- for (const [label, value] of [
101
- ["API URL", localEnvironment.apiUrl],
102
- ["publishable key", localEnvironment.publishableKey],
103
- ["service-role key", localEnvironment.serviceRoleKey],
104
- ]) {
105
- if (!value) throw new Error(`Local Supabase status omitted its ${label}.`);
106
- }
107
- const hostname = new URL(localEnvironment.apiUrl).hostname;
108
- if (!["127.0.0.1", "::1", "localhost"].includes(hostname)) {
109
- throw new Error("Refusing to use a non-loopback Supabase status target.");
110
- }
111
- return localEnvironment;
143
+ return parseLocalSupabaseEnvironment(output);
112
144
  };
113
145
 
114
146
  export const startLocalSupabase = ({
@@ -17,6 +17,31 @@ const TARGETS = Object.freeze({
17
17
 
18
18
  export const REHEARSAL_RUNTIME_TARGETS = Object.freeze(Object.keys(TARGETS));
19
19
 
20
+ export const parseRuntimeInvocation = (arguments_ = process.argv.slice(2)) => {
21
+ const action = arguments_.find((argument) => !argument.startsWith("--"));
22
+ const valueAfter = (prefix) =>
23
+ arguments_
24
+ .find((argument) => argument.startsWith(prefix))
25
+ ?.slice(prefix.length);
26
+ return Object.freeze({
27
+ action: action ?? "status",
28
+ configPath: valueAfter("--config="),
29
+ confirmation: valueAfter("--confirm-candidates="),
30
+ });
31
+ };
32
+
33
+ export const buildRuntimeManagerArguments = ({
34
+ managerPath,
35
+ action,
36
+ configPath,
37
+ confirmation,
38
+ }) => [
39
+ managerPath,
40
+ action,
41
+ ...(configPath ? [`--config=${configPath}`] : []),
42
+ ...(confirmation ? [`--confirm-candidates=${confirmation}`] : []),
43
+ ];
44
+
20
45
  export const resolveRuntimeTarget = (value = "supabase") => {
21
46
  if (typeof value !== "string" || !Object.hasOwn(TARGETS, value)) {
22
47
  throw new Error(
@@ -30,10 +55,10 @@ export const runRuntimeTarget = async (target) => {
30
55
  const resolved = resolveRuntimeTarget(target);
31
56
  switch (resolved.id) {
32
57
  case "supabase":
33
- await import("./supabase_runtime.mjs");
58
+ await import("./supabase.mjs");
34
59
  return;
35
60
  case "postgresql":
36
- await import("./postgresql_runtime.mjs");
61
+ await import("./postgresql.mjs");
37
62
  return;
38
63
  default:
39
64
  throw new Error(`No runtime driver is installed for ${resolved.id}.`);
@@ -1,11 +0,0 @@
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
-
7
- import { loadRehearsalConfig } from "../../lib/rehearsal/configuration.mjs";
8
- import { runRuntimeTarget } from "../../lib/runtime/runtime_target.mjs";
9
-
10
- const { config } = await loadRehearsalConfig({ projectRoot: process.cwd() });
11
- await runRuntimeTarget(config.runtime.target);