@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.md +4 -1
- package/dist/package.json +100 -0
- package/dist/scripts/install/create_root_config.d.mts +3 -0
- package/dist/scripts/install/create_root_config.mjs +13 -0
- package/dist/scripts/runtime/manage_database.d.mts +6 -0
- package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
- package/dist/src/application/session.d.mts +96 -0
- package/dist/src/application/session.mjs +380 -0
- package/dist/src/baseline/artifact.d.mts +119 -0
- package/dist/src/baseline/artifact.mjs +522 -0
- package/dist/src/baseline/builder.d.mts +19 -0
- package/dist/src/baseline/builder.mjs +142 -0
- package/dist/src/baseline/input_discovery.d.mts +13 -0
- package/dist/src/baseline/input_discovery.mjs +149 -0
- package/dist/src/baseline/policy_review.d.mts +56 -0
- package/dist/src/baseline/policy_review.mjs +73 -0
- package/dist/src/baseline/preparation.d.mts +63 -0
- package/dist/src/baseline/preparation.mjs +295 -0
- package/dist/src/baseline/privacy_engine.d.mts +162 -0
- package/dist/src/baseline/privacy_engine.mjs +1162 -0
- package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
- package/dist/src/baseline/privacy_structured_json.mjs +464 -0
- package/dist/src/baseline/sanitization_policy.d.mts +69 -0
- package/dist/src/baseline/sanitization_policy.mjs +227 -0
- package/dist/src/baseline/schema_snapshot.d.mts +19 -0
- package/dist/src/baseline/schema_snapshot.mjs +79 -0
- package/dist/src/cli/arguments.d.mts +43 -0
- package/dist/src/cli/arguments.mjs +135 -0
- package/dist/src/cli/guided.d.mts +51 -0
- package/dist/src/cli/guided.mjs +713 -0
- package/dist/src/cli/init_command.d.mts +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- package/dist/src/cli/project_commands.d.mts +157 -0
- package/dist/src/cli/project_commands.mjs +183 -0
- package/dist/src/cli/rehearsal.d.mts +7 -0
- package/dist/src/cli/rehearsal.mjs +706 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- package/dist/src/cli/runtime_commands.d.mts +265 -0
- package/dist/src/cli/runtime_commands.mjs +599 -0
- package/dist/src/cli/source_commands.d.mts +432 -0
- package/dist/src/cli/source_commands.mjs +299 -0
- package/dist/src/cli/terminal.d.mts +59 -0
- package/dist/src/cli/terminal.mjs +230 -0
- package/dist/src/identity/claim.d.mts +144 -0
- package/dist/src/identity/claim.mjs +947 -0
- package/dist/src/identity/provider_configuration.d.mts +37 -0
- package/dist/src/identity/provider_configuration.mjs +132 -0
- package/dist/src/identity/storage.d.mts +21 -0
- package/dist/src/identity/storage.mjs +122 -0
- package/dist/src/index.d.mts +4 -0
- package/dist/src/index.mjs +3 -0
- package/dist/src/project/configuration.d.mts +181 -0
- package/dist/src/project/configuration.mjs +958 -0
- package/dist/src/project/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/setup.d.mts +87 -0
- package/dist/src/project/setup.mjs +414 -0
- package/dist/src/project/support_report.d.mts +88 -0
- package/dist/src/project/support_report.mjs +83 -0
- package/dist/src/runtime/cleanup.d.mts +85 -0
- package/dist/src/runtime/cleanup.mjs +311 -0
- package/dist/src/runtime/migration_history.d.mts +51 -0
- package/dist/src/runtime/migration_history.mjs +170 -0
- package/dist/src/runtime/plan.d.mts +134 -0
- package/dist/src/runtime/plan.mjs +636 -0
- package/dist/src/runtime/policy.d.mts +66 -0
- package/dist/src/runtime/policy.mjs +369 -0
- package/dist/src/runtime/restore.d.mts +22 -0
- package/dist/src/runtime/restore.mjs +276 -0
- package/dist/src/runtime/service_environment.d.mts +11 -0
- package/dist/src/runtime/service_environment.mjs +58 -0
- package/dist/src/runtime/topology.d.mts +51 -0
- package/dist/src/runtime/topology.mjs +128 -0
- package/dist/src/shared/diagnostics.d.mts +97 -0
- package/dist/src/shared/diagnostics.mjs +170 -0
- package/dist/src/shared/human_output.d.mts +2 -0
- package/{src → dist/src}/shared/human_output.mjs +1 -3
- package/dist/src/shared/node_runtime.d.mts +14 -0
- package/dist/src/shared/node_runtime.mjs +25 -0
- package/dist/src/shared/operation_guard.d.mts +22 -0
- package/dist/src/shared/operation_guard.mjs +150 -0
- package/dist/src/shared/process_environment.d.mts +15 -0
- package/dist/src/shared/process_environment.mjs +50 -0
- package/dist/src/source/access.d.mts +165 -0
- package/dist/src/source/access.mjs +602 -0
- package/dist/src/source/asset_transfer.d.mts +27 -0
- package/dist/src/source/asset_transfer.mjs +202 -0
- package/dist/src/source/baseline.d.mts +66 -0
- package/dist/src/source/baseline.mjs +372 -0
- package/dist/src/source/external_reader.d.mts +42 -0
- package/dist/src/source/external_reader.mjs +278 -0
- package/dist/src/source/postgresql_access.d.mts +86 -0
- package/dist/src/source/postgresql_access.mjs +454 -0
- package/dist/src/source/privacy_paths.d.mts +10 -0
- package/dist/src/source/privacy_paths.mjs +27 -0
- package/dist/src/targets/driver.d.mts +2 -0
- package/dist/src/targets/driver.mjs +13 -0
- package/dist/src/targets/postgresql.d.mts +5 -0
- package/dist/src/targets/postgresql.mjs +641 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +659 -0
- package/dist/src/targets/supabase_environment.d.mts +49 -0
- package/dist/src/targets/supabase_environment.mjs +162 -0
- package/dist/src/targets/target.d.mts +33 -0
- package/dist/src/targets/target.mjs +39 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +17 -9
- package/docs/configuration.md +49 -8
- package/docs/getting-started.md +28 -0
- package/docs/index.md +1 -0
- package/docs/production-source.md +43 -7
- package/docs/releasing.md +5 -3
- package/docs/roadmap.md +3 -2
- package/docs/sanitization.md +415 -14
- package/docs/standalone-workflow.md +12 -6
- package/docs/troubleshooting.md +5 -1
- package/docs/tutorial.md +5 -0
- package/docs/typescript.md +87 -0
- package/package.json +35 -29
- package/scripts/install/create_root_config.mjs +0 -36
- package/src/README.md +0 -17
- package/src/application/session.mjs +0 -435
- package/src/baseline/artifact.mjs +0 -684
- package/src/baseline/builder.mjs +0 -164
- package/src/baseline/input_discovery.mjs +0 -155
- package/src/baseline/policy_review.mjs +0 -92
- package/src/baseline/preparation.mjs +0 -323
- package/src/baseline/privacy_engine.mjs +0 -952
- package/src/baseline/sanitization_policy.mjs +0 -299
- package/src/baseline/schema_snapshot.mjs +0 -113
- package/src/cli/arguments.mjs +0 -126
- package/src/cli/guided.mjs +0 -814
- package/src/cli/init_command.mjs +0 -94
- package/src/cli/rehearsal.mjs +0 -970
- package/src/cli/renderers.mjs +0 -626
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -347
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1163
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -291
- package/src/project/setup.mjs +0 -466
- package/src/project/support_report.mjs +0 -105
- package/src/runtime/cleanup.mjs +0 -381
- package/src/runtime/migration_history.mjs +0 -220
- package/src/runtime/plan.mjs +0 -722
- package/src/runtime/policy.mjs +0 -438
- package/src/runtime/restore.mjs +0 -337
- package/src/runtime/service_environment.mjs +0 -82
- package/src/runtime/topology.mjs +0 -177
- package/src/shared/diagnostics.mjs +0 -207
- package/src/shared/node_runtime.mjs +0 -31
- package/src/shared/operation_guard.mjs +0 -162
- package/src/shared/process_environment.mjs +0 -68
- package/src/source/access.mjs +0 -753
- package/src/source/asset_transfer.mjs +0 -207
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -255
- package/src/source/postgresql_access.mjs +0 -581
- package/src/source/privacy_paths.mjs +0 -38
- package/src/targets/postgresql.mjs +0 -808
- package/src/targets/supabase.mjs +0 -840
- package/src/targets/supabase_environment.mjs +0 -229
- package/src/targets/target.mjs +0 -66
- /package/{src/project → types}/configuration.d.mts +0 -0
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/** Verify an externally provisioned PostgreSQL reader without source writes. */
|
|
2
|
-
|
|
3
|
-
import pg from "pg";
|
|
4
|
-
import { externalViewDefinitionFingerprint } from "./access.mjs";
|
|
5
|
-
|
|
6
|
-
const { Client } = pg;
|
|
7
|
-
|
|
8
|
-
const connect = async (connectionString, clientFactory) => {
|
|
9
|
-
const client = clientFactory
|
|
10
|
-
? await clientFactory(connectionString)
|
|
11
|
-
: new Client({
|
|
12
|
-
connectionString,
|
|
13
|
-
application_name: "rehearsal-external-source-verification",
|
|
14
|
-
});
|
|
15
|
-
if (typeof client.connect === "function") await client.connect();
|
|
16
|
-
return client;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const columnKey = (schema, relation, column) =>
|
|
20
|
-
`${schema}\0${relation}\0${column}`;
|
|
21
|
-
|
|
22
|
-
const expectedReadableColumns = (plan) =>
|
|
23
|
-
new Set([
|
|
24
|
-
...plan.policy.relations.flatMap((relation) =>
|
|
25
|
-
relation.columns.map((column) =>
|
|
26
|
-
columnKey(plan.policy.exportSchema, relation.view, column),
|
|
27
|
-
),
|
|
28
|
-
),
|
|
29
|
-
...[
|
|
30
|
-
plan.policy.migrationLedger.versionColumn,
|
|
31
|
-
plan.policy.migrationLedger.nameColumn,
|
|
32
|
-
plan.policy.migrationLedger.statementsColumn,
|
|
33
|
-
].map((column) =>
|
|
34
|
-
columnKey(
|
|
35
|
-
plan.policy.migrationLedger.schema,
|
|
36
|
-
plan.policy.migrationLedger.table,
|
|
37
|
-
column,
|
|
38
|
-
),
|
|
39
|
-
),
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
const count = (result, label) => {
|
|
43
|
-
const value = Number(result.rows[0]?.count);
|
|
44
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
45
|
-
throw new Error(`External source reader ${label} check was inconclusive.`);
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const verifyExternalPostgresqlReader = async ({
|
|
51
|
-
plan,
|
|
52
|
-
connectionString,
|
|
53
|
-
clientFactory,
|
|
54
|
-
now = new Date(),
|
|
55
|
-
}) => {
|
|
56
|
-
if (plan.policy.reader.mode !== "external") {
|
|
57
|
-
throw new Error("External reader verification requires external mode.");
|
|
58
|
-
}
|
|
59
|
-
const client = await connect(connectionString, clientFactory);
|
|
60
|
-
let transactionOpen = false;
|
|
61
|
-
try {
|
|
62
|
-
await client.query("begin transaction read only");
|
|
63
|
-
transactionOpen = true;
|
|
64
|
-
const roleResult = await client.query(
|
|
65
|
-
`select rolname as role, rolcanlogin, rolvaliduntil,
|
|
66
|
-
rolsuper, rolcreaterole, rolcreatedb, rolreplication, rolbypassrls
|
|
67
|
-
from pg_roles where rolname = current_user`,
|
|
68
|
-
);
|
|
69
|
-
const role = roleResult.rows[0];
|
|
70
|
-
if (!role || role.role !== plan.policy.reader.role || !role.rolcanlogin) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
"External source credential does not authenticate as the reviewed login role.",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (
|
|
76
|
-
role.rolsuper ||
|
|
77
|
-
role.rolcreaterole ||
|
|
78
|
-
role.rolcreatedb ||
|
|
79
|
-
role.rolreplication ||
|
|
80
|
-
role.rolbypassrls
|
|
81
|
-
) {
|
|
82
|
-
throw new Error("External source reader has privileged role attributes.");
|
|
83
|
-
}
|
|
84
|
-
const expiresAt = new Date(role.rolvaliduntil);
|
|
85
|
-
const maximumExpiration =
|
|
86
|
-
now.valueOf() + plan.policy.reader.maximumValidForMinutes * 60_000;
|
|
87
|
-
if (
|
|
88
|
-
!Number.isFinite(expiresAt.valueOf()) ||
|
|
89
|
-
expiresAt.valueOf() <= now.valueOf() ||
|
|
90
|
-
expiresAt.valueOf() > maximumExpiration
|
|
91
|
-
) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
"External source reader must have a future database-enforced expiration within the reviewed limit.",
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const memberships = await client.query(
|
|
98
|
-
`select count(*)::integer as count
|
|
99
|
-
from pg_auth_members membership
|
|
100
|
-
join pg_roles member on member.oid = membership.member
|
|
101
|
-
where member.rolname = current_user`,
|
|
102
|
-
);
|
|
103
|
-
if (count(memberships, "role membership") !== 0) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
"External source reader belongs to another database role.",
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const databaseWrites = await client.query(
|
|
110
|
-
`select count(*)::integer as count
|
|
111
|
-
where has_database_privilege(current_user, current_database(), 'CREATE')`,
|
|
112
|
-
);
|
|
113
|
-
const schemaWrites = await client.query(
|
|
114
|
-
`select count(*)::integer as count
|
|
115
|
-
from pg_namespace
|
|
116
|
-
where nspname not like 'pg_temp_%'
|
|
117
|
-
and has_schema_privilege(current_user, oid, 'CREATE')`,
|
|
118
|
-
);
|
|
119
|
-
const relationWrites = await client.query(
|
|
120
|
-
`select count(*)::integer as count
|
|
121
|
-
from pg_class relation
|
|
122
|
-
join pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
123
|
-
where namespace.nspname not in ('pg_catalog', 'information_schema')
|
|
124
|
-
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
125
|
-
and (has_table_privilege(current_user, relation.oid, 'INSERT')
|
|
126
|
-
or has_table_privilege(current_user, relation.oid, 'UPDATE')
|
|
127
|
-
or has_table_privilege(current_user, relation.oid, 'DELETE')
|
|
128
|
-
or has_table_privilege(current_user, relation.oid, 'TRUNCATE')
|
|
129
|
-
or has_table_privilege(current_user, relation.oid, 'REFERENCES')
|
|
130
|
-
or has_table_privilege(current_user, relation.oid, 'TRIGGER'))`,
|
|
131
|
-
);
|
|
132
|
-
const columnWrites = await client.query(
|
|
133
|
-
`select count(*)::integer as count
|
|
134
|
-
from pg_class relation
|
|
135
|
-
join pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
136
|
-
join pg_attribute attribute on attribute.attrelid = relation.oid
|
|
137
|
-
where namespace.nspname not in ('pg_catalog', 'information_schema')
|
|
138
|
-
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
139
|
-
and attribute.attnum > 0 and not attribute.attisdropped
|
|
140
|
-
and (has_column_privilege(current_user, relation.oid, attribute.attnum, 'INSERT')
|
|
141
|
-
or has_column_privilege(current_user, relation.oid, attribute.attnum, 'UPDATE')
|
|
142
|
-
or has_column_privilege(current_user, relation.oid, attribute.attnum, 'REFERENCES'))`,
|
|
143
|
-
);
|
|
144
|
-
const sequenceAccess = await client.query(
|
|
145
|
-
`select count(*)::integer as count
|
|
146
|
-
from pg_class sequence
|
|
147
|
-
join pg_namespace namespace on namespace.oid = sequence.relnamespace
|
|
148
|
-
where namespace.nspname not in ('pg_catalog', 'information_schema')
|
|
149
|
-
and sequence.relkind = 'S'
|
|
150
|
-
and (has_sequence_privilege(current_user, sequence.oid, 'SELECT')
|
|
151
|
-
or has_sequence_privilege(current_user, sequence.oid, 'USAGE')
|
|
152
|
-
or has_sequence_privilege(current_user, sequence.oid, 'UPDATE'))`,
|
|
153
|
-
);
|
|
154
|
-
if (
|
|
155
|
-
[
|
|
156
|
-
databaseWrites,
|
|
157
|
-
schemaWrites,
|
|
158
|
-
relationWrites,
|
|
159
|
-
columnWrites,
|
|
160
|
-
sequenceAccess,
|
|
161
|
-
].some((result) => count(result, "write or sequence privilege") !== 0)
|
|
162
|
-
) {
|
|
163
|
-
throw new Error(
|
|
164
|
-
"External source reader has database write or sequence privileges.",
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const readable = await client.query(
|
|
169
|
-
`select namespace.nspname as schema, relation.relname as relation,
|
|
170
|
-
attribute.attname as column
|
|
171
|
-
from pg_class relation
|
|
172
|
-
join pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
173
|
-
join pg_attribute attribute on attribute.attrelid = relation.oid
|
|
174
|
-
where namespace.nspname not in ('pg_catalog', 'information_schema')
|
|
175
|
-
and namespace.nspname not like 'pg_toast%'
|
|
176
|
-
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
177
|
-
and attribute.attnum > 0 and not attribute.attisdropped
|
|
178
|
-
and has_column_privilege(current_user, relation.oid, attribute.attnum, 'SELECT')
|
|
179
|
-
order by namespace.nspname, relation.relname, attribute.attnum`,
|
|
180
|
-
);
|
|
181
|
-
const expected = expectedReadableColumns(plan);
|
|
182
|
-
const actual = new Set(
|
|
183
|
-
readable.rows.map((row) =>
|
|
184
|
-
columnKey(row.schema, row.relation, row.column),
|
|
185
|
-
),
|
|
186
|
-
);
|
|
187
|
-
if (
|
|
188
|
-
actual.size !== expected.size ||
|
|
189
|
-
[...actual].some((entry) => !expected.has(entry))
|
|
190
|
-
) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
"External source reader's readable columns do not exactly match the reviewed export surface.",
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
for (const relation of plan.policy.relations) {
|
|
197
|
-
const view = await client.query(
|
|
198
|
-
`select pg_get_viewdef(relation.oid, false) as definition,
|
|
199
|
-
relation.reloptions,
|
|
200
|
-
owner.rolname as owner
|
|
201
|
-
from pg_class relation
|
|
202
|
-
join pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
203
|
-
join pg_roles owner on owner.oid = relation.relowner
|
|
204
|
-
where namespace.nspname = $1 and relation.relname = $2
|
|
205
|
-
and relation.relkind = 'v'`,
|
|
206
|
-
[plan.policy.exportSchema, relation.view],
|
|
207
|
-
);
|
|
208
|
-
const actual = view.rows[0];
|
|
209
|
-
if (!actual) {
|
|
210
|
-
throw new Error("An externally reviewed export view is missing.");
|
|
211
|
-
}
|
|
212
|
-
if (
|
|
213
|
-
!Array.isArray(actual.reloptions) ||
|
|
214
|
-
!actual.reloptions.includes("security_barrier=true")
|
|
215
|
-
) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
"External export views must enable PostgreSQL security_barrier.",
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
if (actual.owner === plan.policy.reader.role) {
|
|
221
|
-
throw new Error("External source reader must not own an export view.");
|
|
222
|
-
}
|
|
223
|
-
if (
|
|
224
|
-
externalViewDefinitionFingerprint(actual.definition) !==
|
|
225
|
-
relation.viewDefinitionSha256
|
|
226
|
-
) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
"External export view definition does not match its reviewed fingerprint.",
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const securityDefiners = await client.query(
|
|
234
|
-
`select count(*)::integer as count
|
|
235
|
-
from pg_proc procedure
|
|
236
|
-
join pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
237
|
-
where namespace.nspname not in ('pg_catalog', 'information_schema')
|
|
238
|
-
and procedure.prosecdef
|
|
239
|
-
and has_function_privilege(current_user, procedure.oid, 'EXECUTE')`,
|
|
240
|
-
);
|
|
241
|
-
if (count(securityDefiners, "security-definer function") !== 0) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
"External source reader can execute a non-system security-definer function.",
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
await client.query("rollback");
|
|
247
|
-
transactionOpen = false;
|
|
248
|
-
return Object.freeze({ expiresAt: expiresAt.toISOString() });
|
|
249
|
-
} catch (error) {
|
|
250
|
-
if (transactionOpen) await client.query("rollback").catch(() => undefined);
|
|
251
|
-
throw error;
|
|
252
|
-
} finally {
|
|
253
|
-
await client.end?.();
|
|
254
|
-
}
|
|
255
|
-
};
|