@happyvertical/smrt-core 0.46.0 → 0.47.1

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 (90) hide show
  1. package/AGENTS.md +39 -0
  2. package/agents/generators.md +19 -5
  3. package/agents/system-diagnostics.md +43 -0
  4. package/dist/browser.js +2 -1
  5. package/dist/change-feed.d.ts +8 -0
  6. package/dist/change-feed.d.ts.map +1 -1
  7. package/dist/change-feed.js +1 -1
  8. package/dist/change-feed.js.map +1 -1
  9. package/dist/decorators/compatibility.d.ts +46 -0
  10. package/dist/decorators/compatibility.d.ts.map +1 -1
  11. package/dist/decorators/compatibility.js +48 -5
  12. package/dist/decorators/compatibility.js.map +1 -1
  13. package/dist/decorators/index.d.ts +119 -1
  14. package/dist/decorators/index.d.ts.map +1 -1
  15. package/dist/decorators/index.js +52 -8
  16. package/dist/decorators/index.js.map +1 -1
  17. package/dist/generators/custom-action.d.ts +379 -2
  18. package/dist/generators/custom-action.d.ts.map +1 -1
  19. package/dist/generators/custom-action.js +694 -17
  20. package/dist/generators/custom-action.js.map +1 -1
  21. package/dist/generators/index.d.ts +1 -1
  22. package/dist/generators/index.d.ts.map +1 -1
  23. package/dist/generators/index.js +2 -2
  24. package/dist/generators/preflight-route.d.ts +12 -10
  25. package/dist/generators/preflight-route.d.ts.map +1 -1
  26. package/dist/generators/preflight-route.js +46 -14
  27. package/dist/generators/preflight-route.js.map +1 -1
  28. package/dist/generators/rest.d.ts +44 -0
  29. package/dist/generators/rest.d.ts.map +1 -1
  30. package/dist/generators/rest.js +84 -9
  31. package/dist/generators/rest.js.map +1 -1
  32. package/dist/generators.js +2 -2
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -4
  36. package/dist/knowledge.d.ts.map +1 -1
  37. package/dist/knowledge.js +59 -15
  38. package/dist/knowledge.js.map +1 -1
  39. package/dist/manifest/static-manifest.d.ts.map +1 -1
  40. package/dist/manifest/static-manifest.js +125 -53
  41. package/dist/manifest/static-manifest.js.map +1 -1
  42. package/dist/manifest/store.js +1 -1
  43. package/dist/manifest/store.js.map +1 -1
  44. package/dist/manifest.json +184 -53
  45. package/dist/postgres-permissions.d.ts +9 -1
  46. package/dist/postgres-permissions.d.ts.map +1 -1
  47. package/dist/postgres-permissions.js +127 -13
  48. package/dist/postgres-permissions.js.map +1 -1
  49. package/dist/registry/index.d.ts +1 -1
  50. package/dist/registry/index.d.ts.map +1 -1
  51. package/dist/registry/shared-state.d.ts +19 -0
  52. package/dist/registry/shared-state.d.ts.map +1 -1
  53. package/dist/registry/shared-state.js +16 -1
  54. package/dist/registry/shared-state.js.map +1 -1
  55. package/dist/registry.d.ts +136 -1
  56. package/dist/registry.d.ts.map +1 -1
  57. package/dist/registry.js +231 -1
  58. package/dist/registry.js.map +1 -1
  59. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  60. package/dist/scanner/manifest-generator.js +18 -16
  61. package/dist/scanner/manifest-generator.js.map +1 -1
  62. package/dist/scanner/types.d.ts +59 -6
  63. package/dist/scanner/types.d.ts.map +1 -1
  64. package/dist/scanner/types.js.map +1 -1
  65. package/dist/smrt-knowledge.json +42 -35
  66. package/dist/system/diagnostics.d.ts +299 -0
  67. package/dist/system/diagnostics.d.ts.map +1 -0
  68. package/dist/system/diagnostics.js +530 -0
  69. package/dist/system/diagnostics.js.map +1 -0
  70. package/dist/system/index.d.ts +1 -0
  71. package/dist/system/index.d.ts.map +1 -1
  72. package/dist/system/index.js +2 -1
  73. package/dist/utils/scanner-module.d.ts +16 -0
  74. package/dist/utils/scanner-module.d.ts.map +1 -1
  75. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  76. package/dist/vite-plugin/api-client-entries.js +10 -8
  77. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  78. package/dist/vite-plugin/index.d.ts +34 -1
  79. package/dist/vite-plugin/index.d.ts.map +1 -1
  80. package/dist/vite-plugin/index.js +82 -3
  81. package/dist/vite-plugin/index.js.map +1 -1
  82. package/dist/vite-plugin/sveltekit-generator.d.ts +28 -2
  83. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  84. package/dist/vite-plugin/sveltekit-generator.js +121 -51
  85. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  86. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  87. package/dist/vite-plugin/web-collections.js +1 -1
  88. package/dist/vite-plugin/web-collections.js.map +1 -1
  89. package/dist/vite-plugin.js +2 -2
  90. package/package.json +16 -11
@@ -7,6 +7,10 @@ export interface PostgresPermissionContract {
7
7
  migrationOwner: string;
8
8
  runtimeRole: string;
9
9
  managedTables: string[];
10
+ /** Existing operator-owned tables retained outside the runtime data surface. */
11
+ retainedTables?: string[];
12
+ /** Exact zero-argument trigger functions bound only to managed tables. */
13
+ managedTriggerFunctions?: string[];
10
14
  monitor?: {
11
15
  role: string;
12
16
  tables: Record<string, string[]>;
@@ -27,9 +31,13 @@ export interface PostgresPermissionPlan {
27
31
  fingerprint: string;
28
32
  limitations: string[];
29
33
  }
34
+ type NormalizedPostgresPermissionContract = PostgresPermissionContract & {
35
+ managedTriggerFunctions: string[];
36
+ retainedTables: string[];
37
+ };
30
38
  type Executor = Pick<DatabaseInterface, 'query'>;
31
39
  /** Validate configuration before opening a connection; unknown options fail closed. */
32
- export declare function validatePostgresPermissionContract(value: unknown): PostgresPermissionContract;
40
+ export declare function validatePostgresPermissionContract(value: unknown): NormalizedPostgresPermissionContract;
33
41
  /** Read-only catalog inspection. No application records or credentials are read. */
34
42
  export declare function planPostgresPermissions(db: Executor, contract: PostgresPermissionContract): Promise<PostgresPermissionPlan>;
35
43
  /** Apply the reviewed contract atomically, rejecting stale catalog snapshots. */
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-permissions.d.ts","sourceRoot":"","sources":["../src/postgres-permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,eAAe,EAAE,IAAI,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;CAC9D;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,0BAA0B,CAAC;IACrC,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAC5C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,KAAK,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;AA8DjD,uFAAuF;AACvF,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,OAAO,GACb,0BAA0B,CA4E5B;AAssBD,oFAAoF;AACpF,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAEjC;AAED,iFAAiF;AACjF,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,0BAA0B,EACpC,OAAO,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAA;CAAE,GACvC,OAAO,CAAC,sBAAsB,CAAC,CA2CjC"}
1
+ {"version":3,"file":"postgres-permissions.d.ts","sourceRoot":"","sources":["../src/postgres-permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAS5D,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,eAAe,EAAE,IAAI,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,0EAA0E;IAC1E,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;CAC9D;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,0BAA0B,CAAC;IACrC,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAC5C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,KAAK,oCAAoC,GAAG,0BAA0B,GAAG;IACvE,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC;AAEF,KAAK,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;AA2JjD,uFAAuF;AACvF,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,OAAO,GACb,oCAAoC,CA2GtC;AA2+BD,oFAAoF;AACpF,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAEjC;AAED,iFAAiF;AACjF,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,0BAA0B,EACpC,OAAO,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAA;CAAE,GACvC,OAAO,CAAC,sBAAsB,CAAC,CA8CjC"}
@@ -1,3 +1,4 @@
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";
1
2
  import { createHash } from "node:crypto";
2
3
  //#region src/postgres-permissions.ts
3
4
  var bookkeeping = /* @__PURE__ */ new Set([
@@ -8,6 +9,31 @@ var bookkeeping = /* @__PURE__ */ new Set([
8
9
  var identifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
9
10
  var literal = (value) => `E'${value.replaceAll("\\", "\\\\").replaceAll("'", "''")}'`;
10
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)];
11
37
  function object(value, keys, label) {
12
38
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
13
39
  for (const key of Object.keys(value)) if (!keys.includes(key)) throw new Error(`Unknown ${label} property: ${key}.`);
@@ -25,6 +51,8 @@ function validatePostgresPermissionContract(value) {
25
51
  "migrationOwner",
26
52
  "runtimeRole",
27
53
  "managedTables",
54
+ "retainedTables",
55
+ "managedTriggerFunctions",
28
56
  "monitor"
29
57
  ], "postgresPermissions");
30
58
  if (input.schemaExclusive !== true) throw new Error("postgresPermissions.schemaExclusive must explicitly be true.");
@@ -34,6 +62,12 @@ function validatePostgresPermissionContract(value) {
34
62
  const runtimeRole = name(input.runtimeRole, "runtimeRole");
35
63
  if (!Array.isArray(input.managedTables)) throw new Error("managedTables must be an array.");
36
64
  const managedTables = [...new Set(input.managedTables.map((table) => name(table, "managedTables entry")))].sort();
65
+ if (input.retainedTables !== void 0 && !Array.isArray(input.retainedTables)) throw new Error("retainedTables must be an array.");
66
+ const retainedTables = [...new Set((input.retainedTables ?? []).map((table) => name(table, "retainedTables entry")))].sort();
67
+ const overlap = retainedTables.find((table) => managedTables.includes(table));
68
+ if (overlap) throw new Error(`Table ${overlap} cannot be both managed and retained.`);
69
+ if (input.managedTriggerFunctions !== void 0 && !Array.isArray(input.managedTriggerFunctions)) throw new Error("managedTriggerFunctions must be an array.");
70
+ const managedTriggerFunctions = [...new Set((input.managedTriggerFunctions ?? []).map((routine) => name(routine, "managedTriggerFunctions entry")))].sort();
37
71
  let monitor;
38
72
  if (input.monitor !== void 0) {
39
73
  const entry = object(input.monitor, ["role", "tables"], "monitor");
@@ -62,6 +96,8 @@ function validatePostgresPermissionContract(value) {
62
96
  migrationOwner,
63
97
  runtimeRole,
64
98
  managedTables,
99
+ retainedTables,
100
+ managedTriggerFunctions,
65
101
  ...monitor ? { monitor } : {}
66
102
  };
67
103
  }
@@ -81,9 +117,13 @@ async function snapshot(db, contract) {
81
117
  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`,
82
118
  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()`,
83
119
  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`,
84
- 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`,
120
+ 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, (SELECT parent.relname FROM pg_depend d JOIN pg_class parent ON parent.oid=d.refobjid JOIN pg_namespace parent_schema ON parent_schema.oid=parent.relnamespace WHERE d.classid='pg_class'::regclass AND d.objid=c.oid AND d.refclassid='pg_class'::regclass AND d.deptype IN ('a','i') AND parent_schema.oid=n.oid LIMIT 1) AS parent_table, ${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`,
121
+ inherits: `SELECT child_schema.nspname AS child_schema, child.relname AS child_table, parent_schema.nspname AS parent_schema, parent.relname AS parent_table FROM pg_inherits i JOIN pg_class child ON child.oid=i.inhrelid JOIN pg_namespace child_schema ON child_schema.oid=child.relnamespace JOIN pg_class parent ON parent.oid=i.inhparent JOIN pg_namespace parent_schema ON parent_schema.oid=parent.relnamespace ORDER BY child_schema.nspname,child.relname,parent_schema.nspname,parent.relname`,
122
+ foreignKeys: `SELECT child_schema.nspname AS child_schema, child.relname AS child_table, parent_schema.nspname AS parent_schema, parent.relname AS parent_table, con.confdeltype::text AS delete_action, con.confupdtype::text AS update_action FROM pg_constraint con JOIN pg_class child ON child.oid=con.conrelid JOIN pg_namespace child_schema ON child_schema.oid=child.relnamespace JOIN pg_class parent ON parent.oid=con.confrelid JOIN pg_namespace parent_schema ON parent_schema.oid=parent.relnamespace WHERE con.contype='f' ORDER BY child_schema.nspname,child.relname,parent_schema.nspname,parent.relname,con.conname`,
123
+ rewriteRules: `WITH RECURSIVE rule_dependencies AS (SELECT r.oid AS rule_oid, r.ev_class AS source_oid, r.rulename AS rule_name, d.refobjid AS referenced_oid FROM pg_rewrite r JOIN pg_depend d ON d.classid='pg_rewrite'::regclass AND d.objid=r.oid AND d.refclassid='pg_class'::regclass WHERE r.rulename <> '_RETURN' UNION SELECT dependencies.rule_oid, dependencies.source_oid, dependencies.rule_name, d.refobjid FROM rule_dependencies dependencies JOIN pg_rewrite view_rule ON view_rule.ev_class=dependencies.referenced_oid AND view_rule.rulename='_RETURN' JOIN pg_depend d ON d.classid='pg_rewrite'::regclass AND d.objid=view_rule.oid AND d.refclassid='pg_class'::regclass) SELECT n.nspname AS schema, c.relname AS table_name, dependencies.rule_name, referenced_schema.nspname AS referenced_schema, referenced.relname AS referenced_table FROM rule_dependencies dependencies JOIN pg_class c ON c.oid=dependencies.source_oid JOIN pg_namespace n ON n.oid=c.relnamespace JOIN pg_class referenced ON referenced.oid=dependencies.referenced_oid JOIN pg_namespace referenced_schema ON referenced_schema.oid=referenced.relnamespace ORDER BY n.nspname,c.relname,dependencies.rule_name,referenced_schema.nspname,referenced.relname`,
85
124
  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`,
86
- routines: `SELECT n.nspname AS schema, p.oid::text, p.proname AS name, 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 WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' ORDER BY n.nspname,p.oid`,
125
+ 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`,
126
+ 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`,
87
127
  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`,
88
128
  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`,
89
129
  systemPrivileges: `WITH resources AS (
@@ -184,6 +224,26 @@ async function plan(db, input) {
184
224
  statements.push(`REVOKE ALL PRIVILEGES${suffix} ON ${kind} ${resource} FROM ${identifier(role)}`);
185
225
  if (desired.length) statements.push(`GRANT ${desired.join(", ")}${suffix} ON ${kind} ${resource} TO ${identifier(role)}`);
186
226
  }
227
+ function reconcileRoutine(resource, entries, desiredByRole) {
228
+ const grantee = (role) => role === "PUBLIC" ? "PUBLIC" : identifier(role);
229
+ 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);
230
+ const managedGrantees = /* @__PURE__ */ new Set([
231
+ contract.migrationOwner,
232
+ ...roles,
233
+ "PUBLIC"
234
+ ]);
235
+ 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);
236
+ for (const role of [...roles, "PUBLIC"]) {
237
+ const desired = desiredByRole.get(role) ?? [];
238
+ const excess = entries.filter((entry) => entry.grantee === role).filter((entry) => !desired.includes(entry.privilege) || entry.grantable);
239
+ const missing = desired.filter((privilege) => !entries.some((entry) => entry.grantee === role && entry.privilege === privilege));
240
+ for (const entry of excess) add("excessive", "excessive-routine-privilege", resource, `${entry.privilege}${entry.grantable ? " WITH GRANT OPTION" : ""} is not declared.`, role);
241
+ for (const privilege of missing) add("missing", "missing-routine-privilege", resource, `${privilege} is required.`, role);
242
+ if (!excess.length && !missing.length) continue;
243
+ statements.push(`REVOKE ALL PRIVILEGES ON FUNCTION ${resource} FROM ${grantee(role)}`);
244
+ if (desired.length) statements.push(`GRANT ${desired.join(", ")} ON FUNCTION ${resource} TO ${grantee(role)}`);
245
+ }
246
+ }
187
247
  for (const role of roles) {
188
248
  reconcile("DATABASE", identifier(database.name), database.acl, role, ["CONNECT"]);
189
249
  for (const entry of state.schemas) {
@@ -222,11 +282,40 @@ async function plan(db, input) {
222
282
  const tables = state.relations.filter((entry) => entry.schema === contract.schema && entry.kind !== "S");
223
283
  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.");
224
284
  const declared = new Set(contract.managedTables);
285
+ const retained = new Set(contract.retainedTables);
225
286
  for (const table of tables) if (table.name.startsWith("_smrt_")) declared.add(table.name);
287
+ for (const table of retained) if (declared.has(table)) unsupported("retained-managed-table", qualified(contract.schema, table), "Retained tables must be outside the managed and framework table surfaces.");
226
288
  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.");
289
+ for (const table of retained) if (!tables.some((entry) => entry.name === table)) unsupported("missing-retained-table", qualified(contract.schema, table), "Retained tables must already exist before permission reconciliation.");
290
+ for (const relation of state.inherits) {
291
+ const managedParent = relation.parent_schema === contract.schema && declared.has(relation.parent_table ?? "");
292
+ const childRetained = relation.child_schema === contract.schema && retained.has(relation.child_table);
293
+ const parentRetained = relation.parent_schema === contract.schema && retained.has(relation.parent_table ?? "");
294
+ if (managedParent && relation.child_schema !== contract.schema) unsupported("managed-inheritance", qualified(relation.child_schema, relation.child_table), "Managed tables may not have external inheritance or partition children: parent table privileges can invoke external triggers outside the retained-table isolation boundary.");
295
+ else if (childRetained !== parentRetained) unsupported("retained-inheritance", qualified(relation.child_schema, relation.child_table), "Retained tables may inherit only from retained tables in the dedicated schema; an accessible parent can otherwise expose retained rows.");
296
+ }
297
+ for (const relation of state.foreignKeys) {
298
+ const managedParent = relation.parent_schema === contract.schema && declared.has(relation.parent_table ?? "");
299
+ const childRetained = relation.child_schema === contract.schema && retained.has(relation.child_table);
300
+ const parentRetained = relation.parent_schema === contract.schema && retained.has(relation.parent_table ?? "");
301
+ if (managedParent && relation.child_schema !== contract.schema) {
302
+ unsupported("managed-foreign-key", `${qualified(relation.child_schema, relation.child_table)} -> ${qualified(relation.parent_schema, relation.parent_table ?? "")}`, "Managed tables may not have external foreign-key children: referential actions can invoke external triggers outside the retained-table isolation boundary.");
303
+ continue;
304
+ }
305
+ if (childRetained === parentRetained) continue;
306
+ unsupported("retained-foreign-key", `${qualified(relation.child_schema, relation.child_table)} -> ${qualified(relation.parent_schema, relation.parent_table ?? "")}`, "Retained tables may reference only retained tables; referential actions from an accessible table can otherwise mutate retained rows.");
307
+ }
308
+ for (const rule of state.rewriteRules) {
309
+ const managedSource = rule.schema === contract.schema && declared.has(rule.table_name);
310
+ const retainedSource = rule.schema === contract.schema && retained.has(rule.table_name);
311
+ const managedTarget = rule.referenced_schema === contract.schema && declared.has(rule.referenced_table);
312
+ if (retainedSource && managedTarget) unsupported("user-rewrite-rule", `${qualified(rule.schema, rule.table_name)} (${rule.rule_name})`, "Retained tables may not reference managed tables through rewrite rules.");
313
+ else if (managedSource && rule.referenced_schema !== contract.schema) unsupported("user-rewrite-rule", `${qualified(rule.schema, rule.table_name)} (${rule.rule_name})`, "Managed rewrite rules may not reference external relations: rule-owner table privileges can invoke external triggers outside the retained-table isolation boundary.");
314
+ else if (rule.referenced_schema === contract.schema && retained.has(rule.referenced_table)) unsupported("user-rewrite-rule", `${qualified(rule.schema, rule.table_name)} (${rule.rule_name})`, "User-defined rewrite rules are unsupported because rule-owner privileges can write retained tables indirectly.");
315
+ }
227
316
  for (const [table, columns] of Object.entries(contract.monitor?.tables ?? {})) {
228
317
  const relation = tables.find((entry) => entry.name === table);
229
- if (!declared.has(table) || !relation) unsupported("monitor-table", table, "Monitor tables must exist and belong to managedTables or framework tables.");
318
+ if (!declared.has(table) || retained.has(table) || !relation) unsupported("monitor-table", table, "Monitor tables must exist and belong to the managed or framework table surface, never retained tables.");
230
319
  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.");
231
320
  }
232
321
  for (const relation of state.relations) {
@@ -237,7 +326,8 @@ async function plan(db, input) {
237
326
  continue;
238
327
  }
239
328
  if (relation.owner !== contract.migrationOwner) unsupported("resource-owner", resource, `Managed resources must be owned by ${contract.migrationOwner}; ownership is never changed.`);
240
- 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.");
329
+ if (relation.kind !== "S" && !declared.has(relation.name) && !retained.has(relation.name)) unsupported("undeclared-table", resource, "The dedicated schema contains an undeclared table; defaults cannot safely cover a mixed schema.");
330
+ if (relation.kind === "S" && (!relation.parent_table || !declared.has(relation.parent_table) && !retained.has(relation.parent_table))) unsupported("undeclared-sequence", resource, "Sequences must belong to a declared managed or retained table before permission reconciliation.");
241
331
  if (![
242
332
  "r",
243
333
  "p",
@@ -246,7 +336,7 @@ async function plan(db, input) {
246
336
  if (relation.rls) unsupported("row-level-security", resource, "RLS policies require a separate application authorization review; ACL verification cannot establish allowed row operations.");
247
337
  for (const role of roles) {
248
338
  const isRuntime = role === contract.runtimeRole;
249
- const desired = isRuntime ? relation.kind === "S" ? ["USAGE"] : bookkeeping.has(relation.name) ? ["SELECT"] : [
339
+ const desired = isRuntime ? relation.kind === "S" ? retained.has(relation.parent_table ?? "") ? [] : ["USAGE"] : retained.has(relation.name) ? [] : bookkeeping.has(relation.name) ? ["SELECT"] : [
250
340
  "SELECT",
251
341
  "INSERT",
252
342
  "UPDATE",
@@ -254,12 +344,35 @@ async function plan(db, input) {
254
344
  ] : [];
255
345
  reconcile(relation.kind === "S" ? "SEQUENCE" : "TABLE", resource, relation.acl, role, desired);
256
346
  for (const column of columns) {
257
- const wanted = !isRuntime && (contract.monitor?.tables[relation.name] ?? []).includes(column.name) ? ["SELECT"] : [];
347
+ const wanted = !isRuntime && !retained.has(relation.name) && (contract.monitor?.tables[relation.name] ?? []).includes(column.name) ? ["SELECT"] : [];
258
348
  reconcile("TABLE", resource, column.acl, role, wanted, ` (${identifier(column.name)})`);
259
349
  }
260
350
  }
261
351
  }
262
- for (const category of ["routines", "types"]) for (const resource of state[category]) for (const role of roles) if (resource.schema === contract.schema || resource.owner === role || relevant(resource.acl, role).length) unsupported(`unsupported-${category}`, qualified(resource.schema, resource.name), `User-defined ${category} are outside the supported table/sequence contract; review their effective privileges explicitly.`, role);
352
+ const triggerRoutineNames = new Set(contract.managedTriggerFunctions);
353
+ const acceptedTriggerRoutineNames = /* @__PURE__ */ new Set();
354
+ const acceptedTriggerRoutineOids = /* @__PURE__ */ new Set();
355
+ const routineConfigIsEmpty = (value) => Array.isArray(value) && value.length === 0 || value === "[]";
356
+ for (const resource of state.routines) {
357
+ const resourceName = qualified(resource.schema, resource.name);
358
+ const framework = frameworkRoutines.find((expected) => resource.schema === contract.schema && resource.name === expected.name && normalizeArgumentTypes(resource.argument_types) === expected.argumentTypes);
359
+ if (framework) {
360
+ 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.");
361
+ else reconcileRoutine(resource.identity, resource.acl, /* @__PURE__ */ new Map([[contract.runtimeRole, ["EXECUTE"]]]));
362
+ continue;
363
+ }
364
+ const bindings = state.triggers.filter((trigger) => trigger.function_oid === resource.oid);
365
+ 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))) {
366
+ acceptedTriggerRoutineNames.add(resource.name);
367
+ acceptedTriggerRoutineOids.add(resource.oid);
368
+ reconcileRoutine(resource.identity, resource.acl, /* @__PURE__ */ new Map());
369
+ continue;
370
+ }
371
+ 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);
372
+ }
373
+ 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.");
374
+ for (const trigger of state.triggers) if (trigger.schema === contract.schema && declared.has(trigger.table_name) && trigger.internal === false && (trigger.enabled === "O" || trigger.enabled === "A") && !acceptedTriggerRoutineOids.has(trigger.function_oid)) unsupported("unsupported-managed-trigger", `${qualified(trigger.schema, trigger.table_name)} (${trigger.name})`, "Enabled managed-table triggers must use an exact declared invoker trigger routine; undeclared or SECURITY DEFINER trigger functions can bypass retained-table isolation.");
375
+ 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);
263
376
  for (const defaults of state.defaults) {
264
377
  const resource = `DEFAULT ${defaults.kind} (${defaults.schema || "global"})`;
265
378
  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.`);
@@ -313,7 +426,8 @@ async function plan(db, input) {
313
426
  "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.",
314
427
  "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.",
315
428
  "Pause migrations and external ACL/role writers during planning and apply. The advisory lock coordinates SMRT permission writers only.",
316
- "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."
429
+ "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.",
430
+ "Retained tables are operator-owned data outside the runtime and monitor surfaces. Creator defaults initially grant runtime access to every new table and sequence, so create or restore every retained table first, then apply its reviewed plan and verify access is revoked before reactivating restricted roles."
317
431
  ],
318
432
  statements: [...new Set(statements)],
319
433
  canApply: !diagnostics.some((entry) => entry.severity === "unsupported"),
@@ -329,20 +443,20 @@ async function planPostgresPermissions(db, contract) {
329
443
  }
330
444
  /** Apply the reviewed contract atomically, rejecting stale catalog snapshots. */
331
445
  async function applyPostgresPermissions(db, contract, options) {
332
- validatePostgresPermissionContract(contract);
446
+ const normalizedContract = validatePostgresPermissionContract(contract);
333
447
  if (!options || !/^[a-f0-9]{64}$/.test(options.expectedFingerprint)) throw new Error("An expectedFingerprint from a reviewed permission plan is required.");
334
448
  if (!db.transaction) throw new Error("PostgreSQL permissions require a transaction-capable database.");
335
449
  return db.transaction(async (tx) => {
336
450
  await tx.query("SET LOCAL lock_timeout = '5s'");
337
451
  await tx.query("SET LOCAL statement_timeout = '30s'");
338
- await tx.query(`SELECT pg_advisory_xact_lock(hashtext(${literal(`smrt-permissions:${contract.schema}`)}))`);
339
- const before = await plan(tx, contract);
452
+ await tx.query(`SELECT pg_advisory_xact_lock(hashtext(${literal(`smrt-permissions:${normalizedContract.schema}`)}))`);
453
+ const before = await plan(tx, normalizedContract);
340
454
  if (before.fingerprint !== options.expectedFingerprint) throw new Error("Permission plan is stale; generate and review a new plan.");
341
455
  if (!before.canApply) throw new Error("Permission plan has unsupported privilege sources or missing prerequisites; resolve its diagnostics first.");
342
456
  const executor = (await tx.query("SELECT current_user AS name, rolsuper FROM pg_roles WHERE rolname=current_user")).rows[0];
343
- if (!executor.rolsuper && executor.name !== contract.migrationOwner) throw new Error("Apply must execute as the migration owner or a PostgreSQL superuser.");
457
+ if (!executor.rolsuper && executor.name !== normalizedContract.migrationOwner) throw new Error("Apply must execute as the migration owner or a PostgreSQL superuser.");
344
458
  for (const statement of before.statements) await tx.query(statement);
345
- const after = await plan(tx, contract);
459
+ const after = await plan(tx, normalizedContract);
346
460
  if (!after.canApply || after.diagnostics.length) throw new Error("Permission verification failed; all permission changes have been rolled back.");
347
461
  return after;
348
462
  });