@rehearsal-db/core 0.1.0-beta.14 → 0.1.0-beta.16
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 +51 -1
- package/README.md +1 -0
- package/dist/package.json +101 -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_contract.d.mts +147 -0
- package/dist/src/baseline/privacy_contract.mjs +2 -0
- package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
- package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
- package/dist/src/baseline/privacy_engine.d.mts +5 -0
- package/dist/src/baseline/privacy_engine.mjs +4 -0
- package/dist/src/baseline/privacy_key.d.mts +7 -0
- package/dist/src/baseline/privacy_key.mjs +42 -0
- package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
- package/dist/src/baseline/privacy_pipeline.mjs +127 -0
- package/dist/src/baseline/privacy_policy.d.mts +6 -0
- package/dist/src/baseline/privacy_policy.mjs +237 -0
- package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
- package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
- package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
- package/dist/src/baseline/privacy_rule_registry.mjs +143 -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/privacy_transforms.d.mts +18 -0
- package/dist/src/baseline/privacy_transforms.mjs +142 -0
- package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
- package/dist/src/baseline/privacy_validation_shared.mjs +127 -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/application_command_handlers.d.mts +8 -0
- package/dist/src/cli/application_command_handlers.mjs +52 -0
- package/dist/src/cli/arguments.d.mts +38 -0
- package/dist/src/cli/arguments.mjs +128 -0
- package/dist/src/cli/basic_command_handlers.d.mts +4 -0
- package/dist/src/cli/basic_command_handlers.mjs +56 -0
- package/dist/src/cli/cli_commands.d.mts +14 -0
- package/dist/src/cli/cli_commands.mjs +32 -0
- package/dist/src/cli/command_contract.d.mts +57 -0
- package/dist/src/cli/command_contract.mjs +39 -0
- package/dist/src/cli/command_output.d.mts +12 -0
- package/dist/src/cli/command_output.mjs +20 -0
- package/dist/src/cli/command_registry.d.mts +7 -0
- package/dist/src/cli/command_registry.mjs +14 -0
- package/dist/src/cli/guided.d.mts +53 -0
- package/dist/src/cli/guided.mjs +719 -0
- package/dist/src/cli/identity_command_handlers.d.mts +8 -0
- package/dist/src/cli/identity_command_handlers.mjs +43 -0
- package/dist/src/cli/init_command.d.mts +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
- package/dist/src/cli/inspection_command_handlers.mjs +79 -0
- package/dist/src/cli/project_command_handlers.d.mts +11 -0
- package/dist/src/cli/project_command_handlers.mjs +92 -0
- package/dist/src/cli/project_commands.d.mts +159 -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 +190 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
- package/dist/src/cli/runtime_action_handlers.mjs +201 -0
- package/dist/src/cli/runtime_commands.d.mts +269 -0
- package/dist/src/cli/runtime_commands.mjs +601 -0
- package/dist/src/cli/source_command_handlers.d.mts +8 -0
- package/dist/src/cli/source_command_handlers.mjs +62 -0
- package/dist/src/cli/source_commands.d.mts +436 -0
- package/dist/src/cli/source_commands.mjs +301 -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 +5 -0
- package/dist/src/identity/claim.mjs +4 -0
- package/dist/src/identity/claim_contract.d.mts +135 -0
- package/dist/src/identity/claim_contract.mjs +6 -0
- package/dist/src/identity/claim_executor.d.mts +10 -0
- package/dist/src/identity/claim_executor.mjs +358 -0
- package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
- package/dist/src/identity/claim_identity_lookup.mjs +125 -0
- package/dist/src/identity/claim_plan.d.mts +8 -0
- package/dist/src/identity/claim_plan.mjs +61 -0
- package/dist/src/identity/claim_policy.d.mts +3 -0
- package/dist/src/identity/claim_policy.mjs +377 -0
- package/dist/src/identity/claim_sql.d.mts +10 -0
- package/dist/src/identity/claim_sql.mjs +46 -0
- package/dist/src/identity/claim_verification.d.mts +13 -0
- package/dist/src/identity/claim_verification.mjs +35 -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 +13 -0
- package/dist/src/project/configuration.mjs +9 -0
- package/dist/src/project/configuration_contract.d.mts +125 -0
- package/dist/src/project/configuration_contract.mjs +27 -0
- package/dist/src/project/configuration_discovery.d.mts +7 -0
- package/dist/src/project/configuration_discovery.mjs +37 -0
- package/dist/src/project/configuration_normalization.d.mts +107 -0
- package/dist/src/project/configuration_normalization.mjs +302 -0
- package/dist/src/project/configuration_rendering.d.mts +14 -0
- package/dist/src/project/configuration_rendering.mjs +224 -0
- package/dist/src/project/configuration_resolution.d.mts +125 -0
- package/dist/src/project/configuration_resolution.mjs +81 -0
- package/dist/src/project/configuration_schema.d.mts +22 -0
- package/dist/src/project/configuration_schema.mjs +160 -0
- package/dist/src/project/configuration_validation.d.mts +22 -0
- package/dist/src/project/configuration_validation.mjs +136 -0
- package/dist/src/project/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/project_detection.d.mts +14 -0
- package/dist/src/project/project_detection.mjs +47 -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/lifecycle_engine.d.mts +63 -0
- package/dist/src/runtime/lifecycle_engine.mjs +114 -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 +637 -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 +620 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +627 -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 +133 -10
- package/docs/configuration.md +9 -3
- package/docs/index.md +1 -0
- package/docs/releasing.md +8 -3
- package/docs/typescript.md +134 -0
- package/package.json +36 -29
- package/types/configuration.d.mts +5 -0
- package/scripts/install/create_root_config.mjs +0 -19
- 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 -1399
- package/src/baseline/privacy_structured_json.mjs +0 -554
- 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 -972
- package/src/cli/renderers.mjs +0 -638
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -355
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1165
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.d.mts +0 -164
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -325
- package/src/project/setup.mjs +0 -474
- 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 -798
- package/src/source/asset_transfer.mjs +0 -259
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -368
- package/src/source/postgresql_access.mjs +0 -638
- 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/source/baseline.mjs
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stream an approved PostgreSQL export surface into a staged, verified baseline
|
|
3
|
-
* using one coherent read-only snapshot. No raw row values are logged.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import { mkdir, statfs } from "node:fs/promises";
|
|
8
|
-
import { dirname } from "node:path";
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
10
|
-
import pg from "pg";
|
|
11
|
-
import {
|
|
12
|
-
activateBaselineGeneration,
|
|
13
|
-
createAndActivateBaseline,
|
|
14
|
-
createBaselineGenerationId,
|
|
15
|
-
removeStagedBaselineGeneration,
|
|
16
|
-
} from "../baseline/artifact.mjs";
|
|
17
|
-
import {
|
|
18
|
-
buildMigrationLedgerInventory,
|
|
19
|
-
createMigrationReplayReceipt,
|
|
20
|
-
readMigrationSourceBundle,
|
|
21
|
-
} from "../runtime/migration_history.mjs";
|
|
22
|
-
import {
|
|
23
|
-
createPrivacyEngine,
|
|
24
|
-
validateExecutablePrivacyPolicy,
|
|
25
|
-
} from "../baseline/privacy_engine.mjs";
|
|
26
|
-
import {
|
|
27
|
-
sourceAccessPolicyFingerprint,
|
|
28
|
-
sourceTargetFingerprint,
|
|
29
|
-
validateSourceAccessPolicy,
|
|
30
|
-
} from "./access.mjs";
|
|
31
|
-
|
|
32
|
-
const { Client } = pg;
|
|
33
|
-
const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
34
|
-
const DEFAULT_LIMITS = Object.freeze({
|
|
35
|
-
batchRows: 500,
|
|
36
|
-
maximumRows: 1_000_000,
|
|
37
|
-
maximumBytes: 2 * 1024 * 1024 * 1024,
|
|
38
|
-
diskHeadroomBytes: 64 * 1024 * 1024,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const quoteIdentifier = (value) => {
|
|
42
|
-
if (!IDENTIFIER.test(value ?? ""))
|
|
43
|
-
throw new Error("Unsafe source extraction identifier.");
|
|
44
|
-
return `"${value}"`;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const canonicalJson = (value) => `${JSON.stringify(value, null, "\t")}\n`;
|
|
48
|
-
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
49
|
-
const parseEstimateValue = (value) => {
|
|
50
|
-
if (
|
|
51
|
-
(typeof value !== "string" && typeof value !== "number") ||
|
|
52
|
-
(typeof value === "string" && !/^(?:0|[1-9]\d*)$/u.test(value))
|
|
53
|
-
) {
|
|
54
|
-
return Number.NaN;
|
|
55
|
-
}
|
|
56
|
-
return Number(value);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const normalizeLimits = (limits = {}) => {
|
|
60
|
-
const normalized = { ...DEFAULT_LIMITS, ...limits };
|
|
61
|
-
for (const [name, value] of Object.entries(normalized)) {
|
|
62
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Source extraction limit ${name} must be a positive integer.`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (normalized.batchRows > 10_000) {
|
|
69
|
-
throw new Error("Source extraction batchRows may not exceed 10000.");
|
|
70
|
-
}
|
|
71
|
-
return Object.freeze(normalized);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const connect = async (connectionString, clientFactory) => {
|
|
75
|
-
const client = clientFactory
|
|
76
|
-
? await clientFactory(connectionString)
|
|
77
|
-
: new Client({
|
|
78
|
-
connectionString,
|
|
79
|
-
application_name: "rehearsal-baseline-refresh",
|
|
80
|
-
});
|
|
81
|
-
await client.connect?.();
|
|
82
|
-
return client;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const readLedger = async (client, declaration) => {
|
|
86
|
-
const result = await client.query(
|
|
87
|
-
`select
|
|
88
|
-
${quoteIdentifier(declaration.versionColumn)} as version,
|
|
89
|
-
${quoteIdentifier(declaration.nameColumn)} as name,
|
|
90
|
-
${quoteIdentifier(declaration.statementsColumn)} as statements
|
|
91
|
-
from ${quoteIdentifier(declaration.schema)}.${quoteIdentifier(declaration.table)}
|
|
92
|
-
order by ${quoteIdentifier(declaration.versionColumn)}`,
|
|
93
|
-
);
|
|
94
|
-
return buildMigrationLedgerInventory(
|
|
95
|
-
result.rows,
|
|
96
|
-
"Approved source migration ledger",
|
|
97
|
-
);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const inspectExportSchema = async (client, policy) => {
|
|
101
|
-
const tables = [];
|
|
102
|
-
for (const relation of policy.relations) {
|
|
103
|
-
const result = await client.query(
|
|
104
|
-
`select column_name as name, data_type, udt_name, is_nullable,
|
|
105
|
-
is_generated, is_identity, ordinal_position
|
|
106
|
-
from information_schema.columns
|
|
107
|
-
where table_schema = $1 and table_name = $2
|
|
108
|
-
order by ordinal_position`,
|
|
109
|
-
[policy.exportSchema, relation.view],
|
|
110
|
-
);
|
|
111
|
-
const actualNames = result.rows.map((row) => row.name);
|
|
112
|
-
if (actualNames.join("\0") !== relation.columns.join("\0")) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`Approved export view ${relation.view} no longer has the reviewed columns.`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
tables.push({
|
|
118
|
-
schema: relation.targetSchema,
|
|
119
|
-
name: relation.targetTable,
|
|
120
|
-
sourceView: relation.view,
|
|
121
|
-
columns: result.rows.map((row) => ({
|
|
122
|
-
name: row.name,
|
|
123
|
-
dataType: row.data_type,
|
|
124
|
-
underlyingType: row.udt_name,
|
|
125
|
-
nullable: row.is_nullable === "YES",
|
|
126
|
-
generated: row.is_generated,
|
|
127
|
-
identity: row.is_identity,
|
|
128
|
-
ordinal: Number(row.ordinal_position),
|
|
129
|
-
})),
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
return Object.freeze(tables);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const estimateExport = async (client, policy) => {
|
|
136
|
-
const tables = [];
|
|
137
|
-
let rows = 0;
|
|
138
|
-
let bytes = 0;
|
|
139
|
-
for (const relation of policy.relations) {
|
|
140
|
-
const result = await client.query(
|
|
141
|
-
`select count(*)::bigint as rows,
|
|
142
|
-
coalesce(sum(pg_column_size(export_row)), 0)::bigint as bytes
|
|
143
|
-
from ${quoteIdentifier(policy.exportSchema)}.${quoteIdentifier(relation.view)} export_row`,
|
|
144
|
-
);
|
|
145
|
-
const estimate = result.rows?.[0];
|
|
146
|
-
const tableRows = parseEstimateValue(estimate?.rows);
|
|
147
|
-
const tableBytes = parseEstimateValue(estimate?.bytes);
|
|
148
|
-
if (
|
|
149
|
-
!Number.isSafeInteger(tableRows) ||
|
|
150
|
-
!Number.isSafeInteger(tableBytes) ||
|
|
151
|
-
tableRows < 0 ||
|
|
152
|
-
tableBytes < 0 ||
|
|
153
|
-
!Number.isSafeInteger(rows + tableRows) ||
|
|
154
|
-
!Number.isSafeInteger(bytes + tableBytes)
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
"Source export estimate is missing, negative, or exceeds safe numeric limits.",
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
rows += tableRows;
|
|
161
|
-
bytes += tableBytes;
|
|
162
|
-
tables.push({
|
|
163
|
-
schema: relation.targetSchema,
|
|
164
|
-
name: relation.targetTable,
|
|
165
|
-
rows: tableRows,
|
|
166
|
-
bytes: tableBytes,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
return Object.freeze({ rows, bytes, tables: Object.freeze(tables) });
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const assertCapacity = async ({
|
|
173
|
-
artifactRoot,
|
|
174
|
-
estimate,
|
|
175
|
-
limits,
|
|
176
|
-
availableBytes,
|
|
177
|
-
}) => {
|
|
178
|
-
if (estimate.rows > limits.maximumRows) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`Approved source contains ${estimate.rows} rows; the configured limit is ${limits.maximumRows}.`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (estimate.bytes > limits.maximumBytes) {
|
|
184
|
-
throw new Error(
|
|
185
|
-
"Approved source estimate exceeds the configured byte limit.",
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
await mkdir(dirname(artifactRoot), { recursive: true, mode: 0o700 });
|
|
189
|
-
if (!Number.isSafeInteger(availableBytes) || availableBytes < 0) {
|
|
190
|
-
throw new Error("Available baseline disk capacity could not be measured.");
|
|
191
|
-
}
|
|
192
|
-
const available = availableBytes;
|
|
193
|
-
const required = estimate.bytes * 2 + limits.diskHeadroomBytes;
|
|
194
|
-
if (available < required) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
`Baseline refresh needs approximately ${required} free bytes but only ${available} are available.`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const actualAvailableBytes = async (artifactRoot) => {
|
|
202
|
-
const stats = await statfs(dirname(artifactRoot));
|
|
203
|
-
return Number(stats.bavail) * Number(stats.bsize);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const assertPolicyCoverage = (privacyPolicy, sourceSchema) => {
|
|
207
|
-
const selected = new Map(
|
|
208
|
-
sourceSchema.map((table) => [
|
|
209
|
-
`${table.schema}.${table.name}`,
|
|
210
|
-
table.columns.map((column) => column.name),
|
|
211
|
-
]),
|
|
212
|
-
);
|
|
213
|
-
const classified = new Map(
|
|
214
|
-
privacyPolicy.tables.map((table) => [
|
|
215
|
-
`${table.schema ?? "public"}.${table.name}`,
|
|
216
|
-
table.columns.map((column) => column.name),
|
|
217
|
-
]),
|
|
218
|
-
);
|
|
219
|
-
if (selected.size !== classified.size) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
"Privacy policy table coverage does not match the approved source scope.",
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
for (const [table, columns] of selected) {
|
|
225
|
-
const decisions = classified.get(table);
|
|
226
|
-
if (
|
|
227
|
-
!decisions ||
|
|
228
|
-
columns.slice().sort().join("\0") !== decisions.slice().sort().join("\0")
|
|
229
|
-
) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`Privacy policy coverage changed for source table ${table}.`,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const streamSanitizedRecords = async function* ({
|
|
238
|
-
client,
|
|
239
|
-
sourcePolicy,
|
|
240
|
-
privacyEngine,
|
|
241
|
-
limits,
|
|
242
|
-
progress,
|
|
243
|
-
}) {
|
|
244
|
-
let totalRows = 0;
|
|
245
|
-
let totalBytes = 0;
|
|
246
|
-
for (const [index, relation] of sourcePolicy.relations.entries()) {
|
|
247
|
-
const cursor = `rehearsal_export_${index}`;
|
|
248
|
-
const columns = relation.columns.map(quoteIdentifier).join(", ");
|
|
249
|
-
const orderBy = relation.orderBy.map(quoteIdentifier).join(", ");
|
|
250
|
-
await client.query(
|
|
251
|
-
`declare ${quoteIdentifier(cursor)} no scroll cursor for
|
|
252
|
-
select ${columns}
|
|
253
|
-
from ${quoteIdentifier(sourcePolicy.exportSchema)}.${quoteIdentifier(relation.view)}
|
|
254
|
-
order by ${orderBy}`,
|
|
255
|
-
);
|
|
256
|
-
try {
|
|
257
|
-
while (true) {
|
|
258
|
-
const batch = await client.query(
|
|
259
|
-
`fetch forward ${limits.batchRows} from ${quoteIdentifier(cursor)}`,
|
|
260
|
-
);
|
|
261
|
-
if (
|
|
262
|
-
!Array.isArray(batch?.rows) ||
|
|
263
|
-
batch.rows.length > limits.batchRows
|
|
264
|
-
) {
|
|
265
|
-
throw new Error(
|
|
266
|
-
"Source reader returned a malformed or oversized cursor batch.",
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (batch.rows.length === 0) break;
|
|
270
|
-
for (const row of batch.rows) {
|
|
271
|
-
const sanitized = privacyEngine.sanitize({
|
|
272
|
-
schema: relation.targetSchema,
|
|
273
|
-
table: relation.targetTable,
|
|
274
|
-
row,
|
|
275
|
-
});
|
|
276
|
-
if (!sanitized) continue;
|
|
277
|
-
totalRows += 1;
|
|
278
|
-
totalBytes += Buffer.byteLength(JSON.stringify(sanitized));
|
|
279
|
-
if (
|
|
280
|
-
totalRows > limits.maximumRows ||
|
|
281
|
-
totalBytes > limits.maximumBytes
|
|
282
|
-
) {
|
|
283
|
-
throw new Error(
|
|
284
|
-
"Source stream exceeded its reviewed extraction boundary.",
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
yield sanitized;
|
|
288
|
-
}
|
|
289
|
-
progress?.({
|
|
290
|
-
table: relation.targetTable,
|
|
291
|
-
rows: totalRows,
|
|
292
|
-
bytes: totalBytes,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
} finally {
|
|
296
|
-
await client
|
|
297
|
-
.query(`close ${quoteIdentifier(cursor)}`)
|
|
298
|
-
.catch(() => undefined);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
export const refreshPostgresqlBaseline = async ({
|
|
304
|
-
connectionString,
|
|
305
|
-
sourcePolicy: sourcePolicyInput,
|
|
306
|
-
sourceAccessReceipt,
|
|
307
|
-
privacyPolicyBytes,
|
|
308
|
-
privacyKey,
|
|
309
|
-
artifactRoot,
|
|
310
|
-
migrationDirectory,
|
|
311
|
-
limits: limitInput,
|
|
312
|
-
availableBytes,
|
|
313
|
-
clientFactory,
|
|
314
|
-
progress,
|
|
315
|
-
assets = [],
|
|
316
|
-
}) => {
|
|
317
|
-
const sourcePolicy = validateSourceAccessPolicy(sourcePolicyInput);
|
|
318
|
-
if (
|
|
319
|
-
sourceTargetFingerprint(connectionString) !== sourcePolicy.targetFingerprint
|
|
320
|
-
) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
"Source reader credential does not match the reviewed target.",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const expectedReceiptShape = {
|
|
326
|
-
accessMode: sourcePolicy.reader.mode,
|
|
327
|
-
exportSchema: sourcePolicy.exportSchema,
|
|
328
|
-
views: sourcePolicy.relations.map((relation) => relation.view).sort(),
|
|
329
|
-
relations: sourcePolicy.relations.map((relation) => ({
|
|
330
|
-
...relation.source,
|
|
331
|
-
columns: relation.columns,
|
|
332
|
-
})),
|
|
333
|
-
migrationLedger: sourcePolicy.migrationLedger,
|
|
334
|
-
readerRole: sourcePolicy.reader.role,
|
|
335
|
-
ownerRole: sourcePolicy.reader.ownerRole ?? null,
|
|
336
|
-
};
|
|
337
|
-
const actualReceiptShape = {
|
|
338
|
-
accessMode: sourceAccessReceipt?.accessMode ?? "managed",
|
|
339
|
-
exportSchema: sourceAccessReceipt?.exportSchema,
|
|
340
|
-
views: [...(sourceAccessReceipt?.views ?? [])].sort(),
|
|
341
|
-
relations: sourceAccessReceipt?.relations,
|
|
342
|
-
migrationLedger: sourceAccessReceipt?.migrationLedger,
|
|
343
|
-
readerRole: sourceAccessReceipt?.readerRole,
|
|
344
|
-
ownerRole: sourceAccessReceipt?.ownerRole ?? null,
|
|
345
|
-
};
|
|
346
|
-
const policyFingerprint = sourceAccessPolicyFingerprint(sourcePolicy);
|
|
347
|
-
if (
|
|
348
|
-
sourceAccessReceipt?.targetFingerprint !== sourcePolicy.targetFingerprint ||
|
|
349
|
-
!/^[a-f0-9]{64}$/u.test(sourceAccessReceipt?.planDigest ?? "") ||
|
|
350
|
-
(sourcePolicy.reader.mode === "external" &&
|
|
351
|
-
sourceAccessReceipt?.policyFingerprint !== policyFingerprint) ||
|
|
352
|
-
(sourceAccessReceipt?.policyFingerprint !== undefined &&
|
|
353
|
-
sourceAccessReceipt.policyFingerprint !== policyFingerprint) ||
|
|
354
|
-
new Date(sourceAccessReceipt.expiresAt).valueOf() <= Date.now() ||
|
|
355
|
-
JSON.stringify(actualReceiptShape) !== JSON.stringify(expectedReceiptShape)
|
|
356
|
-
) {
|
|
357
|
-
throw new Error(
|
|
358
|
-
"Source reader receipt is missing, mismatched, or expired.",
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
const privacyPolicy = validateExecutablePrivacyPolicy(
|
|
362
|
-
JSON.parse(Buffer.from(privacyPolicyBytes).toString("utf8")),
|
|
363
|
-
);
|
|
364
|
-
const privacyEngine = createPrivacyEngine({
|
|
365
|
-
policy: privacyPolicy,
|
|
366
|
-
key: privacyKey,
|
|
367
|
-
});
|
|
368
|
-
const limits = normalizeLimits(limitInput);
|
|
369
|
-
const client = await connect(connectionString, clientFactory);
|
|
370
|
-
let generationId;
|
|
371
|
-
let transactionOpen = false;
|
|
372
|
-
try {
|
|
373
|
-
await client.query(
|
|
374
|
-
"begin transaction isolation level repeatable read read only",
|
|
375
|
-
);
|
|
376
|
-
transactionOpen = true;
|
|
377
|
-
const [sourceLedger, sourceSchema] = await Promise.all([
|
|
378
|
-
readLedger(client, sourcePolicy.migrationLedger),
|
|
379
|
-
inspectExportSchema(client, sourcePolicy),
|
|
380
|
-
]);
|
|
381
|
-
assertPolicyCoverage(privacyPolicy, sourceSchema);
|
|
382
|
-
if (privacyPolicy.migrationCutoff !== sourceLedger.at(-1).version) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
"Privacy policy cutoff does not match the coherent source ledger.",
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
const migrationFiles = await readMigrationSourceBundle({
|
|
388
|
-
directory: new URL("./", pathToFileURL(`${migrationDirectory}/`)),
|
|
389
|
-
sourceLedger,
|
|
390
|
-
});
|
|
391
|
-
const migrationReceipt = createMigrationReplayReceipt({
|
|
392
|
-
files: migrationFiles,
|
|
393
|
-
ledger: sourceLedger,
|
|
394
|
-
});
|
|
395
|
-
const estimate = await estimateExport(client, sourcePolicy);
|
|
396
|
-
await assertCapacity({
|
|
397
|
-
artifactRoot,
|
|
398
|
-
estimate,
|
|
399
|
-
limits,
|
|
400
|
-
availableBytes:
|
|
401
|
-
availableBytes ?? (await actualAvailableBytes(artifactRoot)),
|
|
402
|
-
});
|
|
403
|
-
generationId = createBaselineGenerationId();
|
|
404
|
-
const schemaSha256 = sha256(canonicalJson(sourceSchema));
|
|
405
|
-
const policySha256 = sha256(privacyPolicyBytes);
|
|
406
|
-
const manifest = await createAndActivateBaseline({
|
|
407
|
-
artifactRoot,
|
|
408
|
-
generationId,
|
|
409
|
-
activate: false,
|
|
410
|
-
records: streamSanitizedRecords({
|
|
411
|
-
client,
|
|
412
|
-
sourcePolicy,
|
|
413
|
-
privacyEngine,
|
|
414
|
-
limits,
|
|
415
|
-
progress,
|
|
416
|
-
}),
|
|
417
|
-
metadata: {
|
|
418
|
-
migrationCutoff: sourceLedger.at(-1).version,
|
|
419
|
-
migrationHistorySha256: migrationReceipt.historySha256,
|
|
420
|
-
sanitizationPolicySha256: policySha256,
|
|
421
|
-
sourceMigrationHistory: sourceLedger,
|
|
422
|
-
sourceReceiptSha256: sha256(
|
|
423
|
-
canonicalJson({
|
|
424
|
-
targetFingerprint: sourceAccessReceipt.targetFingerprint,
|
|
425
|
-
planDigest: sourceAccessReceipt.planDigest,
|
|
426
|
-
policyFingerprint: sourceAccessReceipt.policyFingerprint ?? null,
|
|
427
|
-
expiresAt: sourceAccessReceipt.expiresAt,
|
|
428
|
-
}),
|
|
429
|
-
),
|
|
430
|
-
sourceSchemaSha256: schemaSha256,
|
|
431
|
-
privacyKeyFingerprint: privacyEngine.keyFingerprint,
|
|
432
|
-
extraction: {
|
|
433
|
-
rowsEstimated: estimate.rows,
|
|
434
|
-
bytesEstimated: estimate.bytes,
|
|
435
|
-
batchRows: limits.batchRows,
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
expectedTables: privacyPolicy.tables
|
|
439
|
-
.filter((table) => table.sourceRows !== "EXCLUDE")
|
|
440
|
-
.map((table) =>
|
|
441
|
-
(table.schema ?? "public") === "public"
|
|
442
|
-
? table.name
|
|
443
|
-
: `${table.schema}.${table.name}`,
|
|
444
|
-
),
|
|
445
|
-
migrationFiles,
|
|
446
|
-
assets,
|
|
447
|
-
});
|
|
448
|
-
const observer = await connect(connectionString, clientFactory);
|
|
449
|
-
try {
|
|
450
|
-
const [latestLedger, latestSchema] = await Promise.all([
|
|
451
|
-
readLedger(observer, sourcePolicy.migrationLedger),
|
|
452
|
-
inspectExportSchema(observer, sourcePolicy),
|
|
453
|
-
]);
|
|
454
|
-
if (
|
|
455
|
-
sha256(canonicalJson(latestLedger)) !==
|
|
456
|
-
sha256(canonicalJson(sourceLedger)) ||
|
|
457
|
-
sha256(canonicalJson(latestSchema)) !== schemaSha256
|
|
458
|
-
) {
|
|
459
|
-
throw new Error(
|
|
460
|
-
"Source schema or migration ledger changed during baseline preparation.",
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
} finally {
|
|
464
|
-
await observer.end?.();
|
|
465
|
-
}
|
|
466
|
-
await client.query("commit");
|
|
467
|
-
transactionOpen = false;
|
|
468
|
-
await activateBaselineGeneration({ artifactRoot, generationId });
|
|
469
|
-
return { generationId, manifest, estimate };
|
|
470
|
-
} catch (error) {
|
|
471
|
-
if (transactionOpen) await client.query("rollback").catch(() => undefined);
|
|
472
|
-
if (generationId) {
|
|
473
|
-
await removeStagedBaselineGeneration({
|
|
474
|
-
artifactRoot,
|
|
475
|
-
generationId,
|
|
476
|
-
}).catch(() => undefined);
|
|
477
|
-
}
|
|
478
|
-
throw error;
|
|
479
|
-
} finally {
|
|
480
|
-
await client.end?.();
|
|
481
|
-
}
|
|
482
|
-
};
|