@xemahq/biome-database-nest 0.7.0 → 0.9.0

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.
@@ -3,7 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.verifyExternallyManagedRuntime = verifyExternallyManagedRuntime;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const pg_1 = require("pg");
6
+ const credential_generation_1 = require("./credential-generation");
7
+ const migration_mode_1 = require("./migration-mode");
6
8
  const LOG_PREFIX = '[biome-database]';
9
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ const SHA_256_PATTERN = /^[0-9a-f]{64}$/;
11
+ const ALL_TABLE_DML = Object.values(migration_mode_1.RuntimeTablePrivilege);
12
+ const ALL_TABLE_PRIVILEGES = [
13
+ ...ALL_TABLE_DML,
14
+ 'TRUNCATE',
15
+ 'REFERENCES',
16
+ 'TRIGGER',
17
+ ];
18
+ const ALL_SEQUENCE_PRIVILEGES = Object.values(migration_mode_1.RuntimeSequencePrivilege);
19
+ const TABLE_DML_SET = new Set(ALL_TABLE_DML);
20
+ const SEQUENCE_PRIVILEGE_SET = new Set(ALL_SEQUENCE_PRIVILEGES);
7
21
  function quoteIdentifier(identifier) {
8
22
  return `"${identifier.replaceAll('"', '""')}"`;
9
23
  }
@@ -19,60 +33,499 @@ function assertNonEmpty(value, field) {
19
33
  }
20
34
  }
21
35
  function assertExpectation(expectation) {
22
- if (expectation.tablePrivileges.length === 0) {
36
+ if (!Array.isArray(expectation.tablePrivileges) ||
37
+ expectation.tablePrivileges.length === 0) {
23
38
  throw new Error(`${LOG_PREFIX} externally-managed runtime access must require at least one table privilege.`);
24
39
  }
40
+ if (!Array.isArray(expectation.writableRelations)) {
41
+ throw new Error(`${LOG_PREFIX} externally-managed runtime access must explicitly declare writableRelations.`);
42
+ }
43
+ const writableRelationKeys = new Set();
44
+ for (const relation of expectation.writableRelations) {
45
+ assertNonEmpty(relation.name, 'writable relation name');
46
+ const relationPrivileges = relation.privileges;
47
+ if (!Object.values(migration_mode_1.RuntimeWritableRelationKind).includes(relation.kind)) {
48
+ throw new Error(`${LOG_PREFIX} writable relation kind is invalid.`);
49
+ }
50
+ if (!Array.isArray(relationPrivileges) ||
51
+ !relationPrivileges.includes(migration_mode_1.RuntimeTablePrivilege.Select) ||
52
+ !relationPrivileges.some((privilege) => privilege !== migration_mode_1.RuntimeTablePrivilege.Select) ||
53
+ relationPrivileges.some((privilege) => !TABLE_DML_SET.has(privilege)) ||
54
+ new Set(relationPrivileges).size !== relationPrivileges.length) {
55
+ throw new Error(`${LOG_PREFIX} writable relation ${relation.name} must declare unique SELECT plus at least one DML privilege.`);
56
+ }
57
+ const key = `${relation.kind}\u0000${relation.name}`;
58
+ if (writableRelationKeys.has(key)) {
59
+ throw new Error(`${LOG_PREFIX} writableRelations contains duplicate ${relation.name}.`);
60
+ }
61
+ writableRelationKeys.add(key);
62
+ }
63
+ if (expectation.tablePrivileges.some((privilege) => !TABLE_DML_SET.has(privilege)) ||
64
+ new Set(expectation.tablePrivileges).size !==
65
+ expectation.tablePrivileges.length) {
66
+ throw new Error(`${LOG_PREFIX} runtime table privileges must be unique RuntimeTablePrivilege values.`);
67
+ }
68
+ if (!Array.isArray(expectation.sequencePrivileges) ||
69
+ expectation.sequencePrivileges.some((privilege) => !SEQUENCE_PRIVILEGE_SET.has(privilege)) ||
70
+ new Set(expectation.sequencePrivileges).size !==
71
+ expectation.sequencePrivileges.length) {
72
+ throw new Error(`${LOG_PREFIX} runtime sequence privileges must be unique RuntimeSequencePrivilege values.`);
73
+ }
74
+ const memberships = expectation.authorizedRoleMemberships;
75
+ if (!Array.isArray(memberships) || memberships.length !== 0) {
76
+ throw new Error(`${LOG_PREFIX} externally-managed runtime access forbids all role memberships; authorizedRoleMemberships must be an empty tuple.`);
77
+ }
78
+ if (!Array.isArray(expectation.targetFunctions)) {
79
+ throw new Error(`${LOG_PREFIX} externally-managed runtime access must explicitly declare targetFunctions.`);
80
+ }
81
+ const functionKeys = new Set();
82
+ for (const fn of expectation.targetFunctions) {
83
+ assertNonEmpty(fn.name, 'target function name');
84
+ if (typeof fn.identityArguments !== 'string') {
85
+ throw new Error(`${LOG_PREFIX} target function identityArguments must be a string.`);
86
+ }
87
+ if (!Array.isArray(fn.privileges) ||
88
+ fn.privileges.length !== 1 ||
89
+ fn.privileges[0] !== migration_mode_1.RuntimeExtensionFunctionPrivilege.Execute) {
90
+ throw new Error(`${LOG_PREFIX} target function ${fn.name} must declare exactly EXECUTE.`);
91
+ }
92
+ const key = `${fn.name}\u0000${fn.identityArguments}`;
93
+ if (functionKeys.has(key)) {
94
+ throw new Error(`${LOG_PREFIX} targetFunctions contains duplicate ${fn.name}.`);
95
+ }
96
+ functionKeys.add(key);
97
+ }
98
+ assertExtensionDependencies(expectation.extensionDependencies);
25
99
  }
26
- function buildMissingTablePrivilegeQuery(privileges) {
27
- const projections = privileges
28
- .map((privilege) => `CASE WHEN NOT has_table_privilege(current_user, ` +
29
- `format('%I.%I', table_schema, table_name), '${privilege}') ` +
30
- `THEN '${privilege}' END`)
31
- .join(', ');
100
+ function extensionObjectKey(extensionName, schema, object) {
101
+ const identityArguments = object.kind === migration_mode_1.RuntimeExtensionObjectKind.Function
102
+ ? object.identityArguments
103
+ : '';
104
+ return [
105
+ extensionName,
106
+ object.kind,
107
+ schema,
108
+ object.name,
109
+ identityArguments,
110
+ ].join('\u0000');
111
+ }
112
+ function assertExtensionDependencies(dependencies) {
113
+ if (!Array.isArray(dependencies)) {
114
+ throw new Error(`${LOG_PREFIX} externally-managed runtime access must explicitly declare extensionDependencies.`);
115
+ }
116
+ const names = new Set();
117
+ const objectKeys = new Set();
118
+ for (const dependency of dependencies) {
119
+ assertNonEmpty(dependency.name, 'extension dependency name');
120
+ assertNonEmpty(dependency.schema, 'extension dependency schema');
121
+ assertNonEmpty(dependency.version, 'extension dependency version');
122
+ if (names.has(dependency.name)) {
123
+ throw new Error(`${LOG_PREFIX} extensionDependencies contains duplicate extension ${dependency.name}.`);
124
+ }
125
+ names.add(dependency.name);
126
+ if (!Array.isArray(dependency.objects) || dependency.objects.length === 0) {
127
+ throw new Error(`${LOG_PREFIX} extension dependency ${dependency.name} must declare at least one object.`);
128
+ }
129
+ for (const object of dependency.objects) {
130
+ assertNonEmpty(object.name, `${dependency.name} extension object name`);
131
+ const expectedPrivilege = (() => {
132
+ switch (object.kind) {
133
+ case migration_mode_1.RuntimeExtensionObjectKind.Type:
134
+ return migration_mode_1.RuntimeExtensionTypePrivilege.Usage;
135
+ case migration_mode_1.RuntimeExtensionObjectKind.Function:
136
+ if (typeof object.identityArguments !== 'string') {
137
+ throw new Error(`${LOG_PREFIX} ${dependency.name}.${object.name} function identityArguments must be a string.`);
138
+ }
139
+ return migration_mode_1.RuntimeExtensionFunctionPrivilege.Execute;
140
+ case migration_mode_1.RuntimeExtensionObjectKind.Table:
141
+ return migration_mode_1.RuntimeExtensionTablePrivilege.Select;
142
+ case migration_mode_1.RuntimeExtensionObjectKind.Sequence:
143
+ return migration_mode_1.RuntimeExtensionSequencePrivilege.Select;
144
+ default:
145
+ throw new Error(`${LOG_PREFIX} extension dependency ${dependency.name} contains an unsupported object kind.`);
146
+ }
147
+ })();
148
+ if (!Array.isArray(object.privileges) ||
149
+ object.privileges.length !== 1 ||
150
+ object.privileges[0] !== expectedPrivilege) {
151
+ throw new Error(`${LOG_PREFIX} extension object ${dependency.schema}.${object.name} must declare exactly ${expectedPrivilege}.`);
152
+ }
153
+ const key = extensionObjectKey(dependency.name, dependency.schema, object);
154
+ if (objectKeys.has(key)) {
155
+ throw new Error(`${LOG_PREFIX} extensionDependencies contains duplicate object ${dependency.schema}.${object.name}.`);
156
+ }
157
+ objectKeys.add(key);
158
+ }
159
+ }
160
+ }
161
+ function buildTablePrivilegeQuery(crossSchema) {
162
+ const projections = ALL_TABLE_PRIVILEGES.map((privilege) => `CASE WHEN has_table_privilege(current_user, c.oid, '${privilege}') ` +
163
+ `THEN '${privilege}' END`).join(', ');
164
+ const publicProjections = ALL_TABLE_PRIVILEGES.map((privilege) => `CASE WHEN EXISTS (` +
165
+ `SELECT 1 FROM aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner))) acl ` +
166
+ `WHERE acl.grantee = 0 AND acl.privilege_type = '${privilege}'` +
167
+ `) THEN '${privilege}' END`).join(', ');
32
168
  return `
33
- SELECT table_name AS object_name,
34
- array_remove(ARRAY[${projections}], NULL) AS missing_privileges
35
- FROM information_schema.tables
36
- WHERE table_schema = $1
37
- AND table_type = 'BASE TABLE'
38
- AND table_name <> '_prisma_migrations'
39
- AND NOT (
40
- ${privileges
41
- .map((privilege) => `has_table_privilege(current_user, ` +
42
- `format('%I.%I', table_schema, table_name), '${privilege}')`)
43
- .join(' AND ')}
44
- )
45
- ORDER BY table_name
169
+ SELECT n.nspname AS schema_name,
170
+ c.relname AS object_name,
171
+ c.relkind AS relation_kind,
172
+ array_remove(ARRAY[${projections}], NULL) AS privileges,
173
+ e.extname AS extension_name,
174
+ array_remove(ARRAY[${publicProjections}], NULL) AS public_privileges,
175
+ pg_has_role(current_user, c.relowner, 'USAGE') AS owns_object
176
+ FROM pg_class c
177
+ JOIN pg_namespace n ON n.oid = c.relnamespace
178
+ LEFT JOIN pg_depend d
179
+ ON d.classid = 'pg_class'::regclass
180
+ AND d.objid = c.oid
181
+ AND d.objsubid = 0
182
+ AND d.refclassid = 'pg_extension'::regclass
183
+ AND d.deptype = 'e'
184
+ LEFT JOIN pg_extension e ON e.oid = d.refobjid
185
+ WHERE n.nspname ${crossSchema ? '<>' : '='} $1
186
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
187
+ AND n.nspname NOT LIKE 'pg_toast%'
188
+ AND n.nspname NOT LIKE 'pg_temp_%'
189
+ AND c.relkind IN ('r', 'p', 'v', 'm', 'f')
190
+ AND c.relname <> '_prisma_migrations'
191
+ ${crossSchema
192
+ ? `AND (${ALL_TABLE_PRIVILEGES.map((privilege) => `has_table_privilege(current_user, c.oid, '${privilege}')`).join(' OR ')})`
193
+ : ''}
194
+ ORDER BY n.nspname, c.relname
46
195
  `;
47
196
  }
48
- function buildMissingSequencePrivilegeQuery(privileges) {
49
- if (privileges.length === 0) {
50
- return null;
51
- }
52
- const projections = privileges
53
- .map((privilege) => `CASE WHEN NOT has_sequence_privilege(current_user, ` +
54
- `format('%I.%I', sequence_schema, sequence_name), '${privilege}') ` +
55
- `THEN '${privilege}' END`)
56
- .join(', ');
197
+ function buildSequencePrivilegeQuery(crossSchema) {
198
+ const projections = ALL_SEQUENCE_PRIVILEGES.map((privilege) => `CASE WHEN has_sequence_privilege(current_user, c.oid, '${privilege}') ` +
199
+ `THEN '${privilege}' END`).join(', ');
200
+ const publicProjections = ALL_SEQUENCE_PRIVILEGES.map((privilege) => `CASE WHEN EXISTS (` +
201
+ `SELECT 1 FROM aclexplode(COALESCE(c.relacl, acldefault('s', c.relowner))) acl ` +
202
+ `WHERE acl.grantee = 0 AND acl.privilege_type = '${privilege}'` +
203
+ `) THEN '${privilege}' END`).join(', ');
57
204
  return `
58
- SELECT sequence_name AS object_name,
59
- array_remove(ARRAY[${projections}], NULL) AS missing_privileges
60
- FROM information_schema.sequences
61
- WHERE sequence_schema = $1
62
- AND NOT (
63
- ${privileges
64
- .map((privilege) => `has_sequence_privilege(current_user, ` +
65
- `format('%I.%I', sequence_schema, sequence_name), '${privilege}')`)
66
- .join(' AND ')}
67
- )
68
- ORDER BY sequence_name
205
+ SELECT n.nspname AS schema_name,
206
+ c.relname AS object_name,
207
+ array_remove(ARRAY[${projections}], NULL) AS privileges,
208
+ e.extname AS extension_name,
209
+ array_remove(ARRAY[${publicProjections}], NULL) AS public_privileges,
210
+ pg_has_role(current_user, c.relowner, 'USAGE') AS owns_object
211
+ FROM pg_class c
212
+ JOIN pg_namespace n ON n.oid = c.relnamespace
213
+ LEFT JOIN pg_depend d
214
+ ON d.classid = 'pg_class'::regclass
215
+ AND d.objid = c.oid
216
+ AND d.objsubid = 0
217
+ AND d.refclassid = 'pg_extension'::regclass
218
+ AND d.deptype = 'e'
219
+ LEFT JOIN pg_extension e ON e.oid = d.refobjid
220
+ WHERE n.nspname ${crossSchema ? '<>' : '='} $1
221
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
222
+ AND n.nspname NOT LIKE 'pg_toast%'
223
+ AND n.nspname NOT LIKE 'pg_temp_%'
224
+ AND c.relkind = 'S'
225
+ ${crossSchema
226
+ ? `AND (${ALL_SEQUENCE_PRIVILEGES.map((privilege) => `has_sequence_privilege(current_user, c.oid, '${privilege}')`).join(' OR ')})`
227
+ : ''}
228
+ ORDER BY n.nspname, c.relname
69
229
  `;
70
230
  }
71
- function formatMissingPrivileges(kind, rows) {
231
+ function formatCrossSchemaPrivileges(kind, rows) {
72
232
  return rows
73
- .map((row) => `${kind} ${row.object_name} missing ${row.missing_privileges.join('/')}`)
233
+ .map((row) => `${kind} ${row.schema_name}.${row.object_name} ` +
234
+ `[${row.privileges.join('/')}]`)
74
235
  .join(', ');
75
236
  }
237
+ function samePrivileges(observed, expected) {
238
+ const sortedObserved = [...observed].sort();
239
+ const sortedExpected = [...expected].sort();
240
+ return (sortedObserved.length === sortedExpected.length &&
241
+ sortedObserved.every((privilege, index) => privilege === sortedExpected[index]));
242
+ }
243
+ function expectedRelationPrivileges(row, tablePrivileges, writableRelations) {
244
+ if (row.relation_kind === 'r' || row.relation_kind === 'p') {
245
+ return tablePrivileges;
246
+ }
247
+ if (row.relation_kind === 'm') {
248
+ return [migration_mode_1.RuntimeTablePrivilege.Select];
249
+ }
250
+ const writableKind = row.relation_kind === 'v'
251
+ ? migration_mode_1.RuntimeWritableRelationKind.View
252
+ : migration_mode_1.RuntimeWritableRelationKind.ForeignTable;
253
+ return (writableRelations.find((relation) => relation.kind === writableKind && relation.name === row.object_name)?.privileges ?? [migration_mode_1.RuntimeTablePrivilege.Select]);
254
+ }
255
+ function assertExactTargetPrivileges(kind, rows, expected, writableRelations) {
256
+ if (kind === 'table') {
257
+ const observedWritableRelations = new Set(rows.flatMap((row) => {
258
+ if (!('relation_kind' in row))
259
+ return [];
260
+ const writableKind = row.relation_kind === 'v'
261
+ ? migration_mode_1.RuntimeWritableRelationKind.View
262
+ : row.relation_kind === 'f'
263
+ ? migration_mode_1.RuntimeWritableRelationKind.ForeignTable
264
+ : null;
265
+ return writableKind === null
266
+ ? []
267
+ : [`${writableKind}\u0000${row.object_name}`];
268
+ }));
269
+ const missingDeclarations = writableRelations.filter((relation) => !observedWritableRelations.has(`${relation.kind}\u0000${relation.name}`));
270
+ if (missingDeclarations.length > 0) {
271
+ throw new Error(`${LOG_PREFIX} writableRelations names missing target view/foreign table: ` +
272
+ missingDeclarations.map((relation) => relation.name).join(', '));
273
+ }
274
+ }
275
+ const mismatches = rows.filter((row) => {
276
+ const applicableExpected = kind === 'table' && 'relation_kind' in row
277
+ ? expectedRelationPrivileges(row, expected, writableRelations)
278
+ : expected;
279
+ return (!samePrivileges(row.privileges, applicableExpected) ||
280
+ row.public_privileges.length > 0 ||
281
+ row.owns_object);
282
+ });
283
+ if (mismatches.length > 0) {
284
+ throw new Error(`${LOG_PREFIX} runtime ${kind} privilege contract failed: ${mismatches
285
+ .map((row) => {
286
+ const applicableExpected = kind === 'table' && 'relation_kind' in row
287
+ ? expectedRelationPrivileges(row, expected, writableRelations)
288
+ : expected;
289
+ return (`${row.schema_name}.${row.object_name} expected [${applicableExpected.join('/')}], observed [${row.privileges.join('/')}], PUBLIC [` +
290
+ `${row.public_privileges.join('/')}], owns=${row.owns_object}`);
291
+ })
292
+ .join(', ')}.`);
293
+ }
294
+ }
295
+ function assertTargetTypes(rows) {
296
+ const violations = rows.filter((row) => !row.can_use || row.public_has_privilege || row.owns_object);
297
+ if (violations.length > 0) {
298
+ throw new Error(`${LOG_PREFIX} target type privilege contract failed: ${violations
299
+ .map((row) => `${row.object_name} kind=${row.type_kind} USAGE=${row.can_use} ` +
300
+ `PUBLIC=${row.public_has_privilege} owns=${row.owns_object}`)
301
+ .join(', ')}.`);
302
+ }
303
+ }
304
+ function functionIdentityKey(name, identityArguments) {
305
+ return `${name}\u0000${identityArguments}`;
306
+ }
307
+ function hasSafeTargetFunctionConfiguration(configuration, targetSchema) {
308
+ if (configuration === null || configuration.length !== 1) {
309
+ return false;
310
+ }
311
+ const prefix = 'search_path=';
312
+ const setting = configuration[0];
313
+ if (!setting.startsWith(prefix)) {
314
+ return false;
315
+ }
316
+ const path = setting
317
+ .slice(prefix.length)
318
+ .split(',')
319
+ .map((schema) => schema.trim());
320
+ return (path.length === 2 && path[0] === 'pg_catalog' && path[1] === targetSchema);
321
+ }
322
+ function assertTargetFunctions(rows, expectedFunctions, targetSchema) {
323
+ const ownership = rows.filter((row) => row.owns_object);
324
+ const publicExecute = rows.filter((row) => row.public_has_privilege);
325
+ const executable = rows.filter((row) => row.can_execute);
326
+ const expected = new Set(expectedFunctions.map((fn) => functionIdentityKey(fn.name, fn.identityArguments)));
327
+ const observed = new Set(executable.map((row) => functionIdentityKey(row.object_name, row.identity_arguments)));
328
+ const missing = [...expected].filter((key) => !observed.has(key));
329
+ const extra = [...observed].filter((key) => !expected.has(key));
330
+ const unsafe = executable.filter((row) => row.security_definer ||
331
+ !hasSafeTargetFunctionConfiguration(row.configuration, targetSchema));
332
+ if (ownership.length > 0 ||
333
+ publicExecute.length > 0 ||
334
+ missing.length > 0 ||
335
+ extra.length > 0 ||
336
+ unsafe.length > 0) {
337
+ throw new Error(`${LOG_PREFIX} target function privilege contract failed: ` +
338
+ `owned=${JSON.stringify(ownership.map((row) => functionIdentityKey(row.object_name, row.identity_arguments)))}, PUBLIC=${JSON.stringify(publicExecute.map((row) => functionIdentityKey(row.object_name, row.identity_arguments)))}, missing=${JSON.stringify(missing)}, extra=${JSON.stringify(extra)}, ` +
339
+ `unsafe=${JSON.stringify(unsafe.map((row) => ({
340
+ function: functionIdentityKey(row.object_name, row.identity_arguments),
341
+ securityDefiner: row.security_definer,
342
+ configuration: row.configuration ?? [],
343
+ })))}.`);
344
+ }
345
+ }
346
+ function findDependency(dependencies, extensionName, schema) {
347
+ if (extensionName === null) {
348
+ return undefined;
349
+ }
350
+ return dependencies.find((dependency) => dependency.name === extensionName && dependency.schema === schema);
351
+ }
352
+ function expectedExtensionObjectKeys(dependencies) {
353
+ return new Set(dependencies.flatMap((dependency) => dependency.objects.map((object) => extensionObjectKey(dependency.name, dependency.schema, object))));
354
+ }
355
+ function assertExtensionMetadata(expected, observed) {
356
+ const actualByName = new Map(observed.map((row) => [row.extension_name, row]));
357
+ const mismatches = expected.filter((dependency) => {
358
+ const actual = actualByName.get(dependency.name);
359
+ return (actual === undefined ||
360
+ actual.schema_name !== dependency.schema ||
361
+ actual.extension_version !== dependency.version ||
362
+ actual.owns_extension);
363
+ });
364
+ if (observed.length !== expected.length || mismatches.length > 0) {
365
+ throw new Error(`${LOG_PREFIX} extension dependency mismatch: expected ${expected
366
+ .map((dependency) => `${dependency.name}@${dependency.version} in ${dependency.schema}`)
367
+ .join(', ')}, observed ${observed
368
+ .map((row) => `${row.extension_name}@${row.extension_version} in ${row.schema_name} owns=${row.owns_extension}`)
369
+ .join(', ')}.`);
370
+ }
371
+ }
372
+ function assertExactExtensionObjects(dependencies, types, functions, tables, sequences) {
373
+ const expected = expectedExtensionObjectKeys(dependencies);
374
+ const observed = new Set();
375
+ const publicObjects = [];
376
+ const invalidMembership = [];
377
+ for (const row of types) {
378
+ const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
379
+ const key = extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
380
+ kind: migration_mode_1.RuntimeExtensionObjectKind.Type,
381
+ name: row.object_name,
382
+ privileges: [migration_mode_1.RuntimeExtensionTypePrivilege.Usage],
383
+ });
384
+ observed.add(key);
385
+ if (dependency === undefined) {
386
+ invalidMembership.push(`${row.schema_name}.${row.object_name}`);
387
+ }
388
+ if (row.owns_object) {
389
+ invalidMembership.push(`${row.schema_name}.${row.object_name} owned by runtime`);
390
+ }
391
+ if (row.public_has_privilege) {
392
+ publicObjects.push(`${row.schema_name}.${row.object_name} TYPE USAGE`);
393
+ }
394
+ }
395
+ for (const row of functions) {
396
+ const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
397
+ const key = extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
398
+ kind: migration_mode_1.RuntimeExtensionObjectKind.Function,
399
+ name: row.object_name,
400
+ identityArguments: row.identity_arguments,
401
+ privileges: [migration_mode_1.RuntimeExtensionFunctionPrivilege.Execute],
402
+ });
403
+ observed.add(key);
404
+ if (dependency === undefined) {
405
+ invalidMembership.push(`${row.schema_name}.${row.object_name}(${row.identity_arguments})`);
406
+ }
407
+ if (row.owns_object ||
408
+ row.security_definer ||
409
+ (row.configuration !== null && row.configuration.length > 0)) {
410
+ invalidMembership.push(`${row.schema_name}.${row.object_name}(${row.identity_arguments}) ` +
411
+ `owner=${row.owns_object} securityDefiner=${row.security_definer} ` +
412
+ `configuration=${JSON.stringify(row.configuration ?? [])}`);
413
+ }
414
+ if (row.public_has_privilege) {
415
+ publicObjects.push(`${row.schema_name}.${row.object_name}(${row.identity_arguments}) EXECUTE`);
416
+ }
417
+ }
418
+ for (const row of tables) {
419
+ const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
420
+ if (dependency === undefined ||
421
+ !samePrivileges(row.privileges, [
422
+ migration_mode_1.RuntimeExtensionTablePrivilege.Select,
423
+ ]) ||
424
+ row.owns_object) {
425
+ invalidMembership.push(`${row.schema_name}.${row.object_name}`);
426
+ }
427
+ observed.add(extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
428
+ kind: migration_mode_1.RuntimeExtensionObjectKind.Table,
429
+ name: row.object_name,
430
+ privileges: [migration_mode_1.RuntimeExtensionTablePrivilege.Select],
431
+ }));
432
+ if (row.public_privileges.length > 0) {
433
+ publicObjects.push(`${row.schema_name}.${row.object_name} [${row.public_privileges.join('/')}]`);
434
+ }
435
+ }
436
+ for (const row of sequences) {
437
+ const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
438
+ if (dependency === undefined ||
439
+ !samePrivileges(row.privileges, [
440
+ migration_mode_1.RuntimeExtensionSequencePrivilege.Select,
441
+ ]) ||
442
+ row.owns_object) {
443
+ invalidMembership.push(`${row.schema_name}.${row.object_name}`);
444
+ }
445
+ observed.add(extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
446
+ kind: migration_mode_1.RuntimeExtensionObjectKind.Sequence,
447
+ name: row.object_name,
448
+ privileges: [migration_mode_1.RuntimeExtensionSequencePrivilege.Select],
449
+ }));
450
+ if (row.public_privileges.length > 0) {
451
+ publicObjects.push(`${row.schema_name}.${row.object_name} [${row.public_privileges.join('/')}]`);
452
+ }
453
+ }
454
+ const missing = [...expected].filter((key) => !observed.has(key));
455
+ const extra = [...observed].filter((key) => !expected.has(key));
456
+ if (missing.length > 0 ||
457
+ extra.length > 0 ||
458
+ invalidMembership.length > 0 ||
459
+ publicObjects.length > 0) {
460
+ throw new Error(`${LOG_PREFIX} extension object privilege contract failed: ` +
461
+ `missing=${JSON.stringify(missing)}, extra=${JSON.stringify(extra)}, ` +
462
+ `invalidExtensionMembership=${JSON.stringify(invalidMembership)}, ` +
463
+ `PUBLIC=${JSON.stringify(publicObjects)}.`);
464
+ }
465
+ }
466
+ function assertNoRoleMemberships(observedRows) {
467
+ if (observedRows.length > 0) {
468
+ throw new Error(`${LOG_PREFIX} runtime role has forbidden direct or transitive membership(s): ` +
469
+ observedRows
470
+ .map((row) => row.role_name)
471
+ .sort()
472
+ .join(', '));
473
+ }
474
+ }
475
+ function parseLedgerTimestamp(value, migrationName, field) {
476
+ const milliseconds = Date.parse(value);
477
+ if (!Number.isFinite(milliseconds)) {
478
+ throw new Error(`${LOG_PREFIX} migration ${migrationName} has invalid ${field} "${value}".`);
479
+ }
480
+ return milliseconds;
481
+ }
482
+ function validateMigrationLedger(rows) {
483
+ const ids = new Set();
484
+ const names = new Set();
485
+ return rows.map((row) => {
486
+ if (!UUID_PATTERN.test(row.id)) {
487
+ throw new Error(`${LOG_PREFIX} migration ledger row has invalid id "${row.id}".`);
488
+ }
489
+ assertNonEmpty(row.migration_name, 'migration name');
490
+ if (ids.has(row.id) || names.has(row.migration_name)) {
491
+ throw new Error(`${LOG_PREFIX} migration ledger contains a duplicate id or migration name: ${row.migration_name}.`);
492
+ }
493
+ ids.add(row.id);
494
+ names.add(row.migration_name);
495
+ if (!SHA_256_PATTERN.test(row.checksum)) {
496
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} has an invalid SHA-256 checksum.`);
497
+ }
498
+ if (row.started_at === null) {
499
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} has no started_at timestamp.`);
500
+ }
501
+ const startedAt = parseLedgerTimestamp(row.started_at, row.migration_name, 'started_at');
502
+ if (row.rolled_back_at !== null) {
503
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} is rolled back.`);
504
+ }
505
+ if (row.finished_at === null) {
506
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} is unfinished or failed.`);
507
+ }
508
+ if (row.logs !== null) {
509
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} contains failure logs.`);
510
+ }
511
+ const finishedAt = parseLedgerTimestamp(row.finished_at, row.migration_name, 'finished_at');
512
+ if (finishedAt < startedAt) {
513
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} finished before it started.`);
514
+ }
515
+ if (!Number.isInteger(row.applied_steps_count) ||
516
+ row.applied_steps_count <= 0) {
517
+ throw new Error(`${LOG_PREFIX} migration ${row.migration_name} has invalid applied_steps_count ${row.applied_steps_count}.`);
518
+ }
519
+ return {
520
+ id: row.id,
521
+ name: row.migration_name,
522
+ checksum: row.checksum,
523
+ startedAt: row.started_at,
524
+ finishedAt: row.finished_at,
525
+ appliedStepsCount: row.applied_steps_count,
526
+ };
527
+ });
528
+ }
76
529
  function digestLedger(appliedMigrations) {
77
530
  return (0, node_crypto_1.createHash)('sha256')
78
531
  .update(JSON.stringify(appliedMigrations))
@@ -82,6 +535,8 @@ function assertTarget(target) {
82
535
  assertNonEmpty(target.database, 'expected target database');
83
536
  assertNonEmpty(target.schema, 'expected target schema');
84
537
  assertNonEmpty(target.applicationName, 'expected application_name');
538
+ assertNonEmpty(target.expectedRole, 'expected runtime role');
539
+ (0, credential_generation_1.assertDatabaseCredentialGeneration)(target.credentialGeneration, `${LOG_PREFIX} expected credential generation`);
85
540
  }
86
541
  async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compatibility) {
87
542
  assertTarget(target);
@@ -98,17 +553,21 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
98
553
  SELECT current_database() AS database_name,
99
554
  current_schema() AS schema_name,
100
555
  current_setting('application_name') AS application_name,
101
- current_user AS role_name
556
+ current_user AS role_name,
557
+ session_user AS session_role_name
102
558
  `)).rows, 'connection identity');
103
559
  if (identity.database_name !== target.database ||
104
560
  identity.schema_name !== target.schema ||
105
- identity.application_name !== target.applicationName) {
561
+ identity.application_name !== target.applicationName ||
562
+ identity.role_name !== target.expectedRole ||
563
+ identity.session_role_name !== target.expectedRole) {
106
564
  throw new Error(`${LOG_PREFIX} runtime target mismatch: expected ` +
107
- `${target.database}/${target.schema}/${target.applicationName}, observed ` +
108
- `${identity.database_name}/${identity.schema_name ?? '<none>'}/${identity.application_name}.`);
565
+ `${target.database}/${target.schema}/${target.applicationName}/${target.expectedRole}, observed ` +
566
+ `${identity.database_name}/${identity.schema_name ?? '<none>'}/${identity.application_name}/` +
567
+ `${identity.role_name} (session=${identity.session_role_name}).`);
109
568
  }
110
569
  const role = requireSingleRow((await client.query(`
111
- SELECT rolsuper, rolcreatedb, rolcreaterole, rolbypassrls
570
+ SELECT rolsuper, rolcreatedb, rolcreaterole, rolbypassrls, rolreplication
112
571
  FROM pg_roles
113
572
  WHERE rolname = current_user
114
573
  `)).rows, 'runtime role');
@@ -117,11 +576,20 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
117
576
  role.rolcreatedb ? 'CREATEDB' : null,
118
577
  role.rolcreaterole ? 'CREATEROLE' : null,
119
578
  role.rolbypassrls ? 'BYPASSRLS' : null,
579
+ role.rolreplication ? 'REPLICATION' : null,
120
580
  ].filter((value) => value !== null);
121
581
  if (forbiddenRoleAttributes.length > 0) {
122
582
  throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden attribute(s): ` +
123
583
  forbiddenRoleAttributes.join(', '));
124
584
  }
585
+ const roleMemberships = (await client.query(`
586
+ SELECT rolname AS role_name
587
+ FROM pg_roles
588
+ WHERE rolname <> current_user
589
+ AND pg_has_role(current_user, oid, 'MEMBER')
590
+ ORDER BY rolname
591
+ `)).rows;
592
+ assertNoRoleMemberships(roleMemberships);
125
593
  const databasePrivileges = requireSingleRow((await client.query(`
126
594
  SELECT has_database_privilege(current_user, current_database(), 'CONNECT') AS can_connect,
127
595
  has_database_privilege(current_user, current_database(), 'CREATE') AS can_create,
@@ -136,48 +604,222 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
136
604
  throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden database ` +
137
605
  `${databasePrivileges.owns_database ? 'ownership' : 'CREATE privilege'}.`);
138
606
  }
139
- const schemaPrivileges = requireSingleRow((await client.query(`
140
- SELECT has_schema_privilege(current_user, oid, 'USAGE') AS can_use,
141
- has_schema_privilege(current_user, oid, 'CREATE') AS can_create,
142
- pg_has_role(current_user, nspowner, 'USAGE') AS owns_schema
143
- FROM pg_namespace
144
- WHERE nspname = $1
145
- `, [target.schema])).rows, 'target schema privilege');
146
- if (!schemaPrivileges.can_use) {
147
- throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} lacks USAGE on schema ${target.schema}.`);
148
- }
149
- if (schemaPrivileges.can_create || schemaPrivileges.owns_schema) {
150
- throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden CREATE/ownership ` +
151
- `on schema ${target.schema}.`);
152
- }
153
- const persistentDdlSchemas = (await client.query(`
154
- SELECT nspname AS schema_name
607
+ const persistentSchemaPrivileges = (await client.query(`
608
+ SELECT nspname AS schema_name,
609
+ has_schema_privilege(current_user, oid, 'USAGE') AS can_use,
610
+ has_schema_privilege(current_user, oid, 'CREATE') AS can_create,
611
+ pg_has_role(current_user, nspowner, 'USAGE') AS owns_schema,
612
+ EXISTS (
613
+ SELECT 1
614
+ FROM aclexplode(COALESCE(nspacl, acldefault('n', nspowner))) acl
615
+ WHERE acl.grantee = 0
616
+ AND acl.privilege_type = 'USAGE'
617
+ ) AS public_can_use
155
618
  FROM pg_namespace
156
619
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
157
620
  AND nspname NOT LIKE 'pg_toast%'
158
621
  AND nspname NOT LIKE 'pg_temp_%'
159
- AND (
160
- has_schema_privilege(current_user, oid, 'CREATE')
161
- OR pg_has_role(current_user, nspowner, 'USAGE')
162
- )
163
622
  ORDER BY nspname
164
623
  `)).rows;
624
+ const targetSchemaPrivileges = requireSingleRow(persistentSchemaPrivileges.filter((row) => row.schema_name === target.schema), 'target schema privilege');
625
+ if (!targetSchemaPrivileges.can_use) {
626
+ throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} lacks USAGE on schema ${target.schema}.`);
627
+ }
628
+ if (targetSchemaPrivileges.public_can_use) {
629
+ throw new Error(`${LOG_PREFIX} target schema ${target.schema} grants forbidden USAGE to PUBLIC.`);
630
+ }
631
+ const persistentDdlSchemas = persistentSchemaPrivileges.filter((row) => row.can_create || row.owns_schema);
165
632
  if (persistentDdlSchemas.length > 0) {
166
633
  throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden persistent ` +
167
634
  `schema CREATE/ownership on: ${persistentDdlSchemas
168
635
  .map((row) => row.schema_name)
169
636
  .join(', ')}.`);
170
637
  }
171
- const missingTablePrivileges = (await client.query(buildMissingTablePrivilegeQuery(access.tablePrivileges), [target.schema])).rows;
172
- if (missingTablePrivileges.length > 0) {
173
- throw new Error(`${LOG_PREFIX} runtime DML contract failed: ${formatMissingPrivileges('table', missingTablePrivileges)}.`);
638
+ const extensionSchemas = new Set(access.extensionDependencies.map((dependency) => dependency.schema));
639
+ if (extensionSchemas.has(target.schema)) {
640
+ throw new Error(`${LOG_PREFIX} extension dependency schema must differ from target schema ${target.schema}.`);
174
641
  }
175
- const sequenceQuery = buildMissingSequencePrivilegeQuery(access.sequencePrivileges);
176
- if (sequenceQuery !== null) {
177
- const missingSequencePrivileges = (await client.query(sequenceQuery, [target.schema])).rows;
178
- if (missingSequencePrivileges.length > 0) {
179
- throw new Error(`${LOG_PREFIX} runtime DML contract failed: ${formatMissingPrivileges('sequence', missingSequencePrivileges)}.`);
180
- }
642
+ const crossSchemaUsage = persistentSchemaPrivileges.filter((row) => row.schema_name !== target.schema &&
643
+ row.can_use &&
644
+ !extensionSchemas.has(row.schema_name));
645
+ if (crossSchemaUsage.length > 0) {
646
+ throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden cross-schema ` +
647
+ `USAGE on: ${crossSchemaUsage
648
+ .map((row) => row.schema_name)
649
+ .join(', ')}.`);
650
+ }
651
+ const missingOrPublicExtensionSchemas = [...extensionSchemas].filter((schema) => {
652
+ const row = persistentSchemaPrivileges.find((candidate) => candidate.schema_name === schema);
653
+ return row === undefined || !row.can_use || row.public_can_use;
654
+ });
655
+ if (missingOrPublicExtensionSchemas.length > 0) {
656
+ throw new Error(`${LOG_PREFIX} extension schemas must grant direct runtime USAGE and revoke PUBLIC USAGE: ` +
657
+ missingOrPublicExtensionSchemas.join(', '));
658
+ }
659
+ const crossSchemaTablePrivileges = (await client.query(buildTablePrivilegeQuery(true), [
660
+ target.schema,
661
+ ])).rows;
662
+ const crossSchemaSequencePrivileges = (await client.query(buildSequencePrivilegeQuery(true), [target.schema])).rows;
663
+ const undeclaredCrossSchemaTables = crossSchemaTablePrivileges.filter((row) => !access.extensionDependencies.some((dependency) => dependency.name === row.extension_name &&
664
+ dependency.schema === row.schema_name));
665
+ if (undeclaredCrossSchemaTables.length > 0) {
666
+ throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden cross-schema ` +
667
+ `table privileges: ${formatCrossSchemaPrivileges('table', undeclaredCrossSchemaTables)}.`);
668
+ }
669
+ const undeclaredCrossSchemaSequences = crossSchemaSequencePrivileges.filter((row) => !access.extensionDependencies.some((dependency) => dependency.name === row.extension_name &&
670
+ dependency.schema === row.schema_name));
671
+ if (undeclaredCrossSchemaSequences.length > 0) {
672
+ throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden cross-schema ` +
673
+ `sequence privileges: ${formatCrossSchemaPrivileges('sequence', undeclaredCrossSchemaSequences)}.`);
674
+ }
675
+ const targetTablePrivileges = (await client.query(buildTablePrivilegeQuery(false), [
676
+ target.schema,
677
+ ])).rows;
678
+ assertExactTargetPrivileges('table', targetTablePrivileges, access.tablePrivileges, access.writableRelations);
679
+ const targetSequencePrivileges = (await client.query(buildSequencePrivilegeQuery(false), [target.schema])).rows;
680
+ assertExactTargetPrivileges('sequence', targetSequencePrivileges, access.sequencePrivileges, access.writableRelations);
681
+ const targetTypes = (await client.query(`
682
+ SELECT t.typname AS object_name,
683
+ t.typtype AS type_kind,
684
+ has_type_privilege(current_user, t.oid, 'USAGE') AS can_use,
685
+ EXISTS (
686
+ SELECT 1
687
+ FROM aclexplode(COALESCE(t.typacl, acldefault('T', t.typowner))) acl
688
+ WHERE acl.grantee = 0
689
+ AND acl.privilege_type = 'USAGE'
690
+ ) AS public_has_privilege,
691
+ pg_has_role(current_user, t.typowner, 'USAGE') AS owns_object
692
+ FROM pg_type t
693
+ JOIN pg_namespace n ON n.oid = t.typnamespace
694
+ WHERE n.nspname = $1
695
+ AND t.typelem = 0
696
+ AND t.typtype IN ('b', 'd', 'e', 'r', 'm')
697
+ ORDER BY t.typname
698
+ `, [target.schema])).rows;
699
+ assertTargetTypes(targetTypes);
700
+ const targetFunctions = (await client.query(`
701
+ SELECT p.proname AS object_name,
702
+ pg_get_function_identity_arguments(p.oid) AS identity_arguments,
703
+ has_function_privilege(current_user, p.oid, 'EXECUTE') AS can_execute,
704
+ EXISTS (
705
+ SELECT 1
706
+ FROM aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner))) acl
707
+ WHERE acl.grantee = 0
708
+ AND acl.privilege_type = 'EXECUTE'
709
+ ) AS public_has_privilege,
710
+ pg_has_role(current_user, p.proowner, 'USAGE') AS owns_object,
711
+ p.prosecdef AS security_definer,
712
+ p.proconfig AS configuration
713
+ FROM pg_proc p
714
+ JOIN pg_namespace n ON n.oid = p.pronamespace
715
+ WHERE n.nspname = $1
716
+ ORDER BY p.proname, identity_arguments
717
+ `, [target.schema])).rows;
718
+ assertTargetFunctions(targetFunctions, access.targetFunctions, target.schema);
719
+ if (access.extensionDependencies.length > 0) {
720
+ const extensionNames = access.extensionDependencies.map((dependency) => dependency.name);
721
+ const extensionSchemaNames = [...extensionSchemas];
722
+ const extensionRows = (await client.query(`
723
+ SELECT e.extname AS extension_name,
724
+ n.nspname AS schema_name,
725
+ e.extversion AS extension_version,
726
+ pg_has_role(current_user, e.extowner, 'USAGE') AS owns_extension
727
+ FROM pg_extension e
728
+ JOIN pg_namespace n ON n.oid = e.extnamespace
729
+ WHERE e.extname = ANY($1::text[])
730
+ ORDER BY e.extname
731
+ `, [extensionNames])).rows;
732
+ assertExtensionMetadata(access.extensionDependencies, extensionRows);
733
+ const extensionTypes = (await client.query(`
734
+ SELECT n.nspname AS schema_name,
735
+ t.typname AS object_name,
736
+ e.extname AS extension_name,
737
+ EXISTS (
738
+ SELECT 1
739
+ FROM aclexplode(COALESCE(t.typacl, acldefault('T', t.typowner))) acl
740
+ WHERE acl.grantee = 0
741
+ AND acl.privilege_type = 'USAGE'
742
+ ) AS public_has_privilege,
743
+ pg_has_role(current_user, t.typowner, 'USAGE') AS owns_object
744
+ FROM pg_type t
745
+ JOIN pg_namespace n ON n.oid = t.typnamespace
746
+ LEFT JOIN pg_depend d
747
+ ON d.classid = 'pg_type'::regclass
748
+ AND d.objid = t.oid
749
+ AND d.objsubid = 0
750
+ AND d.refclassid = 'pg_extension'::regclass
751
+ AND d.deptype = 'e'
752
+ LEFT JOIN pg_extension e ON e.oid = d.refobjid
753
+ WHERE n.nspname = ANY($1::text[])
754
+ -- Array type privileges are inherited from their element type
755
+ -- and PostgreSQL rejects direct GRANT/REVOKE on array types.
756
+ AND t.typelem = 0
757
+ AND has_type_privilege(current_user, t.oid, 'USAGE')
758
+ ORDER BY n.nspname, t.typname
759
+ `, [extensionSchemaNames])).rows;
760
+ const extensionFunctions = (await client.query(`
761
+ SELECT n.nspname AS schema_name,
762
+ p.proname AS object_name,
763
+ pg_get_function_identity_arguments(p.oid) AS identity_arguments,
764
+ e.extname AS extension_name,
765
+ EXISTS (
766
+ SELECT 1
767
+ FROM aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner))) acl
768
+ WHERE acl.grantee = 0
769
+ AND acl.privilege_type = 'EXECUTE'
770
+ ) AS public_has_privilege,
771
+ pg_has_role(current_user, p.proowner, 'USAGE') AS owns_object,
772
+ p.prosecdef AS security_definer,
773
+ p.proconfig AS configuration
774
+ FROM pg_proc p
775
+ JOIN pg_namespace n ON n.oid = p.pronamespace
776
+ LEFT JOIN pg_depend d
777
+ ON d.classid = 'pg_proc'::regclass
778
+ AND d.objid = p.oid
779
+ AND d.objsubid = 0
780
+ AND d.refclassid = 'pg_extension'::regclass
781
+ AND d.deptype = 'e'
782
+ LEFT JOIN pg_extension e ON e.oid = d.refobjid
783
+ WHERE n.nspname = ANY($1::text[])
784
+ AND has_function_privilege(current_user, p.oid, 'EXECUTE')
785
+ ORDER BY n.nspname, p.proname, identity_arguments
786
+ `, [extensionSchemaNames])).rows;
787
+ assertExactExtensionObjects(access.extensionDependencies, extensionTypes, extensionFunctions, crossSchemaTablePrivileges, crossSchemaSequencePrivileges);
788
+ }
789
+ const ledgerPrivileges = requireSingleRow((await client.query(`
790
+ SELECT has_table_privilege(current_user, c.oid, 'SELECT') AS can_select,
791
+ has_table_privilege(current_user, c.oid, 'INSERT') AS can_insert,
792
+ has_table_privilege(current_user, c.oid, 'UPDATE') AS can_update,
793
+ has_table_privilege(current_user, c.oid, 'DELETE') AS can_delete,
794
+ has_table_privilege(current_user, c.oid, 'TRUNCATE') AS can_truncate,
795
+ has_table_privilege(current_user, c.oid, 'REFERENCES') AS can_references,
796
+ has_table_privilege(current_user, c.oid, 'TRIGGER') AS can_trigger,
797
+ pg_has_role(current_user, c.relowner, 'USAGE') AS owns_object,
798
+ ARRAY(
799
+ SELECT acl.privilege_type
800
+ FROM aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner))) acl
801
+ WHERE acl.grantee = 0
802
+ ORDER BY acl.privilege_type
803
+ ) AS public_privileges
804
+ FROM pg_class c
805
+ WHERE c.oid = to_regclass(
806
+ format('%I.%I', $1::text, '_prisma_migrations')
807
+ )
808
+ `, [target.schema])).rows, 'migration ledger privilege');
809
+ if (!ledgerPrivileges.can_select ||
810
+ ledgerPrivileges.public_privileges.length > 0 ||
811
+ ledgerPrivileges.owns_object) {
812
+ throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} does not have exact private SELECT-only access to ` +
813
+ `${target.schema}._prisma_migrations (PUBLIC=[${ledgerPrivileges.public_privileges.join('/')}], owns=${ledgerPrivileges.owns_object}).`);
814
+ }
815
+ if (ledgerPrivileges.can_insert ||
816
+ ledgerPrivileges.can_update ||
817
+ ledgerPrivileges.can_delete ||
818
+ ledgerPrivileges.can_truncate ||
819
+ ledgerPrivileges.can_references ||
820
+ ledgerPrivileges.can_trigger) {
821
+ throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden write privilege on ` +
822
+ `${target.schema}._prisma_migrations.`);
181
823
  }
182
824
  const migrationRows = (await client.query(`
183
825
  SELECT id::text,
@@ -185,20 +827,13 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
185
827
  checksum,
186
828
  started_at::text,
187
829
  finished_at::text,
830
+ rolled_back_at::text,
831
+ logs,
188
832
  applied_steps_count
189
833
  FROM ${quoteIdentifier(target.schema)}."_prisma_migrations"
190
- WHERE finished_at IS NOT NULL
191
- AND rolled_back_at IS NULL
192
- ORDER BY finished_at, id
834
+ ORDER BY started_at NULLS FIRST, id
193
835
  `)).rows;
194
- const appliedMigrations = migrationRows.map((row) => ({
195
- id: row.id,
196
- name: row.migration_name,
197
- checksum: row.checksum,
198
- startedAt: row.started_at,
199
- finishedAt: row.finished_at,
200
- appliedStepsCount: row.applied_steps_count,
201
- }));
836
+ const appliedMigrations = validateMigrationLedger(migrationRows);
202
837
  const observedLedgerDigest = digestLedger(appliedMigrations);
203
838
  const context = Object.freeze({
204
839
  target: Object.freeze({ ...target }),