@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.
Files changed (236) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -0
  3. package/dist/package.json +101 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  21. package/dist/src/baseline/privacy_contract.mjs +2 -0
  22. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  23. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  24. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  25. package/dist/src/baseline/privacy_engine.mjs +4 -0
  26. package/dist/src/baseline/privacy_key.d.mts +7 -0
  27. package/dist/src/baseline/privacy_key.mjs +42 -0
  28. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  29. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  30. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  31. package/dist/src/baseline/privacy_policy.mjs +237 -0
  32. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  33. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  34. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  35. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  36. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  37. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  38. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  39. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  40. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  41. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  42. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  43. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  44. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  45. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  46. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  47. package/dist/src/cli/application_command_handlers.mjs +52 -0
  48. package/dist/src/cli/arguments.d.mts +38 -0
  49. package/dist/src/cli/arguments.mjs +128 -0
  50. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  51. package/dist/src/cli/basic_command_handlers.mjs +56 -0
  52. package/dist/src/cli/cli_commands.d.mts +14 -0
  53. package/dist/src/cli/cli_commands.mjs +32 -0
  54. package/dist/src/cli/command_contract.d.mts +57 -0
  55. package/dist/src/cli/command_contract.mjs +39 -0
  56. package/dist/src/cli/command_output.d.mts +12 -0
  57. package/dist/src/cli/command_output.mjs +20 -0
  58. package/dist/src/cli/command_registry.d.mts +7 -0
  59. package/dist/src/cli/command_registry.mjs +14 -0
  60. package/dist/src/cli/guided.d.mts +53 -0
  61. package/dist/src/cli/guided.mjs +719 -0
  62. package/dist/src/cli/identity_command_handlers.d.mts +8 -0
  63. package/dist/src/cli/identity_command_handlers.mjs +43 -0
  64. package/dist/src/cli/init_command.d.mts +21 -0
  65. package/dist/src/cli/init_command.mjs +90 -0
  66. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  67. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  68. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  69. package/dist/src/cli/project_command_handlers.mjs +92 -0
  70. package/dist/src/cli/project_commands.d.mts +159 -0
  71. package/dist/src/cli/project_commands.mjs +183 -0
  72. package/dist/src/cli/rehearsal.d.mts +7 -0
  73. package/dist/src/cli/rehearsal.mjs +190 -0
  74. package/dist/src/cli/renderers.d.mts +299 -0
  75. package/dist/src/cli/renderers.mjs +554 -0
  76. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  77. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  78. package/dist/src/cli/runtime_commands.d.mts +269 -0
  79. package/dist/src/cli/runtime_commands.mjs +601 -0
  80. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  81. package/dist/src/cli/source_command_handlers.mjs +62 -0
  82. package/dist/src/cli/source_commands.d.mts +436 -0
  83. package/dist/src/cli/source_commands.mjs +301 -0
  84. package/dist/src/cli/terminal.d.mts +59 -0
  85. package/dist/src/cli/terminal.mjs +230 -0
  86. package/dist/src/identity/claim.d.mts +5 -0
  87. package/dist/src/identity/claim.mjs +4 -0
  88. package/dist/src/identity/claim_contract.d.mts +135 -0
  89. package/dist/src/identity/claim_contract.mjs +6 -0
  90. package/dist/src/identity/claim_executor.d.mts +10 -0
  91. package/dist/src/identity/claim_executor.mjs +358 -0
  92. package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
  93. package/dist/src/identity/claim_identity_lookup.mjs +125 -0
  94. package/dist/src/identity/claim_plan.d.mts +8 -0
  95. package/dist/src/identity/claim_plan.mjs +61 -0
  96. package/dist/src/identity/claim_policy.d.mts +3 -0
  97. package/dist/src/identity/claim_policy.mjs +377 -0
  98. package/dist/src/identity/claim_sql.d.mts +10 -0
  99. package/dist/src/identity/claim_sql.mjs +46 -0
  100. package/dist/src/identity/claim_verification.d.mts +13 -0
  101. package/dist/src/identity/claim_verification.mjs +35 -0
  102. package/dist/src/identity/provider_configuration.d.mts +37 -0
  103. package/dist/src/identity/provider_configuration.mjs +132 -0
  104. package/dist/src/identity/storage.d.mts +21 -0
  105. package/dist/src/identity/storage.mjs +122 -0
  106. package/dist/src/index.d.mts +4 -0
  107. package/dist/src/index.mjs +3 -0
  108. package/dist/src/project/configuration.d.mts +13 -0
  109. package/dist/src/project/configuration.mjs +9 -0
  110. package/dist/src/project/configuration_contract.d.mts +125 -0
  111. package/dist/src/project/configuration_contract.mjs +27 -0
  112. package/dist/src/project/configuration_discovery.d.mts +7 -0
  113. package/dist/src/project/configuration_discovery.mjs +37 -0
  114. package/dist/src/project/configuration_normalization.d.mts +107 -0
  115. package/dist/src/project/configuration_normalization.mjs +302 -0
  116. package/dist/src/project/configuration_rendering.d.mts +14 -0
  117. package/dist/src/project/configuration_rendering.mjs +224 -0
  118. package/dist/src/project/configuration_resolution.d.mts +125 -0
  119. package/dist/src/project/configuration_resolution.mjs +81 -0
  120. package/dist/src/project/configuration_schema.d.mts +22 -0
  121. package/dist/src/project/configuration_schema.mjs +160 -0
  122. package/dist/src/project/configuration_validation.d.mts +22 -0
  123. package/dist/src/project/configuration_validation.mjs +136 -0
  124. package/dist/src/project/install_scaffold.d.mts +42 -0
  125. package/dist/src/project/install_scaffold.mjs +263 -0
  126. package/dist/src/project/project_detection.d.mts +14 -0
  127. package/dist/src/project/project_detection.mjs +47 -0
  128. package/dist/src/project/setup.d.mts +87 -0
  129. package/dist/src/project/setup.mjs +414 -0
  130. package/dist/src/project/support_report.d.mts +88 -0
  131. package/dist/src/project/support_report.mjs +83 -0
  132. package/dist/src/runtime/cleanup.d.mts +85 -0
  133. package/dist/src/runtime/cleanup.mjs +311 -0
  134. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  135. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  136. package/dist/src/runtime/migration_history.d.mts +51 -0
  137. package/dist/src/runtime/migration_history.mjs +170 -0
  138. package/dist/src/runtime/plan.d.mts +134 -0
  139. package/dist/src/runtime/plan.mjs +637 -0
  140. package/dist/src/runtime/policy.d.mts +66 -0
  141. package/dist/src/runtime/policy.mjs +369 -0
  142. package/dist/src/runtime/restore.d.mts +22 -0
  143. package/dist/src/runtime/restore.mjs +276 -0
  144. package/dist/src/runtime/service_environment.d.mts +11 -0
  145. package/dist/src/runtime/service_environment.mjs +58 -0
  146. package/dist/src/runtime/topology.d.mts +51 -0
  147. package/dist/src/runtime/topology.mjs +128 -0
  148. package/dist/src/shared/diagnostics.d.mts +97 -0
  149. package/dist/src/shared/diagnostics.mjs +170 -0
  150. package/dist/src/shared/human_output.d.mts +2 -0
  151. package/{src → dist/src}/shared/human_output.mjs +1 -3
  152. package/dist/src/shared/node_runtime.d.mts +14 -0
  153. package/dist/src/shared/node_runtime.mjs +25 -0
  154. package/dist/src/shared/operation_guard.d.mts +22 -0
  155. package/dist/src/shared/operation_guard.mjs +150 -0
  156. package/dist/src/shared/process_environment.d.mts +15 -0
  157. package/dist/src/shared/process_environment.mjs +50 -0
  158. package/dist/src/source/access.d.mts +165 -0
  159. package/dist/src/source/access.mjs +602 -0
  160. package/dist/src/source/asset_transfer.d.mts +27 -0
  161. package/dist/src/source/asset_transfer.mjs +202 -0
  162. package/dist/src/source/baseline.d.mts +66 -0
  163. package/dist/src/source/baseline.mjs +372 -0
  164. package/dist/src/source/external_reader.d.mts +42 -0
  165. package/dist/src/source/external_reader.mjs +278 -0
  166. package/dist/src/source/postgresql_access.d.mts +86 -0
  167. package/dist/src/source/postgresql_access.mjs +454 -0
  168. package/dist/src/source/privacy_paths.d.mts +10 -0
  169. package/dist/src/source/privacy_paths.mjs +27 -0
  170. package/dist/src/targets/driver.d.mts +2 -0
  171. package/dist/src/targets/driver.mjs +13 -0
  172. package/dist/src/targets/postgresql.d.mts +5 -0
  173. package/dist/src/targets/postgresql.mjs +620 -0
  174. package/dist/src/targets/supabase.d.mts +8 -0
  175. package/dist/src/targets/supabase.mjs +627 -0
  176. package/dist/src/targets/supabase_environment.d.mts +49 -0
  177. package/dist/src/targets/supabase_environment.mjs +162 -0
  178. package/dist/src/targets/target.d.mts +33 -0
  179. package/dist/src/targets/target.mjs +39 -0
  180. package/docs/README.md +1 -0
  181. package/docs/architecture.md +133 -10
  182. package/docs/configuration.md +9 -3
  183. package/docs/index.md +1 -0
  184. package/docs/releasing.md +8 -3
  185. package/docs/typescript.md +134 -0
  186. package/package.json +36 -29
  187. package/types/configuration.d.mts +5 -0
  188. package/scripts/install/create_root_config.mjs +0 -19
  189. package/src/README.md +0 -17
  190. package/src/application/session.mjs +0 -435
  191. package/src/baseline/artifact.mjs +0 -684
  192. package/src/baseline/builder.mjs +0 -164
  193. package/src/baseline/input_discovery.mjs +0 -155
  194. package/src/baseline/policy_review.mjs +0 -92
  195. package/src/baseline/preparation.mjs +0 -323
  196. package/src/baseline/privacy_engine.mjs +0 -1399
  197. package/src/baseline/privacy_structured_json.mjs +0 -554
  198. package/src/baseline/sanitization_policy.mjs +0 -299
  199. package/src/baseline/schema_snapshot.mjs +0 -113
  200. package/src/cli/arguments.mjs +0 -126
  201. package/src/cli/guided.mjs +0 -814
  202. package/src/cli/init_command.mjs +0 -94
  203. package/src/cli/rehearsal.mjs +0 -972
  204. package/src/cli/renderers.mjs +0 -638
  205. package/src/cli/runtime_commands.mjs +0 -707
  206. package/src/cli/source_commands.mjs +0 -355
  207. package/src/cli/terminal.mjs +0 -275
  208. package/src/identity/claim.mjs +0 -1165
  209. package/src/identity/provider_configuration.mjs +0 -177
  210. package/src/identity/storage.mjs +0 -165
  211. package/src/project/configuration.d.mts +0 -164
  212. package/src/project/configuration.mjs +0 -1337
  213. package/src/project/install_scaffold.mjs +0 -325
  214. package/src/project/setup.mjs +0 -474
  215. package/src/project/support_report.mjs +0 -105
  216. package/src/runtime/cleanup.mjs +0 -381
  217. package/src/runtime/migration_history.mjs +0 -220
  218. package/src/runtime/plan.mjs +0 -722
  219. package/src/runtime/policy.mjs +0 -438
  220. package/src/runtime/restore.mjs +0 -337
  221. package/src/runtime/service_environment.mjs +0 -82
  222. package/src/runtime/topology.mjs +0 -177
  223. package/src/shared/diagnostics.mjs +0 -207
  224. package/src/shared/node_runtime.mjs +0 -31
  225. package/src/shared/operation_guard.mjs +0 -162
  226. package/src/shared/process_environment.mjs +0 -68
  227. package/src/source/access.mjs +0 -798
  228. package/src/source/asset_transfer.mjs +0 -259
  229. package/src/source/baseline.mjs +0 -482
  230. package/src/source/external_reader.mjs +0 -368
  231. package/src/source/postgresql_access.mjs +0 -638
  232. package/src/source/privacy_paths.mjs +0 -38
  233. package/src/targets/postgresql.mjs +0 -808
  234. package/src/targets/supabase.mjs +0 -840
  235. package/src/targets/supabase_environment.mjs +0 -229
  236. package/src/targets/target.mjs +0 -66
@@ -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
- };