@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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Resolve and validate a primary Rehearsal runtime plus any explicitly
|
|
3
|
+
* declared dependent runtimes. Every target remains a complete, independently
|
|
4
|
+
* safe Rehearsal configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
9
|
+
import { loadRehearsalConfig } from "../project/configuration.mjs";
|
|
10
|
+
import { inspectRehearsalMigrations } from "./plan.mjs";
|
|
11
|
+
|
|
12
|
+
const isInside = (parent, candidate) => {
|
|
13
|
+
const path = relative(parent, candidate);
|
|
14
|
+
return !path || (!path.startsWith("..") && !isAbsolute(path));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const assertUnique = (entries, valueFor, label) => {
|
|
18
|
+
const seen = new Map();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const value = valueFor(entry);
|
|
21
|
+
if (seen.has(value)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Dependent runtime ${entry.name} conflicts with ${seen.get(value)}: ${label} ${value} must be unique.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
seen.set(value, entry.name);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const targetFrom = ({ name, declaration, loaded, primary }) =>
|
|
31
|
+
Object.freeze({
|
|
32
|
+
name,
|
|
33
|
+
primary,
|
|
34
|
+
declaration,
|
|
35
|
+
configPath: loaded.configPath,
|
|
36
|
+
config: loaded.config,
|
|
37
|
+
paths: loaded.paths,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const loadRuntimeTopology = async (options = {}) => {
|
|
41
|
+
const primaryLoaded = await loadRehearsalConfig(options);
|
|
42
|
+
const primary = targetFrom({
|
|
43
|
+
name: "primary",
|
|
44
|
+
declaration: null,
|
|
45
|
+
loaded: primaryLoaded,
|
|
46
|
+
primary: true,
|
|
47
|
+
});
|
|
48
|
+
const dependents = [];
|
|
49
|
+
for (const declaration of primary.config.dependentTargets) {
|
|
50
|
+
const loaded = await loadRehearsalConfig({
|
|
51
|
+
projectRoot: primaryLoaded.projectRoot,
|
|
52
|
+
configPath: declaration.configPath,
|
|
53
|
+
});
|
|
54
|
+
if (loaded.config.dependentTargets.length > 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Dependent runtime ${declaration.name} cannot declare its own dependentTargets. Nesting is intentionally unsupported.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
dependents.push(
|
|
60
|
+
targetFrom({
|
|
61
|
+
name: declaration.name,
|
|
62
|
+
declaration,
|
|
63
|
+
loaded,
|
|
64
|
+
primary: false,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const targets = [primary, ...dependents];
|
|
69
|
+
assertUnique(targets, (target) => target.name, "target name");
|
|
70
|
+
assertUnique(targets, (target) => target.configPath, "config path");
|
|
71
|
+
assertUnique(
|
|
72
|
+
targets,
|
|
73
|
+
(target) => target.config.runtime.projectId,
|
|
74
|
+
"runtime project ID",
|
|
75
|
+
);
|
|
76
|
+
assertUnique(
|
|
77
|
+
targets.flatMap((target) =>
|
|
78
|
+
Object.entries(target.config.runtime.ports).map(([kind, port]) => ({
|
|
79
|
+
name: `${target.name} (${kind})`,
|
|
80
|
+
port,
|
|
81
|
+
})),
|
|
82
|
+
),
|
|
83
|
+
(entry) => entry.port,
|
|
84
|
+
"runtime port",
|
|
85
|
+
);
|
|
86
|
+
assertUnique(
|
|
87
|
+
targets,
|
|
88
|
+
(target) => target.paths.applicationEnvironment,
|
|
89
|
+
"application environment file",
|
|
90
|
+
);
|
|
91
|
+
assertUnique(
|
|
92
|
+
targets.filter((target) => target.paths.rehearsalConfig),
|
|
93
|
+
(target) => target.paths.rehearsalConfig,
|
|
94
|
+
"Supabase runtime config",
|
|
95
|
+
);
|
|
96
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
97
|
+
for (
|
|
98
|
+
let otherIndex = index + 1;
|
|
99
|
+
otherIndex < targets.length;
|
|
100
|
+
otherIndex += 1
|
|
101
|
+
) {
|
|
102
|
+
const left = targets[index];
|
|
103
|
+
const right = targets[otherIndex];
|
|
104
|
+
if (
|
|
105
|
+
isInside(left.paths.artifactDirectory, right.paths.artifactDirectory) ||
|
|
106
|
+
isInside(right.paths.artifactDirectory, left.paths.artifactDirectory)
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Dependent runtimes ${left.name} and ${right.name} must use separate, non-nested .rehearsal artifact directories.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
projectRoot: primaryLoaded.projectRoot,
|
|
116
|
+
primary,
|
|
117
|
+
dependents: Object.freeze(dependents),
|
|
118
|
+
targets: Object.freeze(targets),
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const candidateSummary = (inspection) => {
|
|
123
|
+
const candidates = inspection.migrations.filter(
|
|
124
|
+
(migration) => migration.status === "candidate",
|
|
125
|
+
);
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
baselineGenerationId: inspection.baselineGenerationId,
|
|
128
|
+
candidateSha256: inspection.candidateSha256,
|
|
129
|
+
candidateCount: candidates.length,
|
|
130
|
+
candidates: Object.freeze(candidates),
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const inspectRuntimeTopologyCandidates = async (options = {}) => {
|
|
135
|
+
const topology = await loadRuntimeTopology(options);
|
|
136
|
+
const targets = [];
|
|
137
|
+
for (const target of topology.targets) {
|
|
138
|
+
targets.push(
|
|
139
|
+
Object.freeze({
|
|
140
|
+
...target,
|
|
141
|
+
summary: candidateSummary(
|
|
142
|
+
await inspectRehearsalMigrations({
|
|
143
|
+
projectRoot: topology.projectRoot,
|
|
144
|
+
configPath: target.configPath,
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (targets.length === 1) {
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
topology,
|
|
153
|
+
targets: Object.freeze(targets),
|
|
154
|
+
candidateSha256: targets[0].summary.candidateSha256,
|
|
155
|
+
candidateCount: targets[0].summary.candidateCount,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const digestInput = targets.map((target) => ({
|
|
159
|
+
name: target.name,
|
|
160
|
+
candidateSha256: target.summary.candidateSha256,
|
|
161
|
+
candidates: target.summary.candidates.map((candidate) => ({
|
|
162
|
+
filename: candidate.filename,
|
|
163
|
+
sha256: candidate.sha256,
|
|
164
|
+
})),
|
|
165
|
+
}));
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
topology,
|
|
168
|
+
targets: Object.freeze(targets),
|
|
169
|
+
candidateSha256: createHash("sha256")
|
|
170
|
+
.update(JSON.stringify(digestInput))
|
|
171
|
+
.digest("hex"),
|
|
172
|
+
candidateCount: targets.reduce(
|
|
173
|
+
(total, target) => total + target.summary.candidateCount,
|
|
174
|
+
0,
|
|
175
|
+
),
|
|
176
|
+
});
|
|
177
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
9
|
export const REHEARSAL_RESULT_VERSION = 1;
|
|
10
|
-
export const REHEARSAL_VERSION = "0.1.0-beta.
|
|
10
|
+
export const REHEARSAL_VERSION = "0.1.0-beta.9";
|
|
11
11
|
|
|
12
12
|
export const REHEARSAL_EXIT_CODES = Object.freeze({
|
|
13
13
|
success: 0,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Project-wide mutation lock and installed-package integrity guard. */
|
|
2
|
+
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
readFile,
|
|
8
|
+
readdir,
|
|
9
|
+
realpath,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { readFileSync, readdirSync, rmSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
|
|
19
|
+
const LOCK_ROOT = join(tmpdir(), "rehearsal-db-operation-locks");
|
|
20
|
+
|
|
21
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
22
|
+
|
|
23
|
+
const packageFiles = (root, directory = root) =>
|
|
24
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
25
|
+
const path = join(directory, entry.name);
|
|
26
|
+
if (entry.isDirectory()) return packageFiles(root, path);
|
|
27
|
+
if (!entry.isFile()) return [];
|
|
28
|
+
return [relative(root, path)];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const createInstalledPackageFingerprint = ({ packageRoot }) => {
|
|
32
|
+
const files = [
|
|
33
|
+
"package.json",
|
|
34
|
+
...["src", "scripts/runtime"].flatMap((directory) =>
|
|
35
|
+
packageFiles(packageRoot, join(packageRoot, directory)),
|
|
36
|
+
),
|
|
37
|
+
].sort();
|
|
38
|
+
const hash = createHash("sha256");
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
hash.update(file);
|
|
41
|
+
hash.update("\0");
|
|
42
|
+
hash.update(readFileSync(join(packageRoot, file)));
|
|
43
|
+
hash.update("\0");
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze({ files: files.length, sha256: hash.digest("hex") });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const assertInstalledPackageFingerprint = ({
|
|
49
|
+
packageRoot,
|
|
50
|
+
expected,
|
|
51
|
+
}) => {
|
|
52
|
+
const actual = createInstalledPackageFingerprint({ packageRoot });
|
|
53
|
+
if (actual.files !== expected?.files || actual.sha256 !== expected?.sha256) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"The installed Rehearsal package changed during this operation. Reinstall the intended version, then start a new command.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return actual;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const processIsAlive = (pid) => {
|
|
62
|
+
if (!Number.isSafeInteger(pid) || pid < 1) return false;
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0);
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return error?.code === "EPERM";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const assertPrivateLockRoot = async () => {
|
|
72
|
+
await mkdir(LOCK_ROOT, { recursive: true, mode: 0o700 });
|
|
73
|
+
const details = await stat(LOCK_ROOT);
|
|
74
|
+
if (
|
|
75
|
+
!details.isDirectory() ||
|
|
76
|
+
(details.mode & 0o077) !== 0 ||
|
|
77
|
+
(typeof process.getuid === "function" && details.uid !== process.getuid())
|
|
78
|
+
) {
|
|
79
|
+
throw new Error("The Rehearsal operation-lock directory is not private.");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const acquireProjectOperation = async ({ projectRoot, command }) => {
|
|
84
|
+
await assertPrivateLockRoot();
|
|
85
|
+
const canonicalRoot = await realpath(projectRoot);
|
|
86
|
+
const projectHash = digest(canonicalRoot);
|
|
87
|
+
const lockDirectory = join(LOCK_ROOT, projectHash);
|
|
88
|
+
const ownerPath = join(lockDirectory, "owner.json");
|
|
89
|
+
const token = randomBytes(16).toString("hex");
|
|
90
|
+
const owner = {
|
|
91
|
+
formatVersion: 1,
|
|
92
|
+
pid: process.pid,
|
|
93
|
+
command,
|
|
94
|
+
projectHash,
|
|
95
|
+
token,
|
|
96
|
+
startedAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
100
|
+
try {
|
|
101
|
+
await mkdir(lockDirectory, { mode: 0o700 });
|
|
102
|
+
await writeFile(ownerPath, `${JSON.stringify(owner)}\n`, {
|
|
103
|
+
mode: 0o600,
|
|
104
|
+
flag: "wx",
|
|
105
|
+
});
|
|
106
|
+
const assertOwnership = (current) => {
|
|
107
|
+
if (current.token !== token || current.pid !== process.pid) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"The Rehearsal operation lock changed ownership unexpectedly.",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
command,
|
|
115
|
+
async release() {
|
|
116
|
+
const current = JSON.parse(await readFile(ownerPath, "utf8"));
|
|
117
|
+
assertOwnership(current);
|
|
118
|
+
await rm(lockDirectory, { recursive: true });
|
|
119
|
+
},
|
|
120
|
+
releaseSync() {
|
|
121
|
+
const current = JSON.parse(readFileSync(ownerPath, "utf8"));
|
|
122
|
+
assertOwnership(current);
|
|
123
|
+
rmSync(lockDirectory, { recursive: true });
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code !== "EEXIST" || attempt > 0) throw error;
|
|
128
|
+
const details = await lstat(lockDirectory);
|
|
129
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
130
|
+
throw new Error("The Rehearsal operation lock is unsafe.");
|
|
131
|
+
}
|
|
132
|
+
let current;
|
|
133
|
+
try {
|
|
134
|
+
current = JSON.parse(await readFile(ownerPath, "utf8"));
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"Another Rehearsal operation owns this project, or its lock is incomplete.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (processIsAlive(current.pid)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Another Rehearsal operation is already running for this project (PID ${current.pid}).`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const staleDirectory = `${lockDirectory}.stale-${token}`;
|
|
146
|
+
try {
|
|
147
|
+
await rename(lockDirectory, staleDirectory);
|
|
148
|
+
} catch (renameError) {
|
|
149
|
+
if (renameError?.code === "ENOENT") continue;
|
|
150
|
+
throw renameError;
|
|
151
|
+
}
|
|
152
|
+
await rm(staleDirectory, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error("Rehearsal could not acquire its project operation lock.");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const inspectProjectOperationLocks = async () =>
|
|
159
|
+
readdir(LOCK_ROOT).catch((error) => {
|
|
160
|
+
if (error?.code === "ENOENT") return [];
|
|
161
|
+
throw error;
|
|
162
|
+
});
|
|
@@ -27,7 +27,11 @@ const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
|
27
27
|
|
|
28
28
|
export const isLoopbackUrl = (value) => {
|
|
29
29
|
try {
|
|
30
|
-
|
|
30
|
+
const parsed = new URL(value);
|
|
31
|
+
return (
|
|
32
|
+
["http:", "https:"].includes(parsed.protocol) &&
|
|
33
|
+
LOOPBACK_HOSTNAMES.has(parsed.hostname)
|
|
34
|
+
);
|
|
31
35
|
} catch {
|
|
32
36
|
return false;
|
|
33
37
|
}
|