@rehearsal-db/core 0.1.0-beta.9 → 0.1.0-rc.2

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 (247) hide show
  1. package/CHANGELOG.md +319 -1
  2. package/COMPATIBILITY.md +15 -0
  3. package/README.md +73 -16
  4. package/dist/package.json +100 -0
  5. package/dist/scripts/install/create_root_config.d.mts +3 -0
  6. package/dist/scripts/install/create_root_config.mjs +13 -0
  7. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  8. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  9. package/dist/src/application/interactive_task.d.mts +30 -0
  10. package/dist/src/application/interactive_task.mjs +83 -0
  11. package/dist/src/application/session.d.mts +97 -0
  12. package/dist/src/application/session.mjs +380 -0
  13. package/dist/src/baseline/artifact.d.mts +119 -0
  14. package/dist/src/baseline/artifact.mjs +522 -0
  15. package/dist/src/baseline/builder.d.mts +19 -0
  16. package/dist/src/baseline/builder.mjs +142 -0
  17. package/dist/src/baseline/input_discovery.d.mts +13 -0
  18. package/dist/src/baseline/input_discovery.mjs +149 -0
  19. package/dist/src/baseline/policy_review.d.mts +56 -0
  20. package/dist/src/baseline/policy_review.mjs +73 -0
  21. package/dist/src/baseline/preparation.d.mts +63 -0
  22. package/dist/src/baseline/preparation.mjs +295 -0
  23. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  24. package/dist/src/baseline/privacy_contract.mjs +2 -0
  25. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  26. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  27. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  28. package/dist/src/baseline/privacy_engine.mjs +4 -0
  29. package/dist/src/baseline/privacy_key.d.mts +7 -0
  30. package/dist/src/baseline/privacy_key.mjs +42 -0
  31. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  32. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  33. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  34. package/dist/src/baseline/privacy_policy.mjs +237 -0
  35. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  36. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  37. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  38. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  39. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  40. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  41. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  42. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  43. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  44. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  45. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  46. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  47. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  48. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  49. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  50. package/dist/src/cli/application_command_handlers.mjs +52 -0
  51. package/dist/src/cli/arguments.d.mts +38 -0
  52. package/dist/src/cli/arguments.mjs +130 -0
  53. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  54. package/dist/src/cli/basic_command_handlers.mjs +57 -0
  55. package/dist/src/cli/cli_commands.d.mts +14 -0
  56. package/dist/src/cli/cli_commands.mjs +34 -0
  57. package/dist/src/cli/command_contract.d.mts +57 -0
  58. package/dist/src/cli/command_contract.mjs +39 -0
  59. package/dist/src/cli/command_output.d.mts +12 -0
  60. package/dist/src/cli/command_output.mjs +20 -0
  61. package/dist/src/cli/command_registry.d.mts +7 -0
  62. package/dist/src/cli/command_registry.mjs +14 -0
  63. package/dist/src/cli/guided.d.mts +53 -0
  64. package/dist/src/cli/guided.mjs +727 -0
  65. package/dist/src/cli/identity_command_handlers.d.mts +12 -0
  66. package/dist/src/cli/identity_command_handlers.mjs +239 -0
  67. package/dist/src/cli/identity_renderer.d.mts +8 -0
  68. package/dist/src/cli/identity_renderer.mjs +33 -0
  69. package/dist/src/cli/init_command.d.mts +21 -0
  70. package/dist/src/cli/init_command.mjs +90 -0
  71. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  72. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  73. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  74. package/dist/src/cli/project_command_handlers.mjs +92 -0
  75. package/dist/src/cli/project_commands.d.mts +283 -0
  76. package/dist/src/cli/project_commands.mjs +192 -0
  77. package/dist/src/cli/rehearsal.d.mts +7 -0
  78. package/dist/src/cli/rehearsal.mjs +190 -0
  79. package/dist/src/cli/renderers.d.mts +299 -0
  80. package/dist/src/cli/renderers.mjs +554 -0
  81. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  82. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  83. package/dist/src/cli/runtime_commands.d.mts +294 -0
  84. package/dist/src/cli/runtime_commands.mjs +619 -0
  85. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  86. package/dist/src/cli/source_command_handlers.mjs +62 -0
  87. package/dist/src/cli/source_commands.d.mts +436 -0
  88. package/dist/src/cli/source_commands.mjs +301 -0
  89. package/dist/src/cli/terminal.d.mts +59 -0
  90. package/dist/src/cli/terminal.mjs +230 -0
  91. package/dist/src/identity/claim.d.mts +5 -0
  92. package/dist/src/identity/claim.mjs +4 -0
  93. package/dist/src/identity/claim_contract.d.mts +135 -0
  94. package/dist/src/identity/claim_contract.mjs +6 -0
  95. package/dist/src/identity/claim_executor.d.mts +10 -0
  96. package/dist/src/identity/claim_executor.mjs +358 -0
  97. package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
  98. package/dist/src/identity/claim_identity_lookup.mjs +134 -0
  99. package/dist/src/identity/claim_plan.d.mts +8 -0
  100. package/dist/src/identity/claim_plan.mjs +61 -0
  101. package/dist/src/identity/claim_policy.d.mts +3 -0
  102. package/dist/src/identity/claim_policy.mjs +377 -0
  103. package/dist/src/identity/claim_sql.d.mts +10 -0
  104. package/dist/src/identity/claim_sql.mjs +46 -0
  105. package/dist/src/identity/claim_verification.d.mts +13 -0
  106. package/dist/src/identity/claim_verification.mjs +35 -0
  107. package/dist/src/identity/connect.d.mts +30 -0
  108. package/dist/src/identity/connect.mjs +111 -0
  109. package/dist/src/identity/provider_configuration.d.mts +37 -0
  110. package/dist/src/identity/provider_configuration.mjs +132 -0
  111. package/dist/src/identity/storage.d.mts +21 -0
  112. package/dist/src/identity/storage.mjs +122 -0
  113. package/dist/src/index.d.mts +4 -0
  114. package/dist/src/index.mjs +3 -0
  115. package/dist/src/project/configuration.d.mts +13 -0
  116. package/dist/src/project/configuration.mjs +9 -0
  117. package/dist/src/project/configuration_contract.d.mts +125 -0
  118. package/dist/src/project/configuration_contract.mjs +27 -0
  119. package/dist/src/project/configuration_discovery.d.mts +7 -0
  120. package/dist/src/project/configuration_discovery.mjs +37 -0
  121. package/dist/src/project/configuration_normalization.d.mts +107 -0
  122. package/dist/src/project/configuration_normalization.mjs +302 -0
  123. package/dist/src/project/configuration_rendering.d.mts +14 -0
  124. package/dist/src/project/configuration_rendering.mjs +228 -0
  125. package/dist/src/project/configuration_resolution.d.mts +125 -0
  126. package/dist/src/project/configuration_resolution.mjs +81 -0
  127. package/dist/src/project/configuration_schema.d.mts +22 -0
  128. package/dist/src/project/configuration_schema.mjs +160 -0
  129. package/dist/src/project/configuration_validation.d.mts +22 -0
  130. package/dist/src/project/configuration_validation.mjs +136 -0
  131. package/dist/src/project/install_scaffold.d.mts +42 -0
  132. package/dist/src/project/install_scaffold.mjs +263 -0
  133. package/dist/src/project/project_detection.d.mts +14 -0
  134. package/dist/src/project/project_detection.mjs +47 -0
  135. package/dist/src/project/setup.d.mts +87 -0
  136. package/dist/src/project/setup.mjs +414 -0
  137. package/dist/src/project/support_report.d.mts +88 -0
  138. package/dist/src/project/support_report.mjs +83 -0
  139. package/dist/src/runtime/cleanup.d.mts +85 -0
  140. package/dist/src/runtime/cleanup.mjs +311 -0
  141. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  142. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  143. package/dist/src/runtime/migration_history.d.mts +51 -0
  144. package/dist/src/runtime/migration_history.mjs +170 -0
  145. package/dist/src/runtime/plan.d.mts +134 -0
  146. package/dist/src/runtime/plan.mjs +640 -0
  147. package/dist/src/runtime/policy.d.mts +66 -0
  148. package/dist/src/runtime/policy.mjs +369 -0
  149. package/dist/src/runtime/restore.d.mts +22 -0
  150. package/dist/src/runtime/restore.mjs +276 -0
  151. package/dist/src/runtime/service_environment.d.mts +11 -0
  152. package/dist/src/runtime/service_environment.mjs +58 -0
  153. package/dist/src/runtime/topology.d.mts +51 -0
  154. package/dist/src/runtime/topology.mjs +128 -0
  155. package/dist/src/shared/diagnostics.d.mts +97 -0
  156. package/dist/src/shared/diagnostics.mjs +170 -0
  157. package/dist/src/shared/human_output.d.mts +2 -0
  158. package/{src → dist/src}/shared/human_output.mjs +1 -3
  159. package/dist/src/shared/node_runtime.d.mts +14 -0
  160. package/dist/src/shared/node_runtime.mjs +25 -0
  161. package/dist/src/shared/operation_guard.d.mts +22 -0
  162. package/dist/src/shared/operation_guard.mjs +150 -0
  163. package/dist/src/shared/process_environment.d.mts +15 -0
  164. package/dist/src/shared/process_environment.mjs +50 -0
  165. package/dist/src/source/access.d.mts +165 -0
  166. package/dist/src/source/access.mjs +602 -0
  167. package/dist/src/source/asset_transfer.d.mts +27 -0
  168. package/dist/src/source/asset_transfer.mjs +202 -0
  169. package/dist/src/source/baseline.d.mts +66 -0
  170. package/dist/src/source/baseline.mjs +372 -0
  171. package/dist/src/source/external_reader.d.mts +42 -0
  172. package/dist/src/source/external_reader.mjs +278 -0
  173. package/dist/src/source/postgresql_access.d.mts +86 -0
  174. package/dist/src/source/postgresql_access.mjs +454 -0
  175. package/dist/src/source/privacy_paths.d.mts +10 -0
  176. package/dist/src/source/privacy_paths.mjs +27 -0
  177. package/dist/src/targets/driver.d.mts +2 -0
  178. package/dist/src/targets/driver.mjs +13 -0
  179. package/dist/src/targets/postgresql.d.mts +5 -0
  180. package/dist/src/targets/postgresql.mjs +620 -0
  181. package/dist/src/targets/supabase.d.mts +8 -0
  182. package/dist/src/targets/supabase.mjs +629 -0
  183. package/dist/src/targets/supabase_environment.d.mts +54 -0
  184. package/dist/src/targets/supabase_environment.mjs +184 -0
  185. package/dist/src/targets/target.d.mts +33 -0
  186. package/dist/src/targets/target.mjs +39 -0
  187. package/docs/README.md +35 -16
  188. package/docs/architecture.md +135 -10
  189. package/docs/commands.md +53 -15
  190. package/docs/configuration.md +186 -31
  191. package/docs/getting-started.md +66 -9
  192. package/docs/index.md +67 -0
  193. package/docs/production-source.md +175 -17
  194. package/docs/releasing.md +24 -12
  195. package/docs/roadmap.md +3 -2
  196. package/docs/runtime-lifecycle.md +99 -0
  197. package/docs/runtime-policies.md +80 -7
  198. package/docs/sanitization.md +572 -4
  199. package/docs/security-model.md +8 -0
  200. package/docs/stability.md +70 -0
  201. package/docs/standalone-workflow.md +36 -13
  202. package/docs/troubleshooting.md +82 -10
  203. package/docs/tutorial.md +7 -1
  204. package/docs/typescript.md +134 -0
  205. package/package.json +44 -29
  206. package/types/configuration.d.mts +5 -0
  207. package/src/README.md +0 -17
  208. package/src/application/session.mjs +0 -313
  209. package/src/baseline/artifact.mjs +0 -661
  210. package/src/baseline/builder.mjs +0 -164
  211. package/src/baseline/input_discovery.mjs +0 -155
  212. package/src/baseline/policy_review.mjs +0 -92
  213. package/src/baseline/preparation.mjs +0 -323
  214. package/src/baseline/privacy_engine.mjs +0 -413
  215. package/src/baseline/sanitization_policy.mjs +0 -299
  216. package/src/baseline/schema_snapshot.mjs +0 -113
  217. package/src/cli/arguments.mjs +0 -120
  218. package/src/cli/guided.mjs +0 -807
  219. package/src/cli/rehearsal.mjs +0 -933
  220. package/src/cli/renderers.mjs +0 -584
  221. package/src/cli/runtime_commands.mjs +0 -553
  222. package/src/cli/source_commands.mjs +0 -326
  223. package/src/cli/terminal.mjs +0 -275
  224. package/src/identity/claim.mjs +0 -975
  225. package/src/identity/storage.mjs +0 -165
  226. package/src/project/configuration.d.mts +0 -153
  227. package/src/project/configuration.mjs +0 -1197
  228. package/src/project/setup.mjs +0 -379
  229. package/src/project/support_report.mjs +0 -105
  230. package/src/runtime/cleanup.mjs +0 -381
  231. package/src/runtime/migration_history.mjs +0 -220
  232. package/src/runtime/plan.mjs +0 -685
  233. package/src/runtime/policy.mjs +0 -438
  234. package/src/runtime/restore.mjs +0 -329
  235. package/src/runtime/service_environment.mjs +0 -82
  236. package/src/runtime/topology.mjs +0 -177
  237. package/src/shared/diagnostics.mjs +0 -199
  238. package/src/shared/operation_guard.mjs +0 -162
  239. package/src/shared/process_environment.mjs +0 -68
  240. package/src/source/access.mjs +0 -578
  241. package/src/source/asset_transfer.mjs +0 -177
  242. package/src/source/baseline.mjs +0 -446
  243. package/src/source/postgresql_access.mjs +0 -480
  244. package/src/targets/postgresql.mjs +0 -808
  245. package/src/targets/supabase.mjs +0 -830
  246. package/src/targets/supabase_environment.mjs +0 -229
  247. package/src/targets/target.mjs +0 -66
@@ -1,578 +0,0 @@
1
- /**
2
- * Validate and digest the separately authorized source-access boundary.
3
- * Plans contain no credentials or raw owner identifiers.
4
- */
5
-
6
- import { createHash } from "node:crypto";
7
-
8
- const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
9
- const ENVIRONMENT_KEY = /^[A-Z][A-Z0-9_]{1,127}$/u;
10
- const SHA256 = /^[a-f0-9]{64}$/u;
11
-
12
- const isObject = (value) =>
13
- value !== null &&
14
- typeof value === "object" &&
15
- !Array.isArray(value) &&
16
- Object.getPrototypeOf(value) === Object.prototype;
17
-
18
- const knownKeys = (value, allowed, label) => {
19
- for (const key of Object.keys(value)) {
20
- if (!allowed.includes(key)) throw new Error(`${label}.${key} is unknown.`);
21
- }
22
- };
23
-
24
- const identifier = (value, label) => {
25
- if (!IDENTIFIER.test(value ?? "")) {
26
- throw new Error(`${label} must be a lowercase PostgreSQL identifier.`);
27
- }
28
- return value;
29
- };
30
-
31
- const envKey = (value, label) => {
32
- if (!ENVIRONMENT_KEY.test(value ?? "")) {
33
- throw new Error(`${label} must name an uppercase environment variable.`);
34
- }
35
- return value;
36
- };
37
-
38
- const fingerprint = (value, label) => {
39
- if (!SHA256.test(value ?? "")) throw new Error(`${label} must be a SHA-256.`);
40
- return value;
41
- };
42
-
43
- const positiveInteger = (value, label) => {
44
- if (!Number.isSafeInteger(value) || value < 1) {
45
- throw new Error(`${label} must be a positive integer.`);
46
- }
47
- return value;
48
- };
49
-
50
- const canonicalJson = (value) => `${JSON.stringify(value, null, "\t")}\n`;
51
- const sha256 = (value) => createHash("sha256").update(value).digest("hex");
52
-
53
- export const sourceTargetFingerprint = (connectionString) => {
54
- let target;
55
- try {
56
- const parsed = new URL(connectionString);
57
- if (!["postgres:", "postgresql:"].includes(parsed.protocol))
58
- throw new Error();
59
- target = {
60
- protocol: "postgresql",
61
- hostname: parsed.hostname.toLowerCase(),
62
- port: parsed.port || "5432",
63
- database: decodeURIComponent(parsed.pathname.replace(/^\//u, "")),
64
- };
65
- } catch {
66
- throw new Error(
67
- "Source credential is not a valid PostgreSQL connection URL.",
68
- );
69
- }
70
- if (!target.hostname || !target.database) {
71
- throw new Error("Source credential must identify a host and database.");
72
- }
73
- return sha256(canonicalJson(target));
74
- };
75
-
76
- export const validateSourceAccessPolicy = (policy) => {
77
- if (!isObject(policy))
78
- throw new Error("Source access policy must be an object.");
79
- knownKeys(
80
- policy,
81
- [
82
- "accessVersion",
83
- "targetFingerprint",
84
- "administratorEnvironmentVariable",
85
- "reader",
86
- "exportSchema",
87
- "migrationLedger",
88
- "relations",
89
- "assetReader",
90
- "assets",
91
- ],
92
- "sourcePolicy",
93
- );
94
- if (policy.accessVersion !== 1) {
95
- throw new Error("Source access policy must use accessVersion 1.");
96
- }
97
- fingerprint(policy.targetFingerprint, "sourcePolicy.targetFingerprint");
98
- envKey(
99
- policy.administratorEnvironmentVariable,
100
- "sourcePolicy.administratorEnvironmentVariable",
101
- );
102
- if (!isObject(policy.reader))
103
- throw new Error("sourcePolicy.reader must be an object.");
104
- knownKeys(
105
- policy.reader,
106
- ["role", "ownerRole", "credentialFile", "validForMinutes"],
107
- "sourcePolicy.reader",
108
- );
109
- const role = identifier(policy.reader.role, "sourcePolicy.reader.role");
110
- const ownerRole = identifier(
111
- policy.reader.ownerRole,
112
- "sourcePolicy.reader.ownerRole",
113
- );
114
- if (role === ownerRole)
115
- throw new Error("Source reader and owner roles must differ.");
116
- if (
117
- typeof policy.reader.credentialFile !== "string" ||
118
- !policy.reader.credentialFile.startsWith(".rehearsal/") ||
119
- policy.reader.credentialFile.split(/[\\/]/u).includes("..")
120
- ) {
121
- throw new Error(
122
- "sourcePolicy.reader.credentialFile must stay under .rehearsal/.",
123
- );
124
- }
125
- if (
126
- !Number.isSafeInteger(policy.reader.validForMinutes) ||
127
- policy.reader.validForMinutes < 5 ||
128
- policy.reader.validForMinutes > 240
129
- ) {
130
- throw new Error(
131
- "sourcePolicy.reader.validForMinutes must be from 5 through 240.",
132
- );
133
- }
134
- const exportSchema = identifier(
135
- policy.exportSchema,
136
- "sourcePolicy.exportSchema",
137
- );
138
- if (!isObject(policy.migrationLedger)) {
139
- throw new Error("sourcePolicy.migrationLedger must be an object.");
140
- }
141
- knownKeys(
142
- policy.migrationLedger,
143
- ["schema", "table", "versionColumn", "nameColumn", "statementsColumn"],
144
- "sourcePolicy.migrationLedger",
145
- );
146
- const migrationLedger = Object.freeze({
147
- schema: identifier(
148
- policy.migrationLedger.schema,
149
- "sourcePolicy.migrationLedger.schema",
150
- ),
151
- table: identifier(
152
- policy.migrationLedger.table,
153
- "sourcePolicy.migrationLedger.table",
154
- ),
155
- versionColumn: identifier(
156
- policy.migrationLedger.versionColumn,
157
- "sourcePolicy.migrationLedger.versionColumn",
158
- ),
159
- nameColumn: identifier(
160
- policy.migrationLedger.nameColumn,
161
- "sourcePolicy.migrationLedger.nameColumn",
162
- ),
163
- statementsColumn: identifier(
164
- policy.migrationLedger.statementsColumn,
165
- "sourcePolicy.migrationLedger.statementsColumn",
166
- ),
167
- });
168
- if (!Array.isArray(policy.relations) || policy.relations.length === 0) {
169
- throw new Error(
170
- "sourcePolicy.relations must declare at least one relation.",
171
- );
172
- }
173
- const viewNames = new Set();
174
- const targetTables = new Set();
175
- const relations = policy.relations.map((relation, index) => {
176
- const label = `sourcePolicy.relations[${index}]`;
177
- if (!isObject(relation)) throw new Error(`${label} must be an object.`);
178
- knownKeys(
179
- relation,
180
- [
181
- "source",
182
- "view",
183
- "targetSchema",
184
- "targetTable",
185
- "columns",
186
- "orderBy",
187
- "rowScope",
188
- ],
189
- label,
190
- );
191
- if (!isObject(relation.source))
192
- throw new Error(`${label}.source must be an object.`);
193
- knownKeys(relation.source, ["schema", "table"], `${label}.source`);
194
- const source = Object.freeze({
195
- schema: identifier(relation.source.schema, `${label}.source.schema`),
196
- table: identifier(relation.source.table, `${label}.source.table`),
197
- });
198
- const view = identifier(relation.view, `${label}.view`);
199
- const targetSchema = identifier(
200
- relation.targetSchema ?? "public",
201
- `${label}.targetSchema`,
202
- );
203
- const targetTable = identifier(
204
- relation.targetTable,
205
- `${label}.targetTable`,
206
- );
207
- const targetKey = `${targetSchema}.${targetTable}`;
208
- if (targetTables.has(targetKey)) {
209
- throw new Error(
210
- `Source access policy duplicates target table ${targetKey}.`,
211
- );
212
- }
213
- targetTables.add(targetKey);
214
- if (viewNames.has(view))
215
- throw new Error(`Source access policy duplicates view ${view}.`);
216
- viewNames.add(view);
217
- if (
218
- !Array.isArray(relation.columns) ||
219
- relation.columns.length === 0 ||
220
- new Set(relation.columns).size !== relation.columns.length
221
- ) {
222
- throw new Error(`${label}.columns must be a non-empty unique array.`);
223
- }
224
- const columns = Object.freeze(
225
- relation.columns.map((column, columnIndex) =>
226
- identifier(column, `${label}.columns[${columnIndex}]`),
227
- ),
228
- );
229
- if (
230
- !Array.isArray(relation.orderBy) ||
231
- relation.orderBy.length === 0 ||
232
- new Set(relation.orderBy).size !== relation.orderBy.length ||
233
- relation.orderBy.some((column) => !columns.includes(column))
234
- ) {
235
- throw new Error(`${label}.orderBy must name unique exported columns.`);
236
- }
237
- const orderBy = Object.freeze([...relation.orderBy]);
238
- if (!isObject(relation.rowScope))
239
- throw new Error(`${label}.rowScope must be an object.`);
240
- knownKeys(
241
- relation.rowScope,
242
- ["kind", "column", "valueEnvironmentVariable"],
243
- `${label}.rowScope`,
244
- );
245
- let rowScope;
246
- if (relation.rowScope.kind === "approved-public") {
247
- if (
248
- relation.rowScope.column !== undefined ||
249
- relation.rowScope.valueEnvironmentVariable !== undefined
250
- ) {
251
- throw new Error(
252
- `${label}.rowScope approved-public accepts no owner fields.`,
253
- );
254
- }
255
- rowScope = Object.freeze({ kind: "approved-public" });
256
- } else if (relation.rowScope.kind === "approved-owner") {
257
- const column = identifier(
258
- relation.rowScope.column,
259
- `${label}.rowScope.column`,
260
- );
261
- if (!columns.includes(column)) {
262
- throw new Error(
263
- `${label}.rowScope.column must be included in exported columns.`,
264
- );
265
- }
266
- rowScope = Object.freeze({
267
- kind: "approved-owner",
268
- column,
269
- valueEnvironmentVariable: envKey(
270
- relation.rowScope.valueEnvironmentVariable,
271
- `${label}.rowScope.valueEnvironmentVariable`,
272
- ),
273
- });
274
- } else {
275
- throw new Error(`${label}.rowScope.kind is unsupported.`);
276
- }
277
- return Object.freeze({
278
- source,
279
- view,
280
- targetSchema,
281
- targetTable,
282
- columns,
283
- orderBy,
284
- rowScope,
285
- });
286
- });
287
- const assets = policy.assets ?? [];
288
- if (!Array.isArray(assets))
289
- throw new Error("sourcePolicy.assets must be an array.");
290
- const normalizedAssets = assets.map((asset, index) => {
291
- const label = `sourcePolicy.assets[${index}]`;
292
- if (!isObject(asset)) throw new Error(`${label} must be an object.`);
293
- knownKeys(asset, ["bucket", "prefix", "rights"], label);
294
- if (!/^[a-z0-9][a-z0-9.-]{0,99}$/u.test(asset.bucket ?? "")) {
295
- throw new Error(`${label}.bucket is invalid.`);
296
- }
297
- const prefixParts =
298
- typeof asset.prefix === "string"
299
- ? asset.prefix.replace(/\/$/u, "").split("/")
300
- : [];
301
- if (
302
- typeof asset.prefix !== "string" ||
303
- !asset.prefix ||
304
- asset.prefix.startsWith("/") ||
305
- prefixParts.some((part) => !part || part === "." || part === "..")
306
- ) {
307
- throw new Error(`${label}.prefix is unsafe.`);
308
- }
309
- if (!["approved-public", "approved-owner"].includes(asset.rights)) {
310
- throw new Error(`${label}.rights must be explicitly approved.`);
311
- }
312
- return Object.freeze({
313
- bucket: asset.bucket,
314
- prefix: asset.prefix,
315
- rights: asset.rights,
316
- });
317
- });
318
- let assetReader = null;
319
- if (normalizedAssets.length > 0) {
320
- if (!isObject(policy.assetReader)) {
321
- throw new Error(
322
- "sourcePolicy.assetReader is required when assets are declared.",
323
- );
324
- }
325
- knownKeys(
326
- policy.assetReader,
327
- [
328
- "endpointFingerprint",
329
- "baseUrlEnvironmentVariable",
330
- "tokenEnvironmentVariable",
331
- "maximumObjects",
332
- "maximumObjectBytes",
333
- "maximumTotalBytes",
334
- ],
335
- "sourcePolicy.assetReader",
336
- );
337
- assetReader = Object.freeze({
338
- endpointFingerprint: fingerprint(
339
- policy.assetReader.endpointFingerprint,
340
- "sourcePolicy.assetReader.endpointFingerprint",
341
- ),
342
- baseUrlEnvironmentVariable: envKey(
343
- policy.assetReader.baseUrlEnvironmentVariable,
344
- "sourcePolicy.assetReader.baseUrlEnvironmentVariable",
345
- ),
346
- tokenEnvironmentVariable: envKey(
347
- policy.assetReader.tokenEnvironmentVariable,
348
- "sourcePolicy.assetReader.tokenEnvironmentVariable",
349
- ),
350
- maximumObjects: positiveInteger(
351
- policy.assetReader.maximumObjects,
352
- "sourcePolicy.assetReader.maximumObjects",
353
- ),
354
- maximumObjectBytes: positiveInteger(
355
- policy.assetReader.maximumObjectBytes,
356
- "sourcePolicy.assetReader.maximumObjectBytes",
357
- ),
358
- maximumTotalBytes: positiveInteger(
359
- policy.assetReader.maximumTotalBytes,
360
- "sourcePolicy.assetReader.maximumTotalBytes",
361
- ),
362
- });
363
- } else if (policy.assetReader !== undefined && policy.assetReader !== null) {
364
- throw new Error(
365
- "sourcePolicy.assetReader is not allowed without declared assets.",
366
- );
367
- }
368
- return Object.freeze({
369
- accessVersion: 1,
370
- targetFingerprint: policy.targetFingerprint,
371
- administratorEnvironmentVariable: policy.administratorEnvironmentVariable,
372
- reader: Object.freeze({
373
- role,
374
- ownerRole,
375
- credentialFile: policy.reader.credentialFile,
376
- validForMinutes: policy.reader.validForMinutes,
377
- }),
378
- exportSchema,
379
- migrationLedger,
380
- assetReader,
381
- relations: Object.freeze(relations),
382
- assets: Object.freeze(normalizedAssets),
383
- });
384
- };
385
-
386
- export const createSourceAccessPlan = ({
387
- policy,
388
- environment = process.env,
389
- }) => {
390
- const normalized = validateSourceAccessPolicy(policy);
391
- const sourceCredential =
392
- environment[normalized.administratorEnvironmentVariable];
393
- if (!sourceCredential) {
394
- throw new Error(
395
- `Source preparation requires ${normalized.administratorEnvironmentVariable}; the value is read from the environment and never printed.`,
396
- );
397
- }
398
- const actualTarget = sourceTargetFingerprint(sourceCredential);
399
- if (actualTarget !== normalized.targetFingerprint) {
400
- throw new Error(
401
- "Source credential does not match the reviewed target fingerprint.",
402
- );
403
- }
404
- const ownerScopes = Object.fromEntries(
405
- normalized.relations
406
- .filter((relation) => relation.rowScope.kind === "approved-owner")
407
- .map((relation) => {
408
- const key = relation.rowScope.valueEnvironmentVariable;
409
- const value = environment[key];
410
- if (!value) throw new Error(`Source preparation requires ${key}.`);
411
- return [relation.view, sha256(value)];
412
- }),
413
- );
414
- if (normalized.assetReader) {
415
- const baseUrl =
416
- environment[normalized.assetReader.baseUrlEnvironmentVariable];
417
- const token = environment[normalized.assetReader.tokenEnvironmentVariable];
418
- if (
419
- !baseUrl ||
420
- assetEndpointFingerprint(baseUrl) !==
421
- normalized.assetReader.endpointFingerprint
422
- ) {
423
- throw new Error(
424
- "Asset reader URL does not match the reviewed endpoint fingerprint.",
425
- );
426
- }
427
- if (!token)
428
- throw new Error(
429
- `Source preparation requires ${normalized.assetReader.tokenEnvironmentVariable}.`,
430
- );
431
- }
432
- const review = {
433
- planVersion: 1,
434
- operation: "prepare-source-access",
435
- targetFingerprint: normalized.targetFingerprint,
436
- exportSchema: normalized.exportSchema,
437
- reader: {
438
- role: normalized.reader.role,
439
- ownerRole: normalized.reader.ownerRole,
440
- validForMinutes: normalized.reader.validForMinutes,
441
- credentialFile: normalized.reader.credentialFile,
442
- },
443
- relations: normalized.relations.map((relation) => ({
444
- source: relation.source,
445
- view: relation.view,
446
- targetTable: relation.targetTable,
447
- columns: relation.columns,
448
- orderBy: relation.orderBy,
449
- rowScope: {
450
- kind: relation.rowScope.kind,
451
- ...(relation.rowScope.kind === "approved-owner"
452
- ? {
453
- column: relation.rowScope.column,
454
- valueSha256: ownerScopes[relation.view],
455
- }
456
- : {}),
457
- },
458
- })),
459
- assets: normalized.assets,
460
- assetReader: normalized.assetReader
461
- ? {
462
- endpointFingerprint: normalized.assetReader.endpointFingerprint,
463
- maximumObjects: normalized.assetReader.maximumObjects,
464
- maximumObjectBytes: normalized.assetReader.maximumObjectBytes,
465
- maximumTotalBytes: normalized.assetReader.maximumTotalBytes,
466
- }
467
- : null,
468
- denyChecks: [
469
- "raw relation reads",
470
- "writes",
471
- "role creation and escalation",
472
- "network functions",
473
- "unrelated private assets",
474
- ],
475
- };
476
- return Object.freeze({
477
- policy: normalized,
478
- review: Object.freeze(review),
479
- digest: sha256(canonicalJson(review)),
480
- });
481
- };
482
-
483
- export const assetEndpointFingerprint = (value) => {
484
- let endpoint;
485
- try {
486
- const parsed = new URL(value);
487
- if (
488
- parsed.protocol !== "https:" &&
489
- !(
490
- parsed.protocol === "http:" &&
491
- ["127.0.0.1", "::1", "localhost"].includes(parsed.hostname)
492
- )
493
- ) {
494
- throw new Error();
495
- }
496
- endpoint = `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/$/u, "")}`;
497
- } catch {
498
- throw new Error("Asset reader endpoint is invalid or insecure.");
499
- }
500
- return sha256(endpoint);
501
- };
502
-
503
- export const assertSourceAccessConfirmation = (plan, confirmation) => {
504
- if (confirmation !== plan.digest) {
505
- throw new Error(
506
- `Source access confirmation does not match this exact plan. Expected ${plan.digest}.`,
507
- );
508
- }
509
- return plan;
510
- };
511
-
512
- export const createSourceRetirementPlan = ({
513
- receipt,
514
- targetFingerprint: target,
515
- }) => {
516
- if (!isObject(receipt) || receipt.receiptVersion !== 1) {
517
- throw new Error("Source access receipt is invalid.");
518
- }
519
- fingerprint(target, "Source retirement target fingerprint");
520
- if (receipt.targetFingerprint !== target) {
521
- throw new Error(
522
- "Source access receipt belongs to a different source target.",
523
- );
524
- }
525
- if (!Array.isArray(receipt.views) || !Array.isArray(receipt.relations)) {
526
- throw new Error("Source access receipt object inventory is invalid.");
527
- }
528
- const relations = receipt.relations.map((relation, index) => {
529
- if (!isObject(relation))
530
- throw new Error(`receipt.relations[${index}] is invalid.`);
531
- knownKeys(
532
- relation,
533
- ["schema", "table", "columns"],
534
- `receipt.relations[${index}]`,
535
- );
536
- if (!Array.isArray(relation.columns) || relation.columns.length === 0) {
537
- throw new Error(`receipt.relations[${index}].columns is invalid.`);
538
- }
539
- return {
540
- schema: identifier(relation.schema, `receipt.relations[${index}].schema`),
541
- table: identifier(relation.table, `receipt.relations[${index}].table`),
542
- columns: relation.columns.map((column) =>
543
- identifier(column, `receipt.relations[${index}].columns[]`),
544
- ),
545
- };
546
- });
547
- if (!isObject(receipt.migrationLedger)) {
548
- throw new Error("Source access receipt migration ledger is invalid.");
549
- }
550
- const ledger = {
551
- schema: identifier(
552
- receipt.migrationLedger.schema,
553
- "receipt.migrationLedger.schema",
554
- ),
555
- table: identifier(
556
- receipt.migrationLedger.table,
557
- "receipt.migrationLedger.table",
558
- ),
559
- };
560
- const review = {
561
- planVersion: 1,
562
- operation: "retire-source-access",
563
- targetFingerprint: target,
564
- preparationDigest: fingerprint(receipt.planDigest, "receipt.planDigest"),
565
- exportSchema: identifier(receipt.exportSchema, "receipt.exportSchema"),
566
- views: [...receipt.views]
567
- .map((view) => identifier(view, "receipt.views[]"))
568
- .sort(),
569
- relations,
570
- migrationLedger: ledger,
571
- readerRole: identifier(receipt.readerRole, "receipt.readerRole"),
572
- ownerRole: identifier(receipt.ownerRole, "receipt.ownerRole"),
573
- };
574
- return Object.freeze({
575
- review: Object.freeze(review),
576
- digest: sha256(canonicalJson(review)),
577
- });
578
- };