@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
|
@@ -1,207 +0,0 @@
|
|
|
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
|
-
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
|
-
import packageManifest from "../../package.json" with { type: "json" };
|
|
9
|
-
|
|
10
|
-
export const REHEARSAL_RESULT_VERSION = 1;
|
|
11
|
-
export const REHEARSAL_VERSION = packageManifest.version;
|
|
12
|
-
|
|
13
|
-
export const REHEARSAL_EXIT_CODES = Object.freeze({
|
|
14
|
-
success: 0,
|
|
15
|
-
configuration_invalid: 2,
|
|
16
|
-
unsafe_environment: 3,
|
|
17
|
-
baseline_invalid: 4,
|
|
18
|
-
baseline_checksum_mismatch: 5,
|
|
19
|
-
migration_candidate_failure: 6,
|
|
20
|
-
migration_verification_failure: 7,
|
|
21
|
-
application_proof_failure: 8,
|
|
22
|
-
runtime_dependency_failure: 9,
|
|
23
|
-
internal_failure: 10,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const SECRET_KEY_PATTERN =
|
|
27
|
-
/(?:authorization|cookie|credential|database[_-]?url|jwt|key|password|secret|service[_-]?role|token)/iu;
|
|
28
|
-
const JWT_PATTERN =
|
|
29
|
-
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu;
|
|
30
|
-
const CONNECTION_STRING_PATTERN =
|
|
31
|
-
/\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\/[^\s"']+/giu;
|
|
32
|
-
const BEARER_PATTERN = /\bBearer\s+[^\s"']+/giu;
|
|
33
|
-
const SUPABASE_KEY_PATTERN = /\bsb_(?:publishable|secret)_[A-Za-z0-9_-]+\b/gu;
|
|
34
|
-
|
|
35
|
-
const redactString = (value) =>
|
|
36
|
-
value
|
|
37
|
-
.replace(CONNECTION_STRING_PATTERN, "[REDACTED_CONNECTION_STRING]")
|
|
38
|
-
.replace(JWT_PATTERN, "[REDACTED_JWT]")
|
|
39
|
-
.replace(BEARER_PATTERN, "Bearer [REDACTED]")
|
|
40
|
-
.replace(SUPABASE_KEY_PATTERN, "[REDACTED_KEY]");
|
|
41
|
-
|
|
42
|
-
export const redactDiagnosticValue = (value, key = "") => {
|
|
43
|
-
if (SECRET_KEY_PATTERN.test(key)) return "[REDACTED]";
|
|
44
|
-
if (typeof value === "string") return redactString(value);
|
|
45
|
-
if (Array.isArray(value)) {
|
|
46
|
-
return value.map((entry) => redactDiagnosticValue(entry));
|
|
47
|
-
}
|
|
48
|
-
if (value && typeof value === "object") {
|
|
49
|
-
return Object.fromEntries(
|
|
50
|
-
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
51
|
-
entryKey,
|
|
52
|
-
redactDiagnosticValue(entryValue, entryKey),
|
|
53
|
-
]),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return value;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const diagnosticIdFor = ({ category, code, message }) =>
|
|
60
|
-
createHash("sha256")
|
|
61
|
-
.update(`${category}\0${code}\0${message}`)
|
|
62
|
-
.digest("hex")
|
|
63
|
-
.slice(0, 12);
|
|
64
|
-
|
|
65
|
-
export class RehearsalError extends Error {
|
|
66
|
-
constructor({
|
|
67
|
-
category,
|
|
68
|
-
code,
|
|
69
|
-
message,
|
|
70
|
-
expected,
|
|
71
|
-
actual,
|
|
72
|
-
context,
|
|
73
|
-
refused,
|
|
74
|
-
suggestions = [],
|
|
75
|
-
cause,
|
|
76
|
-
}) {
|
|
77
|
-
super(message, { cause });
|
|
78
|
-
this.name = "RehearsalError";
|
|
79
|
-
if (!Object.hasOwn(REHEARSAL_EXIT_CODES, category)) {
|
|
80
|
-
throw new Error(`Unknown Rehearsal error category: ${category}.`);
|
|
81
|
-
}
|
|
82
|
-
this.category = category;
|
|
83
|
-
this.code = code;
|
|
84
|
-
this.expected = expected;
|
|
85
|
-
this.actual = actual;
|
|
86
|
-
this.context = context;
|
|
87
|
-
this.refused = refused;
|
|
88
|
-
this.suggestions = suggestions;
|
|
89
|
-
this.diagnosticId = diagnosticIdFor({ category, code, message });
|
|
90
|
-
this.exitCode = REHEARSAL_EXIT_CODES[category];
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const inferCategory = (error) => {
|
|
95
|
-
const message = String(error?.message ?? error);
|
|
96
|
-
if (/checksum/iu.test(message)) return "baseline_checksum_mismatch";
|
|
97
|
-
if (
|
|
98
|
-
/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(
|
|
99
|
-
message,
|
|
100
|
-
)
|
|
101
|
-
) {
|
|
102
|
-
return "runtime_dependency_failure";
|
|
103
|
-
}
|
|
104
|
-
if (/candidate|migration history diverges/iu.test(message)) {
|
|
105
|
-
return "migration_candidate_failure";
|
|
106
|
-
}
|
|
107
|
-
if (/baseline|artifact|generation|sanitization policy/iu.test(message)) {
|
|
108
|
-
return "baseline_invalid";
|
|
109
|
-
}
|
|
110
|
-
if (/migration|ledger|replay/iu.test(message)) {
|
|
111
|
-
return "migration_verification_failure";
|
|
112
|
-
}
|
|
113
|
-
if (/config|unknown rehearsal|unsupported rehearsal/iu.test(message)) {
|
|
114
|
-
return "configuration_invalid";
|
|
115
|
-
}
|
|
116
|
-
if (/loopback|hosted|unsafe|credential|outbound/iu.test(message)) {
|
|
117
|
-
return "unsafe_environment";
|
|
118
|
-
}
|
|
119
|
-
if (
|
|
120
|
-
/docker|postgres(?:ql)?|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(
|
|
121
|
-
message,
|
|
122
|
-
)
|
|
123
|
-
) {
|
|
124
|
-
return "runtime_dependency_failure";
|
|
125
|
-
}
|
|
126
|
-
return "internal_failure";
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export const normalizeRehearsalError = (error, overrides = {}) => {
|
|
130
|
-
if (error instanceof RehearsalError) return error;
|
|
131
|
-
const message = String(error?.message ?? error);
|
|
132
|
-
const category = overrides.category ?? inferCategory(error);
|
|
133
|
-
return new RehearsalError({
|
|
134
|
-
category,
|
|
135
|
-
code: overrides.code ?? category.toUpperCase(),
|
|
136
|
-
message,
|
|
137
|
-
expected: overrides.expected,
|
|
138
|
-
actual: overrides.actual,
|
|
139
|
-
context: overrides.context,
|
|
140
|
-
refused:
|
|
141
|
-
overrides.refused ??
|
|
142
|
-
"Rehearsal stopped before performing the requested operation.",
|
|
143
|
-
suggestions: overrides.suggestions ?? [],
|
|
144
|
-
cause: error,
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export const createRehearsalResult = ({
|
|
149
|
-
command,
|
|
150
|
-
status,
|
|
151
|
-
data,
|
|
152
|
-
warnings = [],
|
|
153
|
-
startedAt = new Date(),
|
|
154
|
-
durationMs = 0,
|
|
155
|
-
}) =>
|
|
156
|
-
redactDiagnosticValue({
|
|
157
|
-
schemaVersion: REHEARSAL_RESULT_VERSION,
|
|
158
|
-
rehearsalVersion: REHEARSAL_VERSION,
|
|
159
|
-
command,
|
|
160
|
-
status,
|
|
161
|
-
startedAt: startedAt.toISOString(),
|
|
162
|
-
durationMs,
|
|
163
|
-
warnings,
|
|
164
|
-
data,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
export const serializeRehearsalError = (error, { debug = false } = {}) => {
|
|
168
|
-
const failure = normalizeRehearsalError(error);
|
|
169
|
-
return redactDiagnosticValue({
|
|
170
|
-
schemaVersion: REHEARSAL_RESULT_VERSION,
|
|
171
|
-
rehearsalVersion: REHEARSAL_VERSION,
|
|
172
|
-
status: "error",
|
|
173
|
-
error: {
|
|
174
|
-
category: failure.category,
|
|
175
|
-
code: failure.code,
|
|
176
|
-
message: failure.message,
|
|
177
|
-
expected: failure.expected,
|
|
178
|
-
actual: failure.actual,
|
|
179
|
-
context: failure.context,
|
|
180
|
-
refused: failure.refused,
|
|
181
|
-
suggestions: failure.suggestions,
|
|
182
|
-
diagnosticId: failure.diagnosticId,
|
|
183
|
-
...(debug && failure.cause
|
|
184
|
-
? { cause: String(failure.cause?.message ?? failure.cause) }
|
|
185
|
-
: {}),
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const formatValue = (value) =>
|
|
191
|
-
typeof value === "string" ? value : JSON.stringify(value);
|
|
192
|
-
|
|
193
|
-
export const renderHumanError = (error, { debug = false } = {}) => {
|
|
194
|
-
const { error: failure } = serializeRehearsalError(error, { debug });
|
|
195
|
-
return [
|
|
196
|
-
`Rehearsal stopped: ${failure.message}`,
|
|
197
|
-
failure.expected ? `Expected: ${formatValue(failure.expected)}` : null,
|
|
198
|
-
failure.actual ? `Actual: ${formatValue(failure.actual)}` : null,
|
|
199
|
-
failure.context ? `Context: ${formatValue(failure.context)}` : null,
|
|
200
|
-
failure.refused ? `Refused: ${failure.refused}` : null,
|
|
201
|
-
...(failure.suggestions ?? []).map((value) => `Try: ${value}`),
|
|
202
|
-
`Diagnostic: ${failure.diagnosticId} (${failure.category})`,
|
|
203
|
-
debug && failure.cause ? `Cause: ${failure.cause}` : null,
|
|
204
|
-
]
|
|
205
|
-
.filter(Boolean)
|
|
206
|
-
.join("\n");
|
|
207
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supported Node.js release lines. Keep this aligned with package.json and the
|
|
3
|
-
* verification matrix.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const REHEARSAL_NODE_MAJORS = Object.freeze([22, 24, 26]);
|
|
7
|
-
export const REHEARSAL_NODE_LABEL = "Node.js 22, 24, or 26";
|
|
8
|
-
export const REHEARSAL_RECOMMENDED_NODE_MAJOR = 24;
|
|
9
|
-
|
|
10
|
-
export const inspectRehearsalNodeRuntime = (
|
|
11
|
-
version = process.versions.node,
|
|
12
|
-
) => {
|
|
13
|
-
const major = Number.parseInt(String(version).split(".")[0], 10);
|
|
14
|
-
return {
|
|
15
|
-
version: String(version),
|
|
16
|
-
major,
|
|
17
|
-
supported: REHEARSAL_NODE_MAJORS.includes(major),
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const assertSupportedRehearsalNodeRuntime = (
|
|
22
|
-
version = process.versions.node,
|
|
23
|
-
) => {
|
|
24
|
-
const runtime = inspectRehearsalNodeRuntime(version);
|
|
25
|
-
if (!runtime.supported) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`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.`,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return runtime;
|
|
31
|
-
};
|
|
@@ -1,162 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
|
|
7
|
-
const SAFE_SYSTEM_ENVIRONMENT_KEYS = Object.freeze([
|
|
8
|
-
"CI",
|
|
9
|
-
"COLORTERM",
|
|
10
|
-
"FORCE_COLOR",
|
|
11
|
-
"HOME",
|
|
12
|
-
"LANG",
|
|
13
|
-
"LANGUAGE",
|
|
14
|
-
"LC_ALL",
|
|
15
|
-
"LOGNAME",
|
|
16
|
-
"NO_COLOR",
|
|
17
|
-
"PATH",
|
|
18
|
-
"SHELL",
|
|
19
|
-
"TERM",
|
|
20
|
-
"TMP",
|
|
21
|
-
"TMPDIR",
|
|
22
|
-
"TEMP",
|
|
23
|
-
"USER",
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
27
|
-
|
|
28
|
-
export const isLoopbackUrl = (value) => {
|
|
29
|
-
try {
|
|
30
|
-
const parsed = new URL(value);
|
|
31
|
-
return (
|
|
32
|
-
["http:", "https:"].includes(parsed.protocol) &&
|
|
33
|
-
LOOPBACK_HOSTNAMES.has(parsed.hostname)
|
|
34
|
-
);
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const assertLoopbackUrl = (label, value) => {
|
|
41
|
-
if (!value || !isLoopbackUrl(value)) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`${label} must use a loopback URL; received a non-local target.`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const pickEnvironmentVariables = (source, keys) =>
|
|
49
|
-
Object.fromEntries(
|
|
50
|
-
keys.flatMap((key) => {
|
|
51
|
-
const value = source[key];
|
|
52
|
-
return value === undefined ? [] : [[key, value]];
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
export const createCleanProcessEnvironment = ({
|
|
57
|
-
inheritedEnvironment = process.env,
|
|
58
|
-
overrides = {},
|
|
59
|
-
passthroughKeys = [],
|
|
60
|
-
} = {}) => ({
|
|
61
|
-
...pickEnvironmentVariables(inheritedEnvironment, [
|
|
62
|
-
...SAFE_SYSTEM_ENVIRONMENT_KEYS,
|
|
63
|
-
...passthroughKeys,
|
|
64
|
-
]),
|
|
65
|
-
...Object.fromEntries(
|
|
66
|
-
Object.entries(overrides).filter(([, value]) => value !== undefined),
|
|
67
|
-
),
|
|
68
|
-
});
|