@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.15

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