@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.
- package/CHANGELOG.md +77 -1
- package/COMPATIBILITY.md +15 -3
- package/README.md +31 -13
- package/docs/README.md +29 -0
- package/docs/adapters.md +15 -6
- package/docs/architecture.md +58 -0
- package/docs/baselines.md +22 -1
- package/docs/commands.md +81 -14
- package/docs/configuration.md +144 -11
- package/docs/getting-started.md +12 -2
- package/docs/glossary.md +4 -0
- package/docs/production-source.md +176 -33
- package/docs/roadmap.md +30 -28
- package/docs/runtime-policies.md +191 -0
- package/docs/sanitization.md +24 -8
- package/docs/security-model.md +29 -8
- package/docs/standalone-workflow.md +107 -0
- package/docs/troubleshooting.md +8 -0
- package/package.json +25 -22
- package/scripts/runtime/manage_database.mjs +18 -0
- package/src/README.md +17 -0
- package/src/application/session.mjs +313 -0
- package/{scripts/lib/rehearsal/baseline_artifact.mjs → src/baseline/artifact.mjs} +110 -14
- package/{scripts/lib/rehearsal/baseline_builder.mjs → src/baseline/builder.mjs} +8 -4
- package/{scripts/lib/rehearsal/baseline_preparation.mjs → src/baseline/preparation.mjs} +11 -4
- package/src/baseline/privacy_engine.mjs +413 -0
- package/{scripts/lib/rehearsal → src/baseline}/sanitization_policy.mjs +27 -7
- package/{scripts/lib/rehearsal → src/baseline}/schema_snapshot.mjs +1 -1
- package/src/cli/arguments.mjs +120 -0
- package/src/cli/guided.mjs +807 -0
- package/src/cli/rehearsal.mjs +933 -0
- package/src/cli/renderers.mjs +584 -0
- package/src/cli/runtime_commands.mjs +553 -0
- package/src/cli/source_commands.mjs +326 -0
- package/src/cli/terminal.mjs +275 -0
- package/src/identity/claim.mjs +975 -0
- package/src/identity/storage.mjs +165 -0
- package/{scripts/lib/rehearsal → src/project}/configuration.d.mts +34 -0
- package/{scripts/lib/rehearsal → src/project}/configuration.mjs +353 -3
- package/{scripts/lib/rehearsal → src/project}/setup.mjs +1 -1
- package/{scripts/lib/rehearsal → src/project}/support_report.mjs +5 -2
- package/{scripts/lib/rehearsal → src/runtime}/cleanup.mjs +3 -3
- package/{scripts/lib/rehearsal → src/runtime}/plan.mjs +5 -5
- package/src/runtime/policy.mjs +438 -0
- package/{scripts/lib/rehearsal/runtime_restore.mjs → src/runtime/restore.mjs} +40 -25
- package/src/runtime/topology.mjs +177 -0
- package/{scripts/lib/rehearsal → src/shared}/diagnostics.mjs +1 -1
- package/src/shared/operation_guard.mjs +162 -0
- package/{scripts/lib/rehearsal → src/shared}/process_environment.mjs +5 -1
- package/src/source/access.mjs +578 -0
- package/src/source/asset_transfer.mjs +177 -0
- package/src/source/baseline.mjs +446 -0
- package/src/source/postgresql_access.mjs +480 -0
- package/{scripts/lib/runtime/postgresql_runtime.mjs → src/targets/postgresql.mjs} +49 -13
- package/{scripts/lib/runtime/supabase_runtime.mjs → src/targets/supabase.mjs} +54 -16
- package/{scripts/lib/environment/local_supabase.mjs → src/targets/supabase_environment.mjs} +52 -20
- package/{scripts/lib/runtime/runtime_target.mjs → src/targets/target.mjs} +27 -2
- package/scripts/operations/database/manage_rehearsal_database.mjs +0 -11
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +0 -2166
- /package/{scripts/lib/rehearsal → src/baseline}/input_discovery.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/baseline}/policy_review.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/runtime}/migration_history.mjs +0 -0
- /package/{scripts/lib/rehearsal → src/runtime}/service_environment.mjs +0 -0
- /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 "../
|
|
19
|
+
} from "../baseline/artifact.mjs";
|
|
20
20
|
import {
|
|
21
21
|
createCleanProcessEnvironment,
|
|
22
22
|
assertLoopbackUrl,
|
|
23
|
-
} from "../
|
|
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 "
|
|
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 "../
|
|
38
|
+
} from "../runtime/restore.mjs";
|
|
39
39
|
import {
|
|
40
40
|
buildMigrationLedgerInventory,
|
|
41
41
|
compareSourceToReplay,
|
|
42
42
|
createMigrationReplayReceipt,
|
|
43
43
|
readMigrationFileInventory,
|
|
44
|
-
} from "../
|
|
45
|
-
import { loadRehearsalConfig } from "../
|
|
46
|
-
import { readRehearsalServiceEnvironment } from "../
|
|
47
|
-
import { readBoundRuntimeSanitizationPolicy } from "../
|
|
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 "../
|
|
52
|
-
import { formatCount } from "../
|
|
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 =
|
|
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)
|
|
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(
|
|
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 =
|
|
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 "../
|
|
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
|
-
|
|
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("./
|
|
58
|
+
await import("./supabase.mjs");
|
|
34
59
|
return;
|
|
35
60
|
case "postgresql":
|
|
36
|
-
await import("./
|
|
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);
|