@rehearsal-db/core 0.1.0-beta.13 → 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.
- package/CHANGELOG.md +93 -0
- package/README.md +4 -1
- package/dist/package.json +100 -0
- package/dist/scripts/install/create_root_config.d.mts +3 -0
- package/dist/scripts/install/create_root_config.mjs +13 -0
- package/dist/scripts/runtime/manage_database.d.mts +6 -0
- package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
- package/dist/src/application/session.d.mts +96 -0
- package/dist/src/application/session.mjs +380 -0
- package/dist/src/baseline/artifact.d.mts +119 -0
- package/dist/src/baseline/artifact.mjs +522 -0
- package/dist/src/baseline/builder.d.mts +19 -0
- package/dist/src/baseline/builder.mjs +142 -0
- package/dist/src/baseline/input_discovery.d.mts +13 -0
- package/dist/src/baseline/input_discovery.mjs +149 -0
- package/dist/src/baseline/policy_review.d.mts +56 -0
- package/dist/src/baseline/policy_review.mjs +73 -0
- package/dist/src/baseline/preparation.d.mts +63 -0
- package/dist/src/baseline/preparation.mjs +295 -0
- package/dist/src/baseline/privacy_engine.d.mts +162 -0
- package/dist/src/baseline/privacy_engine.mjs +1162 -0
- package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
- package/dist/src/baseline/privacy_structured_json.mjs +464 -0
- package/dist/src/baseline/sanitization_policy.d.mts +69 -0
- package/dist/src/baseline/sanitization_policy.mjs +227 -0
- package/dist/src/baseline/schema_snapshot.d.mts +19 -0
- package/dist/src/baseline/schema_snapshot.mjs +79 -0
- package/dist/src/cli/arguments.d.mts +43 -0
- package/dist/src/cli/arguments.mjs +135 -0
- package/dist/src/cli/guided.d.mts +51 -0
- package/dist/src/cli/guided.mjs +713 -0
- package/dist/src/cli/init_command.d.mts +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- package/dist/src/cli/project_commands.d.mts +157 -0
- package/dist/src/cli/project_commands.mjs +183 -0
- package/dist/src/cli/rehearsal.d.mts +7 -0
- package/dist/src/cli/rehearsal.mjs +706 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- package/dist/src/cli/runtime_commands.d.mts +265 -0
- package/dist/src/cli/runtime_commands.mjs +599 -0
- package/dist/src/cli/source_commands.d.mts +432 -0
- package/dist/src/cli/source_commands.mjs +299 -0
- package/dist/src/cli/terminal.d.mts +59 -0
- package/dist/src/cli/terminal.mjs +230 -0
- package/dist/src/identity/claim.d.mts +144 -0
- package/dist/src/identity/claim.mjs +947 -0
- package/dist/src/identity/provider_configuration.d.mts +37 -0
- package/dist/src/identity/provider_configuration.mjs +132 -0
- package/dist/src/identity/storage.d.mts +21 -0
- package/dist/src/identity/storage.mjs +122 -0
- package/dist/src/index.d.mts +4 -0
- package/dist/src/index.mjs +3 -0
- package/dist/src/project/configuration.d.mts +181 -0
- package/dist/src/project/configuration.mjs +958 -0
- package/dist/src/project/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/setup.d.mts +87 -0
- package/dist/src/project/setup.mjs +414 -0
- package/dist/src/project/support_report.d.mts +88 -0
- package/dist/src/project/support_report.mjs +83 -0
- package/dist/src/runtime/cleanup.d.mts +85 -0
- package/dist/src/runtime/cleanup.mjs +311 -0
- package/dist/src/runtime/migration_history.d.mts +51 -0
- package/dist/src/runtime/migration_history.mjs +170 -0
- package/dist/src/runtime/plan.d.mts +134 -0
- package/dist/src/runtime/plan.mjs +636 -0
- package/dist/src/runtime/policy.d.mts +66 -0
- package/dist/src/runtime/policy.mjs +369 -0
- package/dist/src/runtime/restore.d.mts +22 -0
- package/dist/src/runtime/restore.mjs +276 -0
- package/dist/src/runtime/service_environment.d.mts +11 -0
- package/dist/src/runtime/service_environment.mjs +58 -0
- package/dist/src/runtime/topology.d.mts +51 -0
- package/dist/src/runtime/topology.mjs +128 -0
- package/dist/src/shared/diagnostics.d.mts +97 -0
- package/dist/src/shared/diagnostics.mjs +170 -0
- package/dist/src/shared/human_output.d.mts +2 -0
- package/{src → dist/src}/shared/human_output.mjs +1 -3
- package/dist/src/shared/node_runtime.d.mts +14 -0
- package/dist/src/shared/node_runtime.mjs +25 -0
- package/dist/src/shared/operation_guard.d.mts +22 -0
- package/dist/src/shared/operation_guard.mjs +150 -0
- package/dist/src/shared/process_environment.d.mts +15 -0
- package/dist/src/shared/process_environment.mjs +50 -0
- package/dist/src/source/access.d.mts +165 -0
- package/dist/src/source/access.mjs +602 -0
- package/dist/src/source/asset_transfer.d.mts +27 -0
- package/dist/src/source/asset_transfer.mjs +202 -0
- package/dist/src/source/baseline.d.mts +66 -0
- package/dist/src/source/baseline.mjs +372 -0
- package/dist/src/source/external_reader.d.mts +42 -0
- package/dist/src/source/external_reader.mjs +278 -0
- package/dist/src/source/postgresql_access.d.mts +86 -0
- package/dist/src/source/postgresql_access.mjs +454 -0
- package/dist/src/source/privacy_paths.d.mts +10 -0
- package/dist/src/source/privacy_paths.mjs +27 -0
- package/dist/src/targets/driver.d.mts +2 -0
- package/dist/src/targets/driver.mjs +13 -0
- package/dist/src/targets/postgresql.d.mts +5 -0
- package/dist/src/targets/postgresql.mjs +641 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +659 -0
- package/dist/src/targets/supabase_environment.d.mts +49 -0
- package/dist/src/targets/supabase_environment.mjs +162 -0
- package/dist/src/targets/target.d.mts +33 -0
- package/dist/src/targets/target.mjs +39 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +17 -9
- package/docs/configuration.md +49 -8
- package/docs/getting-started.md +28 -0
- package/docs/index.md +1 -0
- package/docs/production-source.md +43 -7
- package/docs/releasing.md +5 -3
- package/docs/roadmap.md +3 -2
- package/docs/sanitization.md +415 -14
- package/docs/standalone-workflow.md +12 -6
- package/docs/troubleshooting.md +5 -1
- package/docs/tutorial.md +5 -0
- package/docs/typescript.md +87 -0
- package/package.json +35 -29
- package/scripts/install/create_root_config.mjs +0 -36
- package/src/README.md +0 -17
- package/src/application/session.mjs +0 -435
- package/src/baseline/artifact.mjs +0 -684
- package/src/baseline/builder.mjs +0 -164
- package/src/baseline/input_discovery.mjs +0 -155
- package/src/baseline/policy_review.mjs +0 -92
- package/src/baseline/preparation.mjs +0 -323
- package/src/baseline/privacy_engine.mjs +0 -952
- package/src/baseline/sanitization_policy.mjs +0 -299
- package/src/baseline/schema_snapshot.mjs +0 -113
- package/src/cli/arguments.mjs +0 -126
- package/src/cli/guided.mjs +0 -814
- package/src/cli/init_command.mjs +0 -94
- package/src/cli/rehearsal.mjs +0 -970
- package/src/cli/renderers.mjs +0 -626
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -347
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1163
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -291
- package/src/project/setup.mjs +0 -466
- package/src/project/support_report.mjs +0 -105
- package/src/runtime/cleanup.mjs +0 -381
- package/src/runtime/migration_history.mjs +0 -220
- package/src/runtime/plan.mjs +0 -722
- package/src/runtime/policy.mjs +0 -438
- package/src/runtime/restore.mjs +0 -337
- package/src/runtime/service_environment.mjs +0 -82
- package/src/runtime/topology.mjs +0 -177
- package/src/shared/diagnostics.mjs +0 -207
- package/src/shared/node_runtime.mjs +0 -31
- package/src/shared/operation_guard.mjs +0 -162
- package/src/shared/process_environment.mjs +0 -68
- package/src/source/access.mjs +0 -753
- package/src/source/asset_transfer.mjs +0 -207
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -255
- package/src/source/postgresql_access.mjs +0 -581
- package/src/source/privacy_paths.mjs +0 -38
- package/src/targets/postgresql.mjs +0 -808
- package/src/targets/supabase.mjs +0 -840
- package/src/targets/supabase_environment.mjs +0 -229
- package/src/targets/target.mjs +0 -66
- /package/{src/project → types}/configuration.d.mts +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Provide stable Rehearsal result, failure, redaction, and rendering
|
|
3
|
+
* contracts for humans and automation. Do not run directly; this module is reusable
|
|
4
|
+
* script infrastructure.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import packageManifest from "../../package.json" with { type: "json" };
|
|
8
|
+
export const REHEARSAL_RESULT_VERSION = 1;
|
|
9
|
+
export const REHEARSAL_VERSION = packageManifest.version;
|
|
10
|
+
export const REHEARSAL_EXIT_CODES = Object.freeze({
|
|
11
|
+
success: 0,
|
|
12
|
+
configuration_invalid: 2,
|
|
13
|
+
unsafe_environment: 3,
|
|
14
|
+
baseline_invalid: 4,
|
|
15
|
+
baseline_checksum_mismatch: 5,
|
|
16
|
+
migration_candidate_failure: 6,
|
|
17
|
+
migration_verification_failure: 7,
|
|
18
|
+
application_proof_failure: 8,
|
|
19
|
+
runtime_dependency_failure: 9,
|
|
20
|
+
internal_failure: 10,
|
|
21
|
+
});
|
|
22
|
+
const SECRET_KEY_PATTERN = /(?:authorization|cookie|credential|database[_-]?url|jwt|key|password|secret|service[_-]?role|token)/iu;
|
|
23
|
+
const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu;
|
|
24
|
+
const CONNECTION_STRING_PATTERN = /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\/[^\s"']+/giu;
|
|
25
|
+
const BEARER_PATTERN = /\bBearer\s+[^\s"']+/giu;
|
|
26
|
+
const SUPABASE_KEY_PATTERN = /\bsb_(?:publishable|secret)_[A-Za-z0-9_-]+\b/gu;
|
|
27
|
+
const redactString = (value) => value
|
|
28
|
+
.replace(CONNECTION_STRING_PATTERN, "[REDACTED_CONNECTION_STRING]")
|
|
29
|
+
.replace(JWT_PATTERN, "[REDACTED_JWT]")
|
|
30
|
+
.replace(BEARER_PATTERN, "Bearer [REDACTED]")
|
|
31
|
+
.replace(SUPABASE_KEY_PATTERN, "[REDACTED_KEY]");
|
|
32
|
+
export const redactDiagnosticValue = (value, key = "") => {
|
|
33
|
+
if (SECRET_KEY_PATTERN.test(key))
|
|
34
|
+
return "[REDACTED]";
|
|
35
|
+
if (typeof value === "string")
|
|
36
|
+
return redactString(value);
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map((entry) => redactDiagnosticValue(entry));
|
|
39
|
+
}
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
42
|
+
entryKey,
|
|
43
|
+
redactDiagnosticValue(entryValue, entryKey),
|
|
44
|
+
]));
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
const diagnosticIdFor = ({ category, code, message, }) => createHash("sha256")
|
|
49
|
+
.update(`${category}\0${code}\0${message}`)
|
|
50
|
+
.digest("hex")
|
|
51
|
+
.slice(0, 12);
|
|
52
|
+
export class RehearsalError extends Error {
|
|
53
|
+
category;
|
|
54
|
+
code;
|
|
55
|
+
expected;
|
|
56
|
+
actual;
|
|
57
|
+
context;
|
|
58
|
+
refused;
|
|
59
|
+
suggestions;
|
|
60
|
+
diagnosticId;
|
|
61
|
+
exitCode;
|
|
62
|
+
constructor({ category, code, message, expected, actual, context, refused, suggestions = [], cause, }) {
|
|
63
|
+
super(message, { cause });
|
|
64
|
+
this.name = "RehearsalError";
|
|
65
|
+
if (!Object.hasOwn(REHEARSAL_EXIT_CODES, category)) {
|
|
66
|
+
throw new Error(`Unknown Rehearsal error category: ${category}.`);
|
|
67
|
+
}
|
|
68
|
+
this.category = category;
|
|
69
|
+
this.code = code;
|
|
70
|
+
this.expected = expected;
|
|
71
|
+
this.actual = actual;
|
|
72
|
+
this.context = context;
|
|
73
|
+
this.refused = refused;
|
|
74
|
+
this.suggestions = suggestions;
|
|
75
|
+
this.diagnosticId = diagnosticIdFor({ category, code, message });
|
|
76
|
+
this.exitCode = REHEARSAL_EXIT_CODES[category];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
80
|
+
const inferCategory = (error) => {
|
|
81
|
+
const message = errorMessage(error);
|
|
82
|
+
if (/checksum/iu.test(message))
|
|
83
|
+
return "baseline_checksum_mismatch";
|
|
84
|
+
if (/container\b.*\b(?:is not ready|starting|unhealthy)|health(?:check)?\b.*\b(?:failed|starting|unhealthy)|out of memory|\boom\b|no space left|resource temporarily unavailable/iu.test(message)) {
|
|
85
|
+
return "runtime_dependency_failure";
|
|
86
|
+
}
|
|
87
|
+
if (/candidate|migration history diverges/iu.test(message)) {
|
|
88
|
+
return "migration_candidate_failure";
|
|
89
|
+
}
|
|
90
|
+
if (/baseline|artifact|generation|sanitization policy/iu.test(message)) {
|
|
91
|
+
return "baseline_invalid";
|
|
92
|
+
}
|
|
93
|
+
if (/migration|ledger|replay/iu.test(message)) {
|
|
94
|
+
return "migration_verification_failure";
|
|
95
|
+
}
|
|
96
|
+
if (/config|unknown rehearsal|unsupported rehearsal/iu.test(message)) {
|
|
97
|
+
return "configuration_invalid";
|
|
98
|
+
}
|
|
99
|
+
if (/loopback|hosted|unsafe|credential|outbound/iu.test(message)) {
|
|
100
|
+
return "unsafe_environment";
|
|
101
|
+
}
|
|
102
|
+
if (/docker|postgres(?:ql)?|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(message)) {
|
|
103
|
+
return "runtime_dependency_failure";
|
|
104
|
+
}
|
|
105
|
+
return "internal_failure";
|
|
106
|
+
};
|
|
107
|
+
export const normalizeRehearsalError = (error, overrides = {}) => {
|
|
108
|
+
if (error instanceof RehearsalError)
|
|
109
|
+
return error;
|
|
110
|
+
const message = errorMessage(error);
|
|
111
|
+
const category = overrides.category ?? inferCategory(error);
|
|
112
|
+
return new RehearsalError({
|
|
113
|
+
category,
|
|
114
|
+
code: overrides.code ?? category.toUpperCase(),
|
|
115
|
+
message,
|
|
116
|
+
expected: overrides.expected,
|
|
117
|
+
actual: overrides.actual,
|
|
118
|
+
context: overrides.context,
|
|
119
|
+
refused: overrides.refused ??
|
|
120
|
+
"Rehearsal stopped before performing the requested operation.",
|
|
121
|
+
suggestions: overrides.suggestions ?? [],
|
|
122
|
+
cause: error,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
export const createRehearsalResult = ({ command, status, data, warnings = [], startedAt = new Date(), durationMs = 0, }) => redactDiagnosticValue({
|
|
126
|
+
schemaVersion: REHEARSAL_RESULT_VERSION,
|
|
127
|
+
rehearsalVersion: REHEARSAL_VERSION,
|
|
128
|
+
command,
|
|
129
|
+
status,
|
|
130
|
+
startedAt: startedAt.toISOString(),
|
|
131
|
+
durationMs,
|
|
132
|
+
warnings,
|
|
133
|
+
data,
|
|
134
|
+
});
|
|
135
|
+
export const serializeRehearsalError = (error, { debug = false } = {}) => {
|
|
136
|
+
const failure = normalizeRehearsalError(error);
|
|
137
|
+
return redactDiagnosticValue({
|
|
138
|
+
schemaVersion: REHEARSAL_RESULT_VERSION,
|
|
139
|
+
rehearsalVersion: REHEARSAL_VERSION,
|
|
140
|
+
status: "error",
|
|
141
|
+
error: {
|
|
142
|
+
category: failure.category,
|
|
143
|
+
code: failure.code,
|
|
144
|
+
message: failure.message,
|
|
145
|
+
expected: failure.expected,
|
|
146
|
+
actual: failure.actual,
|
|
147
|
+
context: failure.context,
|
|
148
|
+
refused: failure.refused,
|
|
149
|
+
suggestions: failure.suggestions,
|
|
150
|
+
diagnosticId: failure.diagnosticId,
|
|
151
|
+
...(debug && failure.cause ? { cause: errorMessage(failure.cause) } : {}),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const formatValue = (value) => typeof value === "string" ? value : (JSON.stringify(value) ?? String(value));
|
|
156
|
+
export const renderHumanError = (error, { debug = false } = {}) => {
|
|
157
|
+
const { error: failure } = serializeRehearsalError(error, { debug });
|
|
158
|
+
return [
|
|
159
|
+
`Rehearsal stopped: ${failure.message}`,
|
|
160
|
+
failure.expected ? `Expected: ${formatValue(failure.expected)}` : null,
|
|
161
|
+
failure.actual ? `Actual: ${formatValue(failure.actual)}` : null,
|
|
162
|
+
failure.context ? `Context: ${formatValue(failure.context)}` : null,
|
|
163
|
+
failure.refused ? `Refused: ${failure.refused}` : null,
|
|
164
|
+
...(failure.suggestions ?? []).map((value) => `Try: ${value}`),
|
|
165
|
+
`Diagnostic: ${failure.diagnosticId} (${failure.category})`,
|
|
166
|
+
debug && failure.cause ? `Cause: ${failure.cause}` : null,
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join("\n");
|
|
170
|
+
};
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
/** Purpose: Keep human-facing Rehearsal counts grammatically consistent. */
|
|
2
|
-
|
|
3
|
-
export const formatCount = (count, singular, plural = `${singular}s`) =>
|
|
4
|
-
`${count} ${count === 1 ? singular : plural}`;
|
|
2
|
+
export const formatCount = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported Node.js release lines. Keep this aligned with package.json and the
|
|
3
|
+
* verification matrix.
|
|
4
|
+
*/
|
|
5
|
+
export interface RehearsalNodeRuntime {
|
|
6
|
+
version: string;
|
|
7
|
+
major: number;
|
|
8
|
+
supported: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const REHEARSAL_NODE_MAJORS: readonly number[];
|
|
11
|
+
export declare const REHEARSAL_NODE_LABEL = "Node.js 22, 24, or 26";
|
|
12
|
+
export declare const REHEARSAL_RECOMMENDED_NODE_MAJOR = 24;
|
|
13
|
+
export declare const inspectRehearsalNodeRuntime: (version?: string | number) => RehearsalNodeRuntime;
|
|
14
|
+
export declare const assertSupportedRehearsalNodeRuntime: (version?: string | number) => RehearsalNodeRuntime;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported Node.js release lines. Keep this aligned with package.json and the
|
|
3
|
+
* verification matrix.
|
|
4
|
+
*/
|
|
5
|
+
export const REHEARSAL_NODE_MAJORS = Object.freeze([
|
|
6
|
+
22, 24, 26,
|
|
7
|
+
]);
|
|
8
|
+
export const REHEARSAL_NODE_LABEL = "Node.js 22, 24, or 26";
|
|
9
|
+
export const REHEARSAL_RECOMMENDED_NODE_MAJOR = 24;
|
|
10
|
+
export const inspectRehearsalNodeRuntime = (version = process.versions.node) => {
|
|
11
|
+
const [majorText = ""] = String(version).split(".");
|
|
12
|
+
const major = Number.parseInt(majorText, 10);
|
|
13
|
+
return {
|
|
14
|
+
version: String(version),
|
|
15
|
+
major,
|
|
16
|
+
supported: REHEARSAL_NODE_MAJORS.includes(major),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export const assertSupportedRehearsalNodeRuntime = (version = process.versions.node) => {
|
|
20
|
+
const runtime = inspectRehearsalNodeRuntime(version);
|
|
21
|
+
if (!runtime.supported) {
|
|
22
|
+
throw new Error(`Rehearsal requires ${REHEARSAL_NODE_LABEL}; this shell is using Node.js ${runtime.version}. Switch to a supported release, then rerun setup. With nvm: nvm install --lts && nvm use --lts.`);
|
|
23
|
+
}
|
|
24
|
+
return runtime;
|
|
25
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Project-wide mutation lock and installed-package integrity guard. */
|
|
2
|
+
export interface InstalledPackageFingerprint {
|
|
3
|
+
readonly files: number;
|
|
4
|
+
readonly sha256: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProjectOperation {
|
|
7
|
+
readonly command: string;
|
|
8
|
+
release(): Promise<void>;
|
|
9
|
+
releaseSync(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const createInstalledPackageFingerprint: ({ packageRoot, }: {
|
|
12
|
+
packageRoot: string;
|
|
13
|
+
}) => InstalledPackageFingerprint;
|
|
14
|
+
export declare const assertInstalledPackageFingerprint: ({ packageRoot, expected, }: {
|
|
15
|
+
packageRoot: string;
|
|
16
|
+
expected: InstalledPackageFingerprint;
|
|
17
|
+
}) => InstalledPackageFingerprint;
|
|
18
|
+
export declare const acquireProjectOperation: ({ projectRoot, command, }: {
|
|
19
|
+
projectRoot: string;
|
|
20
|
+
command: string;
|
|
21
|
+
}) => Promise<ProjectOperation>;
|
|
22
|
+
export declare const inspectProjectOperationLocks: () => Promise<string[]>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Project-wide mutation lock and installed-package integrity guard. */
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { readFileSync, readdirSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
const LOCK_ROOT = join(tmpdir(), "rehearsal-db-operation-locks");
|
|
8
|
+
const hasErrorCode = (error, code) => typeof error === "object" &&
|
|
9
|
+
error !== null &&
|
|
10
|
+
"code" in error &&
|
|
11
|
+
error.code === code;
|
|
12
|
+
const parseLockOwner = (source) => {
|
|
13
|
+
const value = JSON.parse(source);
|
|
14
|
+
if (typeof value !== "object" ||
|
|
15
|
+
value === null ||
|
|
16
|
+
!("pid" in value) ||
|
|
17
|
+
!Number.isSafeInteger(value.pid) ||
|
|
18
|
+
!("token" in value) ||
|
|
19
|
+
typeof value.token !== "string") {
|
|
20
|
+
throw new Error("The Rehearsal operation-lock owner is invalid.");
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
25
|
+
const packageFiles = (root, directory = root) => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
26
|
+
const path = join(directory, entry.name);
|
|
27
|
+
if (entry.isDirectory())
|
|
28
|
+
return packageFiles(root, path);
|
|
29
|
+
if (!entry.isFile())
|
|
30
|
+
return [];
|
|
31
|
+
return [relative(root, path)];
|
|
32
|
+
});
|
|
33
|
+
export const createInstalledPackageFingerprint = ({ packageRoot, }) => {
|
|
34
|
+
const files = [
|
|
35
|
+
"package.json",
|
|
36
|
+
...["dist/src", "dist/scripts/runtime"].flatMap((directory) => packageFiles(packageRoot, join(packageRoot, directory))),
|
|
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
|
+
export const assertInstalledPackageFingerprint = ({ packageRoot, expected, }) => {
|
|
48
|
+
const actual = createInstalledPackageFingerprint({ packageRoot });
|
|
49
|
+
if (actual.files !== expected?.files || actual.sha256 !== expected?.sha256) {
|
|
50
|
+
throw new Error("The installed Rehearsal package changed during this operation. Reinstall the intended version, then start a new command.");
|
|
51
|
+
}
|
|
52
|
+
return actual;
|
|
53
|
+
};
|
|
54
|
+
const processIsAlive = (pid) => {
|
|
55
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid < 1)
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid, 0);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return hasErrorCode(error, "EPERM");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const assertPrivateLockRoot = async () => {
|
|
66
|
+
await mkdir(LOCK_ROOT, { recursive: true, mode: 0o700 });
|
|
67
|
+
const details = await stat(LOCK_ROOT);
|
|
68
|
+
if (!details.isDirectory() ||
|
|
69
|
+
(details.mode & 0o077) !== 0 ||
|
|
70
|
+
(typeof process.getuid === "function" && details.uid !== process.getuid())) {
|
|
71
|
+
throw new Error("The Rehearsal operation-lock directory is not private.");
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
export const acquireProjectOperation = async ({ projectRoot, command, }) => {
|
|
75
|
+
await assertPrivateLockRoot();
|
|
76
|
+
const canonicalRoot = await realpath(projectRoot);
|
|
77
|
+
const projectHash = digest(canonicalRoot);
|
|
78
|
+
const lockDirectory = join(LOCK_ROOT, projectHash);
|
|
79
|
+
const ownerPath = join(lockDirectory, "owner.json");
|
|
80
|
+
const token = randomBytes(16).toString("hex");
|
|
81
|
+
const owner = {
|
|
82
|
+
formatVersion: 1,
|
|
83
|
+
pid: process.pid,
|
|
84
|
+
command,
|
|
85
|
+
projectHash,
|
|
86
|
+
token,
|
|
87
|
+
startedAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
90
|
+
try {
|
|
91
|
+
await mkdir(lockDirectory, { mode: 0o700 });
|
|
92
|
+
await writeFile(ownerPath, `${JSON.stringify(owner)}\n`, {
|
|
93
|
+
mode: 0o600,
|
|
94
|
+
flag: "wx",
|
|
95
|
+
});
|
|
96
|
+
const assertOwnership = (current) => {
|
|
97
|
+
if (current.token !== token || current.pid !== process.pid) {
|
|
98
|
+
throw new Error("The Rehearsal operation lock changed ownership unexpectedly.");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
command,
|
|
103
|
+
async release() {
|
|
104
|
+
const current = parseLockOwner(await readFile(ownerPath, "utf8"));
|
|
105
|
+
assertOwnership(current);
|
|
106
|
+
await rm(lockDirectory, { recursive: true });
|
|
107
|
+
},
|
|
108
|
+
releaseSync() {
|
|
109
|
+
const current = parseLockOwner(readFileSync(ownerPath, "utf8"));
|
|
110
|
+
assertOwnership(current);
|
|
111
|
+
rmSync(lockDirectory, { recursive: true });
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!hasErrorCode(error, "EEXIST") || attempt > 0)
|
|
117
|
+
throw error;
|
|
118
|
+
const details = await lstat(lockDirectory);
|
|
119
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
120
|
+
throw new Error("The Rehearsal operation lock is unsafe.");
|
|
121
|
+
}
|
|
122
|
+
let current;
|
|
123
|
+
try {
|
|
124
|
+
current = parseLockOwner(await readFile(ownerPath, "utf8"));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error("Another Rehearsal operation owns this project, or its lock is incomplete.");
|
|
128
|
+
}
|
|
129
|
+
if (processIsAlive(current.pid)) {
|
|
130
|
+
throw new Error(`Another Rehearsal operation is already running for this project (PID ${current.pid}).`);
|
|
131
|
+
}
|
|
132
|
+
const staleDirectory = `${lockDirectory}.stale-${token}`;
|
|
133
|
+
try {
|
|
134
|
+
await rename(lockDirectory, staleDirectory);
|
|
135
|
+
}
|
|
136
|
+
catch (renameError) {
|
|
137
|
+
if (hasErrorCode(renameError, "ENOENT"))
|
|
138
|
+
continue;
|
|
139
|
+
throw renameError;
|
|
140
|
+
}
|
|
141
|
+
await rm(staleDirectory, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error("Rehearsal could not acquire its project operation lock.");
|
|
145
|
+
};
|
|
146
|
+
export const inspectProjectOperationLocks = async () => readdir(LOCK_ROOT).catch((error) => {
|
|
147
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
148
|
+
return [];
|
|
149
|
+
throw error;
|
|
150
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Build minimal child-process environments and validate loopback URLs for
|
|
3
|
+
* the project-neutral Rehearsal runtime. Do not run directly; this module is reusable
|
|
4
|
+
* script infrastructure.
|
|
5
|
+
*/
|
|
6
|
+
export type ProcessEnvironment = Record<string, string | undefined>;
|
|
7
|
+
export interface CleanProcessEnvironmentOptions {
|
|
8
|
+
inheritedEnvironment?: ProcessEnvironment;
|
|
9
|
+
overrides?: ProcessEnvironment;
|
|
10
|
+
passthroughKeys?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare const isLoopbackUrl: (value: string) => boolean;
|
|
13
|
+
export declare const assertLoopbackUrl: (label: string, value: string) => void;
|
|
14
|
+
export declare const pickEnvironmentVariables: (source: ProcessEnvironment, keys: readonly string[]) => Record<string, string>;
|
|
15
|
+
export declare const createCleanProcessEnvironment: ({ inheritedEnvironment, overrides, passthroughKeys, }?: CleanProcessEnvironmentOptions) => Record<string, string>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Build minimal child-process environments and validate loopback URLs for
|
|
3
|
+
* the project-neutral Rehearsal runtime. Do not run directly; this module is reusable
|
|
4
|
+
* script infrastructure.
|
|
5
|
+
*/
|
|
6
|
+
const SAFE_SYSTEM_ENVIRONMENT_KEYS = Object.freeze([
|
|
7
|
+
"CI",
|
|
8
|
+
"COLORTERM",
|
|
9
|
+
"FORCE_COLOR",
|
|
10
|
+
"HOME",
|
|
11
|
+
"LANG",
|
|
12
|
+
"LANGUAGE",
|
|
13
|
+
"LC_ALL",
|
|
14
|
+
"LOGNAME",
|
|
15
|
+
"NO_COLOR",
|
|
16
|
+
"PATH",
|
|
17
|
+
"SHELL",
|
|
18
|
+
"TERM",
|
|
19
|
+
"TMP",
|
|
20
|
+
"TMPDIR",
|
|
21
|
+
"TEMP",
|
|
22
|
+
"USER",
|
|
23
|
+
]);
|
|
24
|
+
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
|
|
25
|
+
export const isLoopbackUrl = (value) => {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(value);
|
|
28
|
+
return (["http:", "https:"].includes(parsed.protocol) &&
|
|
29
|
+
LOOPBACK_HOSTNAMES.has(parsed.hostname));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export const assertLoopbackUrl = (label, value) => {
|
|
36
|
+
if (!value || !isLoopbackUrl(value)) {
|
|
37
|
+
throw new Error(`${label} must use a loopback URL; received a non-local target.`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const pickEnvironmentVariables = (source, keys) => Object.fromEntries(keys.flatMap((key) => {
|
|
41
|
+
const value = source[key];
|
|
42
|
+
return value === undefined ? [] : [[key, value]];
|
|
43
|
+
}));
|
|
44
|
+
export const createCleanProcessEnvironment = ({ inheritedEnvironment = process.env, overrides = {}, passthroughKeys = [], } = {}) => ({
|
|
45
|
+
...pickEnvironmentVariables(inheritedEnvironment, [
|
|
46
|
+
...SAFE_SYSTEM_ENVIRONMENT_KEYS,
|
|
47
|
+
...passthroughKeys,
|
|
48
|
+
]),
|
|
49
|
+
...pickEnvironmentVariables(overrides, Object.keys(overrides)),
|
|
50
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and digest the separately authorized source-access boundary.
|
|
3
|
+
* Plans contain no credentials or raw owner identifiers.
|
|
4
|
+
*/
|
|
5
|
+
export type SourceReader = Readonly<{
|
|
6
|
+
mode: "managed";
|
|
7
|
+
role: string;
|
|
8
|
+
ownerRole: string;
|
|
9
|
+
credentialFile: string;
|
|
10
|
+
validForMinutes: number;
|
|
11
|
+
}> | Readonly<{
|
|
12
|
+
mode: "external";
|
|
13
|
+
role: string;
|
|
14
|
+
allowedMemberships: readonly string[];
|
|
15
|
+
connectionEnvironmentVariable: string;
|
|
16
|
+
credentialFile: string;
|
|
17
|
+
maximumValidForMinutes: number;
|
|
18
|
+
}>;
|
|
19
|
+
export type SourceRowScope = Readonly<{
|
|
20
|
+
kind: "approved-public";
|
|
21
|
+
}> | Readonly<{
|
|
22
|
+
kind: "approved-owner";
|
|
23
|
+
column: string;
|
|
24
|
+
valueEnvironmentVariable: string;
|
|
25
|
+
}>;
|
|
26
|
+
export interface SourceRelation {
|
|
27
|
+
readonly source: Readonly<{
|
|
28
|
+
schema: string;
|
|
29
|
+
table: string;
|
|
30
|
+
}>;
|
|
31
|
+
readonly view: string;
|
|
32
|
+
readonly targetSchema: string;
|
|
33
|
+
readonly targetTable: string;
|
|
34
|
+
readonly columns: readonly string[];
|
|
35
|
+
readonly orderBy: readonly string[];
|
|
36
|
+
readonly rowScope: SourceRowScope;
|
|
37
|
+
readonly viewDefinitionSha256?: string;
|
|
38
|
+
}
|
|
39
|
+
export type SourceAsset = Readonly<{
|
|
40
|
+
bucket: string;
|
|
41
|
+
prefix: string;
|
|
42
|
+
rights: "approved-public" | "approved-owner";
|
|
43
|
+
}> | Readonly<{
|
|
44
|
+
bucket: string;
|
|
45
|
+
prefixEnvironmentVariable: string;
|
|
46
|
+
rights: "approved-owner";
|
|
47
|
+
pathMapping: string;
|
|
48
|
+
}>;
|
|
49
|
+
export interface SourceAssetReader {
|
|
50
|
+
readonly endpointFingerprint: string;
|
|
51
|
+
readonly baseUrlEnvironmentVariable: string;
|
|
52
|
+
readonly tokenEnvironmentVariable: string;
|
|
53
|
+
readonly maximumObjects: number;
|
|
54
|
+
readonly maximumObjectBytes: number;
|
|
55
|
+
readonly maximumTotalBytes: number;
|
|
56
|
+
}
|
|
57
|
+
type SourceAssetReview = Extract<SourceAsset, {
|
|
58
|
+
prefix: string;
|
|
59
|
+
}> | Readonly<Extract<SourceAsset, {
|
|
60
|
+
prefixEnvironmentVariable: string;
|
|
61
|
+
}> & {
|
|
62
|
+
prefixValueSha256: string;
|
|
63
|
+
}>;
|
|
64
|
+
export interface SourceAccessPolicy {
|
|
65
|
+
readonly accessVersion: 1;
|
|
66
|
+
readonly targetFingerprint: string;
|
|
67
|
+
readonly administratorEnvironmentVariable?: string;
|
|
68
|
+
readonly reader: SourceReader;
|
|
69
|
+
readonly exportSchema: string;
|
|
70
|
+
readonly migrationLedger: Readonly<{
|
|
71
|
+
schema: string;
|
|
72
|
+
table: string;
|
|
73
|
+
versionColumn: string;
|
|
74
|
+
nameColumn: string;
|
|
75
|
+
statementsColumn: string;
|
|
76
|
+
}>;
|
|
77
|
+
readonly assetReader: Readonly<SourceAssetReader> | null;
|
|
78
|
+
readonly relations: readonly Readonly<SourceRelation>[];
|
|
79
|
+
readonly assets: readonly SourceAsset[];
|
|
80
|
+
}
|
|
81
|
+
export interface SourceAccessPlan {
|
|
82
|
+
readonly policy: Readonly<SourceAccessPolicy>;
|
|
83
|
+
readonly review: Readonly<{
|
|
84
|
+
planVersion: 1;
|
|
85
|
+
operation: "prepare-source-access";
|
|
86
|
+
targetFingerprint: string;
|
|
87
|
+
exportSchema: string;
|
|
88
|
+
reader: Readonly<{
|
|
89
|
+
mode: "managed";
|
|
90
|
+
role: string;
|
|
91
|
+
credentialFile: string;
|
|
92
|
+
ownerRole: string;
|
|
93
|
+
validForMinutes: number;
|
|
94
|
+
sourceChanges: true;
|
|
95
|
+
}> | Readonly<{
|
|
96
|
+
mode: "external";
|
|
97
|
+
role: string;
|
|
98
|
+
credentialFile: string;
|
|
99
|
+
connectionEnvironmentVariable: string;
|
|
100
|
+
allowedMemberships: readonly string[];
|
|
101
|
+
maximumValidForMinutes: number;
|
|
102
|
+
sourceChanges: false;
|
|
103
|
+
}>;
|
|
104
|
+
relations: readonly Readonly<{
|
|
105
|
+
source: Readonly<{
|
|
106
|
+
schema: string;
|
|
107
|
+
table: string;
|
|
108
|
+
}>;
|
|
109
|
+
view: string;
|
|
110
|
+
targetTable: string;
|
|
111
|
+
columns: readonly string[];
|
|
112
|
+
orderBy: readonly string[];
|
|
113
|
+
viewDefinitionSha256?: string;
|
|
114
|
+
rowScope: Readonly<{
|
|
115
|
+
kind: "approved-public";
|
|
116
|
+
}> | Readonly<{
|
|
117
|
+
kind: "approved-owner";
|
|
118
|
+
column: string;
|
|
119
|
+
valueSha256: string | undefined;
|
|
120
|
+
}>;
|
|
121
|
+
}>[];
|
|
122
|
+
assets: readonly SourceAssetReview[];
|
|
123
|
+
assetReader: Readonly<Record<string, unknown>> | null;
|
|
124
|
+
denyChecks: readonly string[];
|
|
125
|
+
}>;
|
|
126
|
+
readonly digest: string;
|
|
127
|
+
}
|
|
128
|
+
export interface SourceRetirementPlan {
|
|
129
|
+
readonly review: Readonly<{
|
|
130
|
+
planVersion: 1;
|
|
131
|
+
operation: "retire-source-access";
|
|
132
|
+
accessMode: "managed" | "external";
|
|
133
|
+
targetFingerprint: string;
|
|
134
|
+
preparationDigest: string;
|
|
135
|
+
exportSchema: string;
|
|
136
|
+
views: readonly string[];
|
|
137
|
+
relations: readonly Readonly<{
|
|
138
|
+
schema: string;
|
|
139
|
+
table: string;
|
|
140
|
+
columns: readonly string[];
|
|
141
|
+
}>[];
|
|
142
|
+
migrationLedger: Readonly<{
|
|
143
|
+
schema: string;
|
|
144
|
+
table: string;
|
|
145
|
+
}>;
|
|
146
|
+
readerRole: string;
|
|
147
|
+
ownerRole?: string;
|
|
148
|
+
}>;
|
|
149
|
+
readonly digest: string;
|
|
150
|
+
}
|
|
151
|
+
export declare const externalViewDefinitionFingerprint: (definition: string) => string;
|
|
152
|
+
export declare const sourceTargetFingerprint: (connectionString: string) => string;
|
|
153
|
+
export declare const validateSourceAccessPolicy: (policy: unknown) => Readonly<SourceAccessPolicy>;
|
|
154
|
+
export declare const createSourceAccessPlan: ({ policy, environment, }: {
|
|
155
|
+
policy: unknown;
|
|
156
|
+
environment?: NodeJS.ProcessEnv;
|
|
157
|
+
}) => Readonly<SourceAccessPlan>;
|
|
158
|
+
export declare const sourceAccessPolicyFingerprint: (policy: unknown) => string;
|
|
159
|
+
export declare const assetEndpointFingerprint: (value: string) => string;
|
|
160
|
+
export declare const assertSourceAccessConfirmation: <Plan extends SourceAccessPlan>(plan: Plan, confirmation: string) => Plan;
|
|
161
|
+
export declare const createSourceRetirementPlan: ({ receipt, targetFingerprint: target, }: {
|
|
162
|
+
receipt: unknown;
|
|
163
|
+
targetFingerprint: string;
|
|
164
|
+
}) => Readonly<SourceRetirementPlan>;
|
|
165
|
+
export {};
|