@happyvertical/smrt-core 0.45.3 → 0.47.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.
Files changed (106) hide show
  1. package/AGENTS.md +39 -0
  2. package/agents/generators.md +0 -1
  3. package/dist/browser.d.ts +2 -1
  4. package/dist/browser.d.ts.map +1 -1
  5. package/dist/decorators/compatibility.d.ts +46 -0
  6. package/dist/decorators/compatibility.d.ts.map +1 -1
  7. package/dist/decorators/compatibility.js +48 -5
  8. package/dist/decorators/compatibility.js.map +1 -1
  9. package/dist/decorators/index.d.ts +119 -1
  10. package/dist/decorators/index.d.ts.map +1 -1
  11. package/dist/decorators/index.js +52 -8
  12. package/dist/decorators/index.js.map +1 -1
  13. package/dist/embedded-write-queue.d.ts.map +1 -1
  14. package/dist/embedded-write-queue.js +20 -1
  15. package/dist/embedded-write-queue.js.map +1 -1
  16. package/dist/generators/custom-action.d.ts +450 -41
  17. package/dist/generators/custom-action.d.ts.map +1 -1
  18. package/dist/generators/custom-action.js +765 -56
  19. package/dist/generators/custom-action.js.map +1 -1
  20. package/dist/generators/index.d.ts +1 -3
  21. package/dist/generators/index.d.ts.map +1 -1
  22. package/dist/generators/index.js +3 -4
  23. package/dist/generators/mcp.d.ts.map +1 -1
  24. package/dist/generators/mcp.js +1 -1
  25. package/dist/generators/mcp.js.map +1 -1
  26. package/dist/generators/preflight-route.d.ts +12 -10
  27. package/dist/generators/preflight-route.d.ts.map +1 -1
  28. package/dist/generators/preflight-route.js +46 -14
  29. package/dist/generators/preflight-route.js.map +1 -1
  30. package/dist/generators/rest.d.ts +44 -0
  31. package/dist/generators/rest.d.ts.map +1 -1
  32. package/dist/generators/rest.js +84 -9
  33. package/dist/generators/rest.js.map +1 -1
  34. package/dist/generators/tenant-gate.d.ts +33 -9
  35. package/dist/generators/tenant-gate.d.ts.map +1 -1
  36. package/dist/generators/tenant-gate.js +2 -1
  37. package/dist/generators/tenant-gate.js.map +1 -1
  38. package/dist/generators.js +3 -4
  39. package/dist/index.d.ts +5 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +6 -6
  42. package/dist/knowledge.d.ts.map +1 -1
  43. package/dist/knowledge.js +87 -31
  44. package/dist/knowledge.js.map +1 -1
  45. package/dist/manifest/static-manifest.d.ts.map +1 -1
  46. package/dist/manifest/static-manifest.js +125 -53
  47. package/dist/manifest/static-manifest.js.map +1 -1
  48. package/dist/manifest/store.js +1 -1
  49. package/dist/manifest/store.js.map +1 -1
  50. package/dist/manifest.json +184 -53
  51. package/dist/migrations/framework-base-tables.d.ts +148 -0
  52. package/dist/migrations/framework-base-tables.d.ts.map +1 -0
  53. package/dist/migrations/framework-base-tables.js +419 -0
  54. package/dist/migrations/framework-base-tables.js.map +1 -0
  55. package/dist/migrations/index.d.ts +1 -0
  56. package/dist/migrations/index.d.ts.map +1 -1
  57. package/dist/migrations/index.js +2 -1
  58. package/dist/migrations.js +2 -1
  59. package/dist/postgres-permissions.d.ts +45 -0
  60. package/dist/postgres-permissions.d.ts.map +1 -0
  61. package/dist/postgres-permissions.js +424 -0
  62. package/dist/postgres-permissions.js.map +1 -0
  63. package/dist/registry/framework-base-classes.d.ts.map +1 -1
  64. package/dist/registry/framework-base-classes.js +4 -1
  65. package/dist/registry/framework-base-classes.js.map +1 -1
  66. package/dist/registry/index.d.ts +1 -1
  67. package/dist/registry/index.d.ts.map +1 -1
  68. package/dist/registry/shared-state.d.ts +19 -0
  69. package/dist/registry/shared-state.d.ts.map +1 -1
  70. package/dist/registry/shared-state.js +16 -1
  71. package/dist/registry/shared-state.js.map +1 -1
  72. package/dist/registry.d.ts +136 -1
  73. package/dist/registry.d.ts.map +1 -1
  74. package/dist/registry.js +231 -1
  75. package/dist/registry.js.map +1 -1
  76. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  77. package/dist/scanner/manifest-generator.js +18 -16
  78. package/dist/scanner/manifest-generator.js.map +1 -1
  79. package/dist/scanner/types.d.ts +59 -6
  80. package/dist/scanner/types.d.ts.map +1 -1
  81. package/dist/scanner/types.js.map +1 -1
  82. package/dist/scripts/create-wrappers.js +0 -5
  83. package/dist/smrt-knowledge.json +35 -35
  84. package/dist/testing/database.d.ts +6 -1
  85. package/dist/testing/database.d.ts.map +1 -1
  86. package/dist/testing/database.js +9 -1
  87. package/dist/testing/database.js.map +1 -1
  88. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  89. package/dist/vite-plugin/api-client-entries.js +23 -19
  90. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  91. package/dist/vite-plugin/index.d.ts.map +1 -1
  92. package/dist/vite-plugin/index.js +3 -167
  93. package/dist/vite-plugin/index.js.map +1 -1
  94. package/dist/vite-plugin/sveltekit-generator.d.ts +31 -4
  95. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  96. package/dist/vite-plugin/sveltekit-generator.js +137 -71
  97. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  98. package/dist/vite-plugin/templates/default-ui.ts +22 -1
  99. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  100. package/dist/vite-plugin/web-collections.js +1 -1
  101. package/dist/vite-plugin/web-collections.js.map +1 -1
  102. package/package.json +10 -10
  103. package/dist/generators/cli.d.ts +0 -155
  104. package/dist/generators/cli.d.ts.map +0 -1
  105. package/dist/generators/cli.js +0 -473
  106. package/dist/generators/cli.js.map +0 -1
@@ -0,0 +1,424 @@
1
+ import { CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_BATCH, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY } from "./system/schema.js";
2
+ import { createHash } from "node:crypto";
3
+ //#region src/postgres-permissions.ts
4
+ var bookkeeping = /* @__PURE__ */ new Set([
5
+ "_smrt_migrations",
6
+ "_smrt_schema_migrations",
7
+ "_smrt_backfills"
8
+ ]);
9
+ var identifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
10
+ var literal = (value) => `E'${value.replaceAll("\\", "\\\\").replaceAll("'", "''")}'`;
11
+ var qualified = (schema, name) => `${identifier(schema)}.${identifier(name)}`;
12
+ var normalizeFunctionSource = (value) => value.replaceAll("\r\n", "\n").trim();
13
+ var normalizeArgumentTypes = (value) => value.replaceAll(/\s*,\s*/g, ",");
14
+ var normalizeFunctionResult = (value) => value.replaceAll(/\s+/g, " ").replaceAll(/\s*,\s*/g, ",").replaceAll(/\(\s*/g, "(").replaceAll(/\s*\)/g, ")").trim().toLowerCase();
15
+ function sourceFromFunctionDdl(ddl) {
16
+ const match = ddl.match(/\bAS\s+\$([A-Za-z_][A-Za-z0-9_]*)\$\n([\s\S]*?)\n\$\1\$;/);
17
+ if (!match) throw new Error("Framework function DDL has no dollar-quoted body.");
18
+ return normalizeFunctionSource(match[2]);
19
+ }
20
+ function resultFromFunctionDdl(ddl) {
21
+ const match = ddl.match(/\bRETURNS\s+([\s\S]*?)\nLANGUAGE\s+/);
22
+ if (!match) throw new Error("Framework function DDL has no return result.");
23
+ return normalizeFunctionResult(match[1]);
24
+ }
25
+ function frameworkRoutine(identity, ddl) {
26
+ const match = identity.match(/^([^()]+)\((.*)\)$/);
27
+ if (!match) throw new Error(`Invalid framework function identity: ${identity}`);
28
+ return {
29
+ name: match[1],
30
+ argumentTypes: normalizeArgumentTypes(match[2]),
31
+ argumentDefaults: match[1] === "_smrt_drain_changes" ? String(POSTGRES_CHANGE_FEED_DRAIN_BATCH) : null,
32
+ result: resultFromFunctionDdl(ddl),
33
+ source: sourceFromFunctionDdl(ddl)
34
+ };
35
+ }
36
+ var frameworkRoutines = [frameworkRoutine(POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION), frameworkRoutine(POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION)];
37
+ function object(value, keys, label) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
39
+ for (const key of Object.keys(value)) if (!keys.includes(key)) throw new Error(`Unknown ${label} property: ${key}.`);
40
+ return value;
41
+ }
42
+ function name(value, label) {
43
+ if (typeof value !== "string" || !value.length || value.includes("\0") || Buffer.byteLength(value) > 63) throw new Error(`${label} must be a nonempty PostgreSQL identifier (at most 63 bytes).`);
44
+ return value;
45
+ }
46
+ /** Validate configuration before opening a connection; unknown options fail closed. */
47
+ function validatePostgresPermissionContract(value) {
48
+ const input = object(value, [
49
+ "schema",
50
+ "schemaExclusive",
51
+ "migrationOwner",
52
+ "runtimeRole",
53
+ "managedTables",
54
+ "managedTriggerFunctions",
55
+ "monitor"
56
+ ], "postgresPermissions");
57
+ if (input.schemaExclusive !== true) throw new Error("postgresPermissions.schemaExclusive must explicitly be true.");
58
+ const schema = name(input.schema, "schema");
59
+ if (schema === "information_schema" || schema.startsWith("pg_")) throw new Error("A dedicated application schema is required.");
60
+ const migrationOwner = name(input.migrationOwner, "migrationOwner");
61
+ const runtimeRole = name(input.runtimeRole, "runtimeRole");
62
+ if (!Array.isArray(input.managedTables)) throw new Error("managedTables must be an array.");
63
+ const managedTables = [...new Set(input.managedTables.map((table) => name(table, "managedTables entry")))].sort();
64
+ if (input.managedTriggerFunctions !== void 0 && !Array.isArray(input.managedTriggerFunctions)) throw new Error("managedTriggerFunctions must be an array.");
65
+ const managedTriggerFunctions = [...new Set((input.managedTriggerFunctions ?? []).map((routine) => name(routine, "managedTriggerFunctions entry")))].sort();
66
+ let monitor;
67
+ if (input.monitor !== void 0) {
68
+ const entry = object(input.monitor, ["role", "tables"], "monitor");
69
+ const role = name(entry.role, "monitor.role");
70
+ if (!entry.tables || typeof entry.tables !== "object" || Array.isArray(entry.tables)) throw new Error("monitor.tables must map tables to column arrays.");
71
+ const tables = Object.create(null);
72
+ for (const [table, columns] of Object.entries(entry.tables).sort(([a], [b]) => a.localeCompare(b))) {
73
+ name(table, "monitor table");
74
+ if (!Array.isArray(columns) || !columns.length) throw new Error(`monitor table ${table} requires a nonempty column array.`);
75
+ tables[table] = [...new Set(columns.map((column) => name(column, "monitor column")))].sort();
76
+ }
77
+ monitor = {
78
+ role,
79
+ tables
80
+ };
81
+ }
82
+ const roles = [
83
+ migrationOwner,
84
+ runtimeRole,
85
+ ...monitor ? [monitor.role] : []
86
+ ];
87
+ if (new Set(roles).size !== roles.length || roles.some((role) => role.toUpperCase() === "PUBLIC" || role.startsWith("pg_"))) throw new Error("Migration, runtime and monitor roles must be distinct non-system roles.");
88
+ return {
89
+ schema,
90
+ schemaExclusive: true,
91
+ migrationOwner,
92
+ runtimeRole,
93
+ managedTables,
94
+ managedTriggerFunctions,
95
+ ...monitor ? { monitor } : {}
96
+ };
97
+ }
98
+ var acl = (expression) => `COALESCE((SELECT json_agg(json_build_object('grantee', CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END, 'grantor', pg_get_userbyid(a.grantor), 'privilege', a.privilege_type, 'grantable', a.is_grantable) ORDER BY a.grantee, a.privilege_type, a.grantor) FROM aclexplode(${expression}) a), '[]'::json)`;
99
+ async function snapshot(db, contract) {
100
+ const roleNames = [
101
+ contract.migrationOwner,
102
+ contract.runtimeRole,
103
+ ...contract.monitor ? [contract.monitor.role] : []
104
+ ].map(literal).join(",");
105
+ const restrictedRoleNames = [contract.runtimeRole, ...contract.monitor ? [contract.monitor.role] : []].map(literal).join(",");
106
+ const informationSchemaPublicSelect = "'administrable_role_authorizations','applicable_roles','attributes','character_sets','check_constraint_routine_usage','check_constraints','collation_character_set_applicability','collations','column_column_usage','column_domain_usage','column_options','column_privileges','column_udt_usage','columns','constraint_column_usage','constraint_table_usage','data_type_privileges','domain_constraints','domain_udt_usage','domains','element_types','enabled_roles','foreign_data_wrapper_options','foreign_data_wrappers','foreign_server_options','foreign_servers','foreign_table_options','foreign_tables','information_schema_catalog_name','key_column_usage','parameters','referential_constraints','role_column_grants','role_routine_grants','role_table_grants','role_udt_grants','role_usage_grants','routine_column_usage','routine_privileges','routine_routine_usage','routine_sequence_usage','routine_table_usage','routines','schemata','sequences','sql_features','sql_implementation_info','sql_sizing','table_constraints','table_privileges','tables','triggered_update_columns','triggers','udt_privileges','usage_privileges','user_defined_types','user_mapping_options','user_mappings','view_column_usage','view_routine_usage','view_table_usage','views'";
107
+ const initialAcl = (fallback) => `CASE WHEN i.privtype='i' THEN i.initprivs WHEN i.privtype='e' THEN NULL::aclitem[] ELSE ${fallback} END`;
108
+ const queries = {
109
+ server: `SELECT current_setting('server_version_num') AS version, current_database() AS database, current_user AS executor, (SELECT rolsuper FROM pg_roles WHERE rolname=current_user) AS superuser`,
110
+ roles: `SELECT oid::text, rolname, rolsuper, rolcreaterole, rolcreatedb, rolreplication, rolbypassrls FROM pg_roles WHERE rolname IN (${roleNames}) ORDER BY rolname`,
111
+ memberships: `SELECT pg_get_userbyid(member) AS member, pg_get_userbyid(roleid) AS parent FROM pg_auth_members WHERE member IN (SELECT oid FROM pg_roles WHERE rolname IN (${roleNames})) OR roleid IN (SELECT oid FROM pg_roles WHERE rolname IN (${roleNames})) ORDER BY member, roleid`,
112
+ database: `SELECT datname AS name, pg_get_userbyid(datdba) AS owner, ${acl("COALESCE(datacl, acldefault('d',datdba))")} AS acl FROM pg_database WHERE datname=current_database()`,
113
+ schemas: `SELECT nspname AS name, pg_get_userbyid(nspowner) AS owner, ${acl("COALESCE(nspacl, acldefault('n',nspowner))")} AS acl FROM pg_namespace WHERE left(nspname,3) <> 'pg_' AND nspname <> 'information_schema' ORDER BY nspname`,
114
+ relations: `SELECT c.oid::text, n.nspname AS schema, c.relname AS name, c.relkind AS kind, pg_get_userbyid(c.relowner) AS owner, c.relrowsecurity AS rls, ${acl("COALESCE(c.relacl, acldefault(CASE WHEN c.relkind='S' THEN 's'::\"char\" ELSE 'r'::\"char\" END,c.relowner))")} AS acl FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' AND c.relkind IN ('r','p','v','m','f','S') ORDER BY n.nspname,c.relname`,
115
+ columns: `SELECT c.oid::text AS relation, a.attname AS name, ${acl("a.attacl")} AS acl FROM pg_attribute a JOIN pg_class c ON c.oid=a.attrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' AND c.relkind IN ('r','p','v','m','f') AND a.attnum>0 AND NOT a.attisdropped ORDER BY c.oid,a.attnum`,
116
+ routines: `SELECT n.nspname AS schema, p.oid::text, p.proname AS name, format('%I.%I(%s)', n.nspname, p.proname, oidvectortypes(p.proargtypes)) AS identity, oidvectortypes(p.proargtypes) AS argument_types, pg_get_expr(p.proargdefaults,0) AS argument_defaults, p.prokind AS kind, p.prorettype::regtype::text AS return_type, pg_get_function_result(p.oid) AS result, p.procost::text AS cost, p.prorows::text AS rows, p.prosupport::regproc::text AS support, p.pronargs::text AS argument_count, l.lanname AS language, p.prosecdef AS security_definer, p.provolatile::text AS volatility, p.proparallel::text AS parallel, p.proleakproof AS leakproof, p.proisstrict AS strict, COALESCE(to_json(p.proconfig),'[]'::json) AS config, p.prosrc AS source, CASE WHEN p.prokind='f' THEN pg_get_functiondef(p.oid) END AS definition, pg_get_userbyid(p.proowner) AS owner, ${acl("COALESCE(p.proacl, acldefault('f',p.proowner))")} AS acl FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace JOIN pg_language l ON l.oid=p.prolang WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' ORDER BY n.nspname,p.oid`,
117
+ triggers: `SELECT t.oid::text, n.nspname AS schema, c.relname AS table_name, t.tgname AS name, t.tgfoid::text AS function_oid, t.tgenabled AS enabled, t.tgisinternal AS internal, t.tgtype::text AS type, encode(t.tgargs,'hex') AS arguments, pg_get_triggerdef(t.oid, true) AS definition FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' ORDER BY n.nspname,c.oid,t.oid`,
118
+ types: `SELECT n.nspname AS schema, t.typname AS name, pg_get_userbyid(t.typowner) AS owner, ${acl("COALESCE(t.typacl, acldefault('T',t.typowner))")} AS acl FROM pg_type t JOIN pg_namespace n ON n.oid=t.typnamespace LEFT JOIN pg_class c ON c.oid=t.typrelid WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' AND NOT EXISTS (SELECT 1 FROM pg_type element WHERE element.typarray=t.oid) AND (t.typrelid=0 OR c.relkind='c') ORDER BY n.nspname,t.typname`,
119
+ systemSchemas: `SELECT nspname AS name, pg_get_userbyid(nspowner) AS owner, ${acl("COALESCE(nspacl, acldefault('n',nspowner))")} AS acl FROM pg_namespace WHERE left(nspname,3)='pg_' OR nspname='information_schema' ORDER BY nspname`,
120
+ systemPrivileges: `WITH resources AS (
121
+ SELECT n.nspname AS schema, c.relname AS name, c.oid::text AS oid, CASE WHEN c.relkind='S' THEN 'sequence' ELSE 'relation' END AS kind, pg_get_userbyid(c.relowner) AS owner,
122
+ COALESCE(c.relacl,acldefault(CASE WHEN c.relkind='S' THEN 's'::"char" ELSE 'r'::"char" END,c.relowner)) AS current_acl,
123
+ CASE WHEN i.objoid IS NULL AND n.nspname='information_schema' AND c.oid<16384 AND c.relname IN (${informationSchemaPublicSelect}) THEN ARRAY[makeaclitem(0,c.relowner,'SELECT',false)] ELSE ${initialAcl("acldefault(CASE WHEN c.relkind='S' THEN 's'::\"char\" ELSE 'r'::\"char\" END,c.relowner)")} END AS initial_acl
124
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
125
+ LEFT JOIN pg_init_privs i ON i.objoid=c.oid AND i.classoid='pg_class'::regclass AND i.objsubid=0
126
+ WHERE (left(n.nspname,3)='pg_' OR n.nspname='information_schema') AND c.relkind IN ('r','p','v','m','f','S')
127
+ UNION ALL
128
+ SELECT n.nspname, c.relname || '.' || a.attname, c.oid::text || ':' || a.attnum::text, 'column', pg_get_userbyid(c.relowner),
129
+ a.attacl, ${initialAcl("NULL::aclitem[]")}
130
+ FROM pg_attribute a JOIN pg_class c ON c.oid=a.attrelid JOIN pg_namespace n ON n.oid=c.relnamespace
131
+ LEFT JOIN pg_init_privs i ON i.objoid=c.oid AND i.classoid='pg_class'::regclass AND i.objsubid=a.attnum
132
+ WHERE (left(n.nspname,3)='pg_' OR n.nspname='information_schema') AND a.attnum>0 AND NOT a.attisdropped
133
+ UNION ALL
134
+ SELECT n.nspname, p.proname, p.oid::text, 'routine', pg_get_userbyid(p.proowner), COALESCE(p.proacl,acldefault('f',p.proowner)), ${initialAcl("CASE WHEN p.oid<16384 THEN acldefault('f',p.proowner) ELSE NULL::aclitem[] END")}
135
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
136
+ LEFT JOIN pg_init_privs i ON i.objoid=p.oid AND i.classoid='pg_proc'::regclass AND i.objsubid=0
137
+ WHERE left(n.nspname,3)='pg_' OR n.nspname='information_schema'
138
+ UNION ALL
139
+ SELECT n.nspname, t.typname, t.oid::text, 'type', pg_get_userbyid(t.typowner), COALESCE(t.typacl,acldefault('T',t.typowner)), ${initialAcl("CASE WHEN t.oid<16384 THEN acldefault('T',t.typowner) ELSE NULL::aclitem[] END")}
140
+ FROM pg_type t JOIN pg_namespace n ON n.oid=t.typnamespace LEFT JOIN pg_class c ON c.oid=t.typrelid
141
+ LEFT JOIN pg_init_privs i ON i.objoid=t.oid AND i.classoid='pg_type'::regclass AND i.objsubid=0
142
+ WHERE (left(n.nspname,3)='pg_' OR n.nspname='information_schema') AND NOT EXISTS (SELECT 1 FROM pg_type element WHERE element.typarray=t.oid) AND (t.typrelid=0 OR c.relkind='c')
143
+ ) SELECT r.schema,r.name,r.oid,r.kind,r.owner,COALESCE(d.acl,'[]'::json) AS acl FROM resources r
144
+ CROSS JOIN LATERAL (
145
+ SELECT json_agg(json_build_object('grantee', CASE WHEN a.grantee=0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END,
146
+ 'grantor',pg_get_userbyid(a.grantor),'privilege',a.privilege_type,'grantable',a.is_grantable)
147
+ ORDER BY a.grantee,a.privilege_type,a.grantor) AS acl
148
+ FROM aclexplode(r.current_acl) a
149
+ WHERE a.grantee IN (SELECT oid FROM pg_roles WHERE rolname IN (${restrictedRoleNames}))
150
+ OR (a.grantee=0 AND NOT EXISTS (SELECT 1 FROM aclexplode(r.initial_acl) b WHERE b.grantee=0 AND b.privilege_type=a.privilege_type AND (b.is_grantable OR NOT a.is_grantable)))
151
+ ) d WHERE d.acl IS NOT NULL OR (r.kind<>'column' AND r.owner IN (${restrictedRoleNames})) ORDER BY r.schema,r.kind,r.name,r.oid`,
152
+ largeObjects: `SELECT oid::text, pg_get_userbyid(lomowner) AS owner, ${acl("COALESCE(lomacl, acldefault('L',lomowner))")} AS acl FROM pg_largeobject_metadata ORDER BY oid`,
153
+ largeObjectSettings: `SELECT setting,reset_val,source FROM pg_settings WHERE name='lo_compat_privileges'`,
154
+ largeObjectRoleSettings: `SELECT s.setdatabase::text AS database, CASE WHEN s.setrole=0 THEN 'ALL ROLES' ELSE pg_get_userbyid(s.setrole) END AS role, split_part(config.value,'=',2) AS setting FROM pg_db_role_setting s CROSS JOIN LATERAL unnest(s.setconfig) config(value) WHERE s.setdatabase IN (0,(SELECT oid FROM pg_database WHERE datname=current_database())) AND (s.setrole=0 OR s.setrole IN (SELECT oid FROM pg_roles WHERE rolname IN (${restrictedRoleNames}))) AND split_part(config.value,'=',1)='lo_compat_privileges' ORDER BY s.setdatabase,s.setrole,config.value`,
155
+ parameters: `SELECT parname AS name, ${acl("paracl")} AS acl FROM pg_parameter_acl ORDER BY parname`,
156
+ defaults: `SELECT pg_get_userbyid(d.defaclrole) AS owner, COALESCE(n.nspname,'') AS schema, d.defaclobjtype AS kind, ${acl("d.defaclacl")} AS acl FROM pg_default_acl d LEFT JOIN pg_namespace n ON n.oid=d.defaclnamespace ORDER BY d.defaclrole,d.defaclnamespace,d.defaclobjtype`
157
+ };
158
+ const result = {};
159
+ for (const [key, query] of Object.entries(queries)) {
160
+ if (key === "parameters" && Number(result.server[0].version) < 15e4) {
161
+ result[key] = [];
162
+ continue;
163
+ }
164
+ result[key] = (await db.query(query)).rows;
165
+ }
166
+ return result;
167
+ }
168
+ async function plan(db, input) {
169
+ const contract = validatePostgresPermissionContract(input);
170
+ const state = await snapshot(db, contract);
171
+ const diagnostics = [];
172
+ const statements = [];
173
+ const roles = [contract.runtimeRole, ...contract.monitor ? [contract.monitor.role] : []];
174
+ const add = (severity, code, resource, message, role) => diagnostics.push({
175
+ severity,
176
+ code,
177
+ resource,
178
+ message,
179
+ ...role ? { role } : {}
180
+ });
181
+ const unsupported = (code, resource, message, role) => add("unsupported", code, resource, message, role);
182
+ if (Number(state.server[0].version) < 14e4) unsupported("server-version", "database", "PostgreSQL 14 or later is required.");
183
+ for (const role of [contract.migrationOwner, ...roles]) {
184
+ const row = state.roles.find((entry) => entry.rolname === role);
185
+ if (!row) {
186
+ unsupported("missing-role", role, "Infrastructure must create this role.", role);
187
+ continue;
188
+ }
189
+ if (roles.includes(role) && [
190
+ "rolsuper",
191
+ "rolcreaterole",
192
+ "rolcreatedb",
193
+ "rolreplication",
194
+ "rolbypassrls"
195
+ ].some((key) => row[key])) unsupported("privileged-role", role, "Remove SUPERUSER, CREATEROLE, CREATEDB, REPLICATION and BYPASSRLS authority using infrastructure role management.", role);
196
+ }
197
+ for (const membership of state.memberships) unsupported("role-membership", membership.member, `Role ${membership.member} is a member of ${membership.parent}; inbound and outbound membership, inherited privileges, and SET ROLE authority require infrastructure review. Memberships are never modified.`, membership.member);
198
+ const schema = state.schemas.find((entry) => entry.name === contract.schema);
199
+ if (!schema) unsupported("missing-schema", contract.schema, "Create the dedicated schema as the migration owner before reconciliation.");
200
+ else if (schema.owner !== contract.migrationOwner) unsupported("schema-owner", contract.schema, `Schema must be owned by ${contract.migrationOwner}; ownership is never changed.`);
201
+ const database = state.database[0];
202
+ if (roles.includes(database.owner)) unsupported("database-owner", database.name, "Runtime and monitor must not own the database.", database.owner);
203
+ const relevant = (entries, role) => entries.filter((entry) => entry.grantee === role || entry.grantee === "PUBLIC");
204
+ function reconcile(kind, resource, entries, role, desired, suffix = "") {
205
+ for (const entry of entries) if (entry.grantor === role && entry.grantee !== role) unsupported("dependent-grant", resource, `Configured role granted ${entry.privilege} to ${entry.grantee}; grant chains require infrastructure review to preserve other roles.`, role);
206
+ for (const entry of entries) if (entry.grantee === "PUBLIC" && !(kind === "DATABASE" && entry.privilege === "CONNECT" && !entry.grantable)) unsupported("public-privilege", resource, `PUBLIC ${entry.privilege} is outside the explicit role contract; infrastructure must review it without changing unrelated access.`, role);
207
+ const excess = relevant(entries, role).filter((entry) => !desired.includes(entry.privilege) || entry.grantable);
208
+ const missing = desired.filter((privilege) => !entries.some((entry) => entry.grantee === role && entry.privilege === privilege));
209
+ for (const entry of excess) {
210
+ add("excessive", "excessive-privilege", resource, `${entry.privilege}${entry.grantable ? " WITH GRANT OPTION" : ""} is not declared (source: ${entry.grantee}).`, role);
211
+ if (entry.grantee === "PUBLIC") unsupported("public-privilege", resource, `PUBLIC ${entry.privilege} requires infrastructure review; revoking it would affect other roles.`, role);
212
+ }
213
+ for (const privilege of missing) add("missing", "missing-privilege", resource, `${privilege} is required.`, role);
214
+ if (!excess.length && !missing.length) return;
215
+ statements.push(`REVOKE ALL PRIVILEGES${suffix} ON ${kind} ${resource} FROM ${identifier(role)}`);
216
+ if (desired.length) statements.push(`GRANT ${desired.join(", ")}${suffix} ON ${kind} ${resource} TO ${identifier(role)}`);
217
+ }
218
+ function reconcileRoutine(resource, entries, desiredByRole) {
219
+ const grantee = (role) => role === "PUBLIC" ? "PUBLIC" : identifier(role);
220
+ for (const entry of entries) if (roles.includes(entry.grantor ?? "") && entry.grantee !== entry.grantor) unsupported("dependent-routine-grant", resource, `Configured role granted ${entry.privilege} to ${entry.grantee}; grant chains require infrastructure review to preserve other roles.`, entry.grantor);
221
+ const managedGrantees = /* @__PURE__ */ new Set([
222
+ contract.migrationOwner,
223
+ ...roles,
224
+ "PUBLIC"
225
+ ]);
226
+ for (const entry of entries) if (!managedGrantees.has(entry.grantee)) unsupported("outside-routine-grant", resource, `${entry.grantee} has ${entry.privilege}; direct routine access outside the declared roles requires infrastructure review.`, entry.grantee);
227
+ for (const role of [...roles, "PUBLIC"]) {
228
+ const desired = desiredByRole.get(role) ?? [];
229
+ const excess = entries.filter((entry) => entry.grantee === role).filter((entry) => !desired.includes(entry.privilege) || entry.grantable);
230
+ const missing = desired.filter((privilege) => !entries.some((entry) => entry.grantee === role && entry.privilege === privilege));
231
+ for (const entry of excess) add("excessive", "excessive-routine-privilege", resource, `${entry.privilege}${entry.grantable ? " WITH GRANT OPTION" : ""} is not declared.`, role);
232
+ for (const privilege of missing) add("missing", "missing-routine-privilege", resource, `${privilege} is required.`, role);
233
+ if (!excess.length && !missing.length) continue;
234
+ statements.push(`REVOKE ALL PRIVILEGES ON FUNCTION ${resource} FROM ${grantee(role)}`);
235
+ if (desired.length) statements.push(`GRANT ${desired.join(", ")} ON FUNCTION ${resource} TO ${grantee(role)}`);
236
+ }
237
+ }
238
+ for (const role of roles) {
239
+ reconcile("DATABASE", identifier(database.name), database.acl, role, ["CONNECT"]);
240
+ for (const entry of state.schemas) {
241
+ if (entry.owner === role) unsupported("resource-owner", entry.name, "Configured role owns a schema and has DDL authority.", role);
242
+ if (entry.name === contract.schema) reconcile("SCHEMA", identifier(entry.name), entry.acl, role, ["USAGE"]);
243
+ else if (relevant(entry.acl, role).some((grant) => grant.privilege === "CREATE")) unsupported("outside-schema-create", entry.name, "Effective CREATE outside the dedicated schema requires infrastructure repair.", role);
244
+ }
245
+ }
246
+ for (const schema of state.systemSchemas) for (const role of roles) {
247
+ if (schema.owner === role) unsupported("system-schema-owner", schema.name, "Configured roles must not own system schemas; infrastructure must restore separate ownership. System schema ownership is never modified.", role);
248
+ for (const grant of relevant(schema.acl, role)) if (grant.privilege === "CREATE") unsupported("system-schema-authority", schema.name, `Effective CREATE on a system schema (source: ${grant.grantee}) gives DDL authority and requires infrastructure repair. System schema ACLs are never modified.`, role);
249
+ }
250
+ for (const resource of state.systemPrivileges) {
251
+ if (resource.kind !== "column" && roles.includes(resource.owner)) unsupported("system-object-owner", qualified(resource.schema, resource.name), `Configured role owns a system ${resource.kind}; ownership retains implicit alteration and grant authority even when its ordinary ACLs are revoked. Infrastructure must restore ownership; SMRT never changes it.`, resource.owner);
252
+ for (const grant of resource.acl) unsupported("system-privilege-delta", qualified(resource.schema, resource.name), `${grant.grantee} has exceptional ${grant.privilege}${grant.grantable ? " WITH GRANT OPTION" : ""} on a system ${resource.kind}; review infrastructure grants against PostgreSQL initial ACLs. System ACLs are never modified.`, grant.grantee === "PUBLIC" ? void 0 : grant.grantee);
253
+ }
254
+ for (const resource of state.largeObjects) {
255
+ if (roles.includes(resource.owner)) unsupported("large-object-owner", `large object ${resource.oid}`, "Configured role owns a large object outside the declared table/column contract. Infrastructure must review ownership; SMRT never changes it.", resource.owner);
256
+ for (const grant of resource.acl) if (roles.includes(grant.grantee) || grant.grantee === "PUBLIC") unsupported("large-object-privilege", `large object ${resource.oid}`, `${grant.grantee} has ${grant.privilege}${grant.grantable ? " WITH GRANT OPTION" : ""} outside the declared table/column contract. Infrastructure must review this ACL; SMRT neither reads payloads nor changes large-object permissions.`, grant.grantee === "PUBLIC" ? void 0 : grant.grantee);
257
+ }
258
+ const explicitlyOff = (value) => [
259
+ "off",
260
+ "false",
261
+ "no",
262
+ "0"
263
+ ].includes(String(value).toLowerCase());
264
+ if (state.largeObjectSettings.length !== 1) unsupported("large-object-compatibility", "lo_compat_privileges", "The large-object ACL compatibility setting could not be established; qualification requires an observable disabled setting.");
265
+ for (const settings of state.largeObjectSettings) if (!explicitlyOff(settings.setting) || !explicitlyOff(settings.reset_val) || ![
266
+ "default",
267
+ "configuration file",
268
+ "command line",
269
+ "database"
270
+ ].includes(String(settings.source))) unsupported("large-object-compatibility", "lo_compat_privileges", "Large-object ACL checks must be enabled for every configured role. An enabled or operator-specific lo_compat_privileges override cannot establish that baseline; review deployment settings and reconnect without an operator-specific override.");
271
+ for (const settings of state.largeObjectRoleSettings) if (!explicitlyOff(settings.setting)) unsupported("large-object-compatibility", "lo_compat_privileges", `Role/database defaults enable or do not establish large-object ACL checks (${String(settings.role)}, database OID ${String(settings.database)}). Infrastructure must disable compatibility mode before qualification.`, settings.role === "ALL ROLES" ? void 0 : String(settings.role));
272
+ for (const parameter of state.parameters) for (const role of roles) for (const grant of relevant(parameter.acl, role)) unsupported("parameter-privilege", parameter.name, `Effective ${grant.privilege} on a PostgreSQL configuration parameter (source: ${grant.grantee}) requires infrastructure review; parameter ACLs are never modified.`, role);
273
+ const tables = state.relations.filter((entry) => entry.schema === contract.schema && entry.kind !== "S");
274
+ for (const table of bookkeeping) if (!tables.some((entry) => entry.name === table)) unsupported("missing-bookkeeping-table", qualified(contract.schema, table), "Bootstrap all framework bookkeeping tables as the migration owner before permission setup. Stop runtime and monitoring access during migrations or restores, then reconcile before reactivation.");
275
+ const declared = new Set(contract.managedTables);
276
+ for (const table of tables) if (table.name.startsWith("_smrt_")) declared.add(table.name);
277
+ for (const table of declared) if (!tables.some((entry) => entry.name === table)) unsupported("missing-table", qualified(contract.schema, table), "Run supported schema migrations before reconciling permissions.");
278
+ for (const [table, columns] of Object.entries(contract.monitor?.tables ?? {})) {
279
+ const relation = tables.find((entry) => entry.name === table);
280
+ if (!declared.has(table) || !relation) unsupported("monitor-table", table, "Monitor tables must exist and belong to managedTables or framework tables.");
281
+ for (const column of columns) if (!relation || !state.columns.some((entry) => entry.relation === relation.oid && entry.name === column)) unsupported("monitor-column", `${table}.${column}`, "The declared monitoring column does not exist.");
282
+ }
283
+ for (const relation of state.relations) {
284
+ const resource = qualified(relation.schema, relation.name);
285
+ const columns = state.columns.filter((entry) => entry.relation === relation.oid);
286
+ if (relation.schema !== contract.schema) {
287
+ for (const role of roles) if (relation.owner === role || relevant(relation.acl, role).length || columns.some((column) => relevant(column.acl, role).length)) unsupported("outside-resource-access", resource, "Effective access to an unrelated relation requires infrastructure review; unrelated ACLs are preserved.", role);
288
+ continue;
289
+ }
290
+ if (relation.owner !== contract.migrationOwner) unsupported("resource-owner", resource, `Managed resources must be owned by ${contract.migrationOwner}; ownership is never changed.`);
291
+ if (relation.kind !== "S" && !declared.has(relation.name)) unsupported("undeclared-table", resource, "The dedicated schema contains an undeclared table; defaults cannot safely cover a mixed schema.");
292
+ if (![
293
+ "r",
294
+ "p",
295
+ "S"
296
+ ].includes(relation.kind)) unsupported("unsupported-relation", resource, `Relation kind ${relation.kind} requires a separate security review.`);
297
+ if (relation.rls) unsupported("row-level-security", resource, "RLS policies require a separate application authorization review; ACL verification cannot establish allowed row operations.");
298
+ for (const role of roles) {
299
+ const isRuntime = role === contract.runtimeRole;
300
+ const desired = isRuntime ? relation.kind === "S" ? ["USAGE"] : bookkeeping.has(relation.name) ? ["SELECT"] : [
301
+ "SELECT",
302
+ "INSERT",
303
+ "UPDATE",
304
+ "DELETE"
305
+ ] : [];
306
+ reconcile(relation.kind === "S" ? "SEQUENCE" : "TABLE", resource, relation.acl, role, desired);
307
+ for (const column of columns) {
308
+ const wanted = !isRuntime && (contract.monitor?.tables[relation.name] ?? []).includes(column.name) ? ["SELECT"] : [];
309
+ reconcile("TABLE", resource, column.acl, role, wanted, ` (${identifier(column.name)})`);
310
+ }
311
+ }
312
+ }
313
+ const triggerRoutineNames = new Set(contract.managedTriggerFunctions);
314
+ const acceptedTriggerRoutineNames = /* @__PURE__ */ new Set();
315
+ const routineConfigIsEmpty = (value) => Array.isArray(value) && value.length === 0 || value === "[]";
316
+ for (const resource of state.routines) {
317
+ const resourceName = qualified(resource.schema, resource.name);
318
+ const framework = frameworkRoutines.find((expected) => resource.schema === contract.schema && resource.name === expected.name && normalizeArgumentTypes(resource.argument_types) === expected.argumentTypes);
319
+ if (framework) {
320
+ if (!(resource.owner === contract.migrationOwner && resource.kind === "f" && resource.language === "plpgsql" && resource.security_definer === false && resource.argument_defaults === framework.argumentDefaults && normalizeFunctionResult(resource.result) === framework.result && resource.cost === "100" && resource.rows === "1000" && resource.support === "-" && resource.volatility === "v" && resource.parallel === "u" && resource.leakproof === false && resource.strict === false && routineConfigIsEmpty(resource.config) && normalizeFunctionSource(String(resource.source)) === framework.source)) unsupported("framework-routine-definition", resourceName, "Framework routine must retain its canonical identity, owner, invoker security, defaults, execution properties, settings and generated body.");
321
+ else reconcileRoutine(resource.identity, resource.acl, /* @__PURE__ */ new Map([[contract.runtimeRole, ["EXECUTE"]]]));
322
+ continue;
323
+ }
324
+ const bindings = state.triggers.filter((trigger) => trigger.function_oid === resource.oid);
325
+ if (resource.schema === contract.schema && triggerRoutineNames.has(resource.name) && resource.kind === "f" && resource.return_type === "trigger" && resource.argument_types === "" && resource.argument_count === "0" && resource.owner === contract.migrationOwner && resource.language === "plpgsql" && resource.security_definer === false && routineConfigIsEmpty(resource.config) && bindings.length > 0 && bindings.every((trigger) => trigger.schema === contract.schema && trigger.internal === false && (trigger.enabled === "O" || trigger.enabled === "A") && declared.has(trigger.table_name))) {
326
+ acceptedTriggerRoutineNames.add(resource.name);
327
+ reconcileRoutine(resource.identity, resource.acl, /* @__PURE__ */ new Map());
328
+ continue;
329
+ }
330
+ for (const role of roles) if (resource.schema === contract.schema || resource.owner === role || relevant(resource.acl, role).length) unsupported("unsupported-routines", resourceName, triggerRoutineNames.has(resource.name) ? "Declared trigger routines must be migration-owner invoker PL/pgSQL functions returning trigger with no declared arguments, no function settings, and enabled non-internal bindings only to declared managed tables." : "User-defined routines are outside the supported table/sequence and declared trigger contract; review their effective privileges explicitly.", role);
331
+ }
332
+ for (const name of triggerRoutineNames) if (!acceptedTriggerRoutineNames.has(name)) unsupported("missing-managed-trigger-function", qualified(contract.schema, name), "Run the migration that creates this exact managed trigger function and its enabled binding before reconciling permissions.");
333
+ for (const resource of state.types) for (const role of roles) if (resource.schema === contract.schema || resource.owner === role || relevant(resource.acl, role).length) unsupported("unsupported-types", qualified(resource.schema, resource.name), "User-defined types are outside the supported table/sequence contract; review their effective privileges explicitly.", role);
334
+ for (const defaults of state.defaults) {
335
+ const resource = `DEFAULT ${defaults.kind} (${defaults.schema || "global"})`;
336
+ if (defaults.owner !== contract.migrationOwner && defaults.acl.some((entry) => roles.includes(entry.grantee))) unsupported("other-creator-access", resource, `Creator ${defaults.owner} grants configured roles future access outside this contract.`);
337
+ if (defaults.schema !== contract.schema && defaults.schema !== "" && defaults.acl.some((entry) => entry.grantee === "PUBLIC" || roles.includes(entry.grantee))) unsupported("outside-default-privilege", resource, "Defaults outside the managed schema grant future access and require infrastructure review.");
338
+ if (defaults.schema === "" && defaults.acl.some((entry) => roles.includes(entry.grantee) || entry.grantee === "PUBLIC" && [
339
+ "r",
340
+ "S",
341
+ "n"
342
+ ].includes(defaults.kind))) unsupported("global-default-privilege", resource, "Global creator defaults grant configured roles or PUBLIC access and cannot be repaired within this schema.");
343
+ if (defaults.schema === contract.schema && defaults.owner !== contract.migrationOwner) unsupported("other-creator-default", resource, `Defaults for creator ${defaults.owner} require separate ownership review.`);
344
+ if (defaults.schema === contract.schema && !["r", "S"].includes(defaults.kind) && defaults.acl.some((entry) => roles.includes(entry.grantee) || entry.grantee === "PUBLIC")) unsupported("unsupported-default", resource, "Only table and sequence schema defaults are supported.");
345
+ }
346
+ for (const [kind, sqlKind, runtime] of [[
347
+ "r",
348
+ "TABLES",
349
+ [
350
+ "SELECT",
351
+ "INSERT",
352
+ "UPDATE",
353
+ "DELETE"
354
+ ]
355
+ ], [
356
+ "S",
357
+ "SEQUENCES",
358
+ ["USAGE"]
359
+ ]]) {
360
+ const entry = state.defaults.find((item) => item.schema === contract.schema && item.owner === contract.migrationOwner && item.kind === kind);
361
+ if (entry?.acl.some((grant) => grant.grantee === "PUBLIC")) unsupported("public-default", `${contract.schema} ${sqlKind}`, "PUBLIC creator defaults are outside the explicit role contract and require infrastructure review.");
362
+ for (const role of roles) {
363
+ const desired = role === contract.runtimeRole ? runtime : [];
364
+ const extra = relevant(entry?.acl ?? [], role).filter((grant) => !desired.includes(grant.privilege) || grant.grantable);
365
+ const missing = desired.filter((privilege) => !(entry?.acl ?? []).some((grant) => grant.grantee === role && grant.privilege === privilege));
366
+ if (!extra.length && !missing.length) continue;
367
+ for (const grant of extra) {
368
+ add("excessive", "default-excessive", `${contract.schema} ${sqlKind}`, `${grant.privilege} default from ${grant.grantee} is not declared.`, role);
369
+ if (grant.grantee === "PUBLIC") unsupported("public-default", contract.schema, "PUBLIC defaults affect other roles and require infrastructure repair.");
370
+ }
371
+ for (const privilege of missing) add("missing", "default-missing", `${contract.schema} ${sqlKind}`, `Creator ${contract.migrationOwner} must default ${privilege}.`, role);
372
+ const prefix = `ALTER DEFAULT PRIVILEGES FOR ROLE ${identifier(contract.migrationOwner)} IN SCHEMA ${identifier(contract.schema)}`;
373
+ statements.push(`${prefix} REVOKE ALL PRIVILEGES ON ${sqlKind} FROM ${identifier(role)}`);
374
+ if (desired.length) statements.push(`${prefix} GRANT ${desired.join(", ")} ON ${sqlKind} TO ${identifier(role)}`);
375
+ }
376
+ }
377
+ const executor = state.server[0];
378
+ if (statements.length && !executor.superuser && executor.executor !== contract.migrationOwner) unsupported("execution-authority", "database", "Apply requires the migration owner or a superuser; plan and apply must use the same actor.");
379
+ if (statements.some((sql) => sql.includes(" ON DATABASE ")) && !executor.superuser && database.owner !== executor.executor) unsupported("database-authority", database.name, "Database ACL remediation requires its owner or a superuser.");
380
+ return {
381
+ contract,
382
+ diagnostics,
383
+ limitations: [
384
+ "Only PostgreSQL table, column, sequence, schema and database ACLs are qualified; application authorization and RLS are separate. Large-object ownership/access is unsupported, and lo_compat_privileges must remain off for deployed roles.",
385
+ "Future user-defined routines and types are unsupported. PostgreSQL implicit global defaults grant PUBLIC EXECUTE/USAGE; rerun diagnostics after every migration before activating runtime roles.",
386
+ "Pause migrations and external ACL/role writers during planning and apply. The advisory lock coordinates SMRT permission writers only.",
387
+ "All framework bookkeeping tables must exist before setup. Stop runtime and monitor access throughout migrations, restores and repair: recreated bookkeeping tables temporarily receive creator CRUD defaults and require explicit reconciliation before either role is reactivated."
388
+ ],
389
+ statements: [...new Set(statements)],
390
+ canApply: !diagnostics.some((entry) => entry.severity === "unsupported"),
391
+ fingerprint: createHash("sha256").update(JSON.stringify({
392
+ contract,
393
+ state
394
+ })).digest("hex")
395
+ };
396
+ }
397
+ /** Read-only catalog inspection. No application records or credentials are read. */
398
+ async function planPostgresPermissions(db, contract) {
399
+ return plan(db, contract);
400
+ }
401
+ /** Apply the reviewed contract atomically, rejecting stale catalog snapshots. */
402
+ async function applyPostgresPermissions(db, contract, options) {
403
+ const normalizedContract = validatePostgresPermissionContract(contract);
404
+ if (!options || !/^[a-f0-9]{64}$/.test(options.expectedFingerprint)) throw new Error("An expectedFingerprint from a reviewed permission plan is required.");
405
+ if (!db.transaction) throw new Error("PostgreSQL permissions require a transaction-capable database.");
406
+ return db.transaction(async (tx) => {
407
+ await tx.query("SET LOCAL lock_timeout = '5s'");
408
+ await tx.query("SET LOCAL statement_timeout = '30s'");
409
+ await tx.query(`SELECT pg_advisory_xact_lock(hashtext(${literal(`smrt-permissions:${normalizedContract.schema}`)}))`);
410
+ const before = await plan(tx, normalizedContract);
411
+ if (before.fingerprint !== options.expectedFingerprint) throw new Error("Permission plan is stale; generate and review a new plan.");
412
+ if (!before.canApply) throw new Error("Permission plan has unsupported privilege sources or missing prerequisites; resolve its diagnostics first.");
413
+ const executor = (await tx.query("SELECT current_user AS name, rolsuper FROM pg_roles WHERE rolname=current_user")).rows[0];
414
+ if (!executor.rolsuper && executor.name !== normalizedContract.migrationOwner) throw new Error("Apply must execute as the migration owner or a PostgreSQL superuser.");
415
+ for (const statement of before.statements) await tx.query(statement);
416
+ const after = await plan(tx, normalizedContract);
417
+ if (!after.canApply || after.diagnostics.length) throw new Error("Permission verification failed; all permission changes have been rolled back.");
418
+ return after;
419
+ });
420
+ }
421
+ //#endregion
422
+ export { applyPostgresPermissions, planPostgresPermissions, validatePostgresPermissionContract };
423
+
424
+ //# sourceMappingURL=postgres-permissions.js.map