@homeflare/alchemy 0.44.0 → 0.45.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 (53) hide show
  1. package/dist/postgres/fake-role-sql.d.ts +2 -1
  2. package/dist/postgres/fake-role-sql.d.ts.map +1 -1
  3. package/dist/postgres/fake-sql.d.ts +3 -3
  4. package/dist/postgres/fake-sql.d.ts.map +1 -1
  5. package/dist/postgres/index.d.ts +1 -1
  6. package/dist/postgres/index.d.ts.map +1 -1
  7. package/dist/postgres/index.js +117 -94
  8. package/dist/postgres/index.js.map +11 -11
  9. package/dist/postgres/role-attrs.d.ts +7 -4
  10. package/dist/postgres/role-attrs.d.ts.map +1 -1
  11. package/dist/postgres/role-diff.d.ts +2 -2
  12. package/dist/postgres/role-diff.d.ts.map +1 -1
  13. package/dist/postgres/role-errors.d.ts +13 -7
  14. package/dist/postgres/role-errors.d.ts.map +1 -1
  15. package/dist/postgres/role-membership-sql.d.ts +30 -27
  16. package/dist/postgres/role-membership-sql.d.ts.map +1 -1
  17. package/dist/postgres/role-provider.d.ts.map +1 -1
  18. package/dist/postgres/role-scram.d.ts +2 -0
  19. package/dist/postgres/role-scram.d.ts.map +1 -1
  20. package/dist/postgres/role-secrets.d.ts +8 -0
  21. package/dist/postgres/role-secrets.d.ts.map +1 -1
  22. package/dist/postgres/role-sql.d.ts.map +1 -1
  23. package/dist/postgres/role.d.ts +2 -2
  24. package/dist/postgres/role.d.ts.map +1 -1
  25. package/docs/postgres-role.md +18 -6
  26. package/package.json +1 -1
  27. package/src/ceph/ceph-argv.test.ts +1 -1
  28. package/src/launchd/sudo-allowlist.test.ts +2 -2
  29. package/src/linux/sudo-allowlist.test.ts +3 -3
  30. package/src/linux/sudo-runner.test.ts +2 -2
  31. package/src/postgres/fake-role-sql.ts +26 -6
  32. package/src/postgres/fake-sql.ts +10 -5
  33. package/src/postgres/index.ts +1 -0
  34. package/src/postgres/role-attrs.ts +7 -4
  35. package/src/postgres/role-diff.ts +7 -3
  36. package/src/postgres/role-errors.ts +22 -14
  37. package/src/postgres/role-guard.test.ts +3 -3
  38. package/src/postgres/role-identity.test.ts +14 -2
  39. package/src/postgres/role-lifecycle.test.ts +2 -2
  40. package/src/postgres/role-membership-options.test.ts +73 -0
  41. package/src/postgres/role-membership-repair.test.ts +153 -0
  42. package/src/postgres/role-membership-sql.ts +88 -63
  43. package/src/postgres/role-password-ascii.test.ts +53 -0
  44. package/src/postgres/role-password-transports.test.ts +123 -0
  45. package/src/postgres/role-privilege.test.ts +37 -10
  46. package/src/postgres/role-provenance.test.ts +1 -1
  47. package/src/postgres/role-provider.ts +3 -2
  48. package/src/postgres/role-refusals.test.ts +0 -25
  49. package/src/postgres/role-scram.ts +2 -0
  50. package/src/postgres/role-secrets.ts +15 -0
  51. package/src/postgres/role-sql.ts +3 -1
  52. package/src/postgres/role.ts +9 -1
  53. package/src/postgres/schema-socket.test.ts +1 -0
@@ -37,7 +37,8 @@ export interface PostgresRoleProps {
37
37
  readonly login: boolean;
38
38
  /** `CONNECTION LIMIT n`, `-1` for unlimited — maps to `pg_roles.rolconnlimit`. */
39
39
  readonly connectionLimit: number;
40
- /** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit`. */
40
+ /** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit`; false also clears per-grant
41
+ * INHERIT on declared memberships (PG 16+ uses the role flag only to default new grants). */
41
42
  readonly inherit: boolean;
42
43
  /**
43
44
  * `VALID UNTIL 'timestamp'` — maps to `pg_roles.rolvaliduntil`. ISO 8601 without a timezone is
@@ -86,18 +87,20 @@ export interface PostgresRoleAttributes {
86
87
  */
87
88
  readonly memberOf: readonly string[] | null;
88
89
  /**
89
- * The membership rows behind `memberOf`, including `admin_option` and `set_option`. Present
90
+ * The membership rows behind `memberOf`, including `admin_option`, `set_option` and
91
+ * `inherit_option`. Present
90
92
  * on a catalog read; stripped before attributes are stored (`storedAttributes`), because the
91
93
  * engine's diff receives stored attributes and would never see them there. The provider's
92
94
  * diff reads these rows live. `grantor` is the role that made the grant
93
- * (`pg_auth_members.grantor`), null when that role was dropped — a dropped-grantor row cannot
94
- * be named in `REVOKE … GRANTED BY`, so reconcile can only fail typed on it.
95
+ * (`pg_auth_members.grantor`). Null is a defensive inconsistent-catalog case, not an ordinary
96
+ * DROP ROLE outcome; such a row cannot be named in `REVOKE … GRANTED BY`.
95
97
  */
96
98
  readonly memberships?: readonly {
97
99
  readonly parent: string;
98
100
  readonly grantor: string | null;
99
101
  readonly admin: boolean;
100
102
  readonly set: boolean;
103
+ readonly inherit: boolean;
101
104
  }[];
102
105
  /**
103
106
  * A seal of the last password value written, or `''` if none was ever written (`seal()` from
@@ -1 +1 @@
1
- {"version":3,"file":"role-attrs.d.ts","sourceRoot":"","sources":["../../src/postgres/role-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAExD,oEAAoE;AACpE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAe/F,eAAO,MAAM,iBAAiB,UACrB,MAAM,KACZ;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAA;CAAE,GAAG,SAOrD,CAAC;AAEF;mEACmE;AACnE,eAAO,MAAM,cAAc,aAAc,MAAM,QAAQ,MAAM,GAAG,IAAI,KAAG,OACX,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,+FAA+F;AAC/F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;KACvB,EAAE,CAAC;IACJ;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B"}
1
+ {"version":3,"file":"role-attrs.d.ts","sourceRoot":"","sources":["../../src/postgres/role-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAExD,oEAAoE;AACpE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAe/F,eAAO,MAAM,iBAAiB,UACrB,MAAM,KACZ;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAA;CAAE,GAAG,SAOrD,CAAC;AAEF;mEACmE;AACnE,eAAO,MAAM,cAAc,aAAc,MAAM,QAAQ,MAAM,GAAG,IAAI,KAAG,OACX,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;iGAC6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,+FAA+F;AAC/F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5C;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC3B,EAAE,CAAC;IACJ;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B"}
@@ -2,7 +2,7 @@ import type { Input } from 'alchemy/Input';
2
2
  import * as Effect from 'effect/Effect';
3
3
  import type { Environment } from '../secrets/write-only.ts';
4
4
  import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
5
- import { PostgresRoleIdentityRefused, type PostgresRoleNameRefused, PostgresRoleRenameRefused, type PostgresRoleValidUntilRefused } from './role-errors.ts';
5
+ import { PostgresRoleIdentityRefused, type PostgresRoleNameRefused, type PostgresRolePasswordNonAsciiRefused, PostgresRoleRenameRefused, type PostgresRoleValidUntilRefused } from './role-errors.ts';
6
6
  /** A catalog read the provider performed for this diff. `found: undefined` means the name is
7
7
  * absent. Omitting the argument means no catalog read (the offline comparison). */
8
8
  export interface RoleObservation {
@@ -18,5 +18,5 @@ export declare const diffPostgresRole: (news: Input<PostgresRoleProps>, output:
18
18
  readonly action: 'update';
19
19
  } | {
20
20
  readonly action: 'noop';
21
- } | undefined, PostgresRoleRenameRefused | PostgresRoleNameRefused | PostgresRoleValidUntilRefused | PostgresRoleIdentityRefused>;
21
+ } | undefined, PostgresRoleRenameRefused | PostgresRoleNameRefused | PostgresRoleValidUntilRefused | PostgresRoleIdentityRefused | PostgresRolePasswordNonAsciiRefused>;
22
22
  //# sourceMappingURL=role-diff.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-diff.d.ts","sourceRoot":"","sources":["../../src/postgres/role-diff.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EACL,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,yBAAyB,EACzB,KAAK,6BAA6B,EACnC,MAAM,kBAAkB,CAAC;AAM1B;mFACmF;AACnF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,SACrB,KAAK,CAAC,iBAAiB,CAAC,UACtB,sBAAsB,GAAG,SAAS,QACrC,WAAW,gBACF,eAAe,KAC5B,MAAM,CAAC,MAAM,CACd;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACrE,yBAAyB,GACzB,uBAAuB,GACvB,6BAA6B,GAC7B,2BAA2B,CAwC3B,CAAC"}
1
+ {"version":3,"file":"role-diff.d.ts","sourceRoot":"","sources":["../../src/postgres/role-diff.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EACL,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,mCAAmC,EACxC,yBAAyB,EACzB,KAAK,6BAA6B,EACnC,MAAM,kBAAkB,CAAC;AAM1B;mFACmF;AACnF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,SACrB,KAAK,CAAC,iBAAiB,CAAC,UACtB,sBAAsB,GAAG,SAAS,QACrC,WAAW,gBACF,eAAe,KAC5B,MAAM,CAAC,MAAM,CACd;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACrE,yBAAyB,GACzB,uBAAuB,GACvB,6BAA6B,GAC7B,2BAA2B,GAC3B,mCAAmC,CA0CnC,CAAC"}
@@ -82,12 +82,9 @@ export declare class PostgresRoleCreateVanished extends PostgresRoleCreateVanish
82
82
  declare const PostgresRoleMembershipUnrepaired_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
83
83
  readonly _tag: "PostgresRoleMembershipUnrepaired";
84
84
  } & Readonly<A>;
85
- /** A `pg_auth_members` row survived reconcile in a shape the declaration does not allow — not
86
- * wanted, still `ADMIN`, or still `SET` at the upstream default TRUE. A live grantor's row is
87
- * named in `REVOKE … GRANTED BY`, which either binds or fails loudly; the one shape no
88
- * statement this family may issue can touch is a row whose grantor role was dropped
89
- * (`pg_auth_members` keeps it keyed on the dead oid, and no `GRANTED BY` name reaches it), so
90
- * the message asks the operator to revoke it by hand as a bootstrap superuser and re-plan. */
85
+ /** An option repair was refused, or a catalog row survived outside the declaration. A null grantor is a
86
+ * defensive corruption/test case: PostgreSQL tracks grantor dependencies, so ordinary
87
+ * DROP ROLE does not leave a dangling grantor. Refuse instead of claiming convergence. */
91
88
  export declare class PostgresRoleMembershipUnrepaired extends PostgresRoleMembershipUnrepaired_base<{
92
89
  readonly role: string;
93
90
  readonly parent: string;
@@ -121,6 +118,15 @@ export declare class PostgresRoleIdentityRefused extends PostgresRoleIdentityRef
121
118
  }> {
122
119
  get message(): string;
123
120
  }
124
- export type PostgresRoleError = PostgresRoleNameRefused | PostgresRoleRenameRefused | PostgresRoleValidUntilRefused | PostgresRolePasswordEnvUnsetError | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused;
121
+ declare const PostgresRolePasswordNonAsciiRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
122
+ readonly _tag: "PostgresRolePasswordNonAsciiRefused";
123
+ } & Readonly<A>;
124
+ /** Never carry the rejected password in this error: errors reach logs and traces. */
125
+ export declare class PostgresRolePasswordNonAsciiRefused extends PostgresRolePasswordNonAsciiRefused_base<{
126
+ readonly role: string;
127
+ }> {
128
+ get message(): string;
129
+ }
130
+ export type PostgresRoleError = PostgresRolePasswordNonAsciiRefused | PostgresRoleNameRefused | PostgresRoleRenameRefused | PostgresRoleValidUntilRefused | PostgresRolePasswordEnvUnsetError | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused;
125
131
  export {};
126
132
  //# sourceMappingURL=role-errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/role-errors.ts"],"names":[],"mappings":";;;AAWA;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,6BAA4C;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;;iDAIiD;AACjD,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;yDAGyD;AACzD,qBAAa,iCAAkC,SAAQ,uCAErD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;yFAEyF;AACzF,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;yEAEyE;AACzE,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAC;CAC9C,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;uEAEuE;AACvE,qBAAa,0BAA2B,SAAQ,gCAA+C;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;;;;8FAK8F;AAC9F,qBAAa,gCAAiC,SAAQ,sCAEpD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAY7B;CACF;;;;AAED;;sCAEsC;AACtC,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;mDAEmD;AACnD,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;AAED,MAAM,MAAM,iBAAiB,GACzB,uBAAuB,GACvB,yBAAyB,GACzB,6BAA6B,GAC7B,iCAAiC,GACjC,6BAA6B,GAC7B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,CAAC"}
1
+ {"version":3,"file":"role-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/role-errors.ts"],"names":[],"mappings":";;;AAYA;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,6BAA4C;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;;iDAIiD;AACjD,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;yDAGyD;AACzD,qBAAa,iCAAkC,SAAQ,uCAErD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;yFAEyF;AACzF,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;yEAEyE;AACzE,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAC;CAC9C,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;uEAEuE;AACvE,qBAAa,0BAA2B,SAAQ,gCAA+C;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;0FAE0F;AAC1F,qBAAa,gCAAiC,SAAQ,sCAEpD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAY7B;CACF;;;;AAED;;sCAEsC;AACtC,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;mDAEmD;AACnD,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED,qFAAqF;AACrF,qBAAa,mCAAoC,SAAQ,yCAEvD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;IAC1B,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;AAED,MAAM,MAAM,iBAAiB,GACzB,mCAAmC,GACnC,uBAAuB,GACvB,yBAAyB,GACzB,6BAA6B,GAC7B,iCAAiC,GACjC,6BAA6B,GAC7B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,CAAC"}
@@ -11,8 +11,8 @@
11
11
  * are exercised (`user-manag.sgml`). A safe seat membership is `ADMIN FALSE, SET FALSE`.
12
12
  * ⛔ REVOKE BY GRANTOR. A plain `REVOKE parent FROM member` only touches rows the session's
13
13
  * own grantor made; Postgres 18 warns and leaves another grantor's rows standing
14
- * (`revoke.sgml`). Every revoke here names `GRANTED BY`, so the statement either removes
15
- * the row it targets or fails loudly — never a silent no-op.
14
+ * (`revoke.sgml`). Every revoke here names `GRANTED BY`. A mismatched grantor can still
15
+ * produce only a warning; the catalog re-read below catches surviving unsafe rows.
16
16
  */
17
17
  import type { SqlError } from 'effect/unstable/sql/SqlError';
18
18
  import * as Effect from 'effect/Effect';
@@ -20,30 +20,40 @@ import type { PgExecutor } from './database-sql.ts';
20
20
  import { PostgresRoleMembershipUnrepaired, PostgresRoleParentMissing } from './role-errors.ts';
21
21
  import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
22
22
  /** One `pg_auth_members` row as this family reads it: the parent name, the grantor who made
23
- * the grant, and the two options a name-only compare would hide. `admin` true is `WITH ADMIN`;
23
+ * the grant, and the three options a name-only compare would hide. `admin` true is `WITH ADMIN`;
24
+ * `inherit` true inherits the parent's privileges even when the member is NOINHERIT;
24
25
  * `set` true (the `GRANT` default) lets the member `SET ROLE` to the parent. `grantor` is null
25
- * when the granting role was later dropped — such a row can never be named in a `REVOKE …
26
- * GRANTED BY`, so it can only be repaired by hand as a bootstrap superuser. */
26
+ * only in an inconsistent catalog or a synthetic test row, not after an ordinary DROP ROLE:
27
+ * PostgreSQL tracks grantor dependencies. Keep the null guard as a defensive refusal. */
27
28
  export interface MembershipRow {
28
29
  readonly parent: string;
29
30
  readonly grantor: string | null;
30
31
  readonly admin: boolean;
31
32
  readonly set: boolean;
33
+ readonly inherit: boolean;
32
34
  }
33
35
  export declare const selectRoleMemberships: (pg: PgExecutor, name: string) => Effect.Effect<readonly MembershipRow[], SqlError>;
34
36
  /** Seat grants say `SET FALSE`. Upstream `GRANT` defaults `SET` to TRUE, and `SET ROLE` to the
35
37
  * parent is how `SUPERUSER` / `CREATEROLE` / `CREATEDB` are exercised (`user-manag.sgml`). */
36
38
  export declare const buildGrantMembershipSql: (member: string, parent: string) => string;
37
- /** Repair an existing membership in place: one atomic full-options `GRANT` per parent.
38
- * Upstream keeps any `WITH` option the new `GRANT` omits (`grant.sgml`), so a repair must
39
- * spell both bits — `ADMIN FALSE, SET FALSE` — rather than rely on `SET FALSE` alone. */
40
- export declare const buildRepairMembershipSql: (member: string, parent: string) => string;
39
+ /** ⛔ Do not repair with GRANT … ADMIN FALSE: measured in PR 336, that left grants made
40
+ * by the member standing after stripping its ADMIN. PG 16+ tracks dependent role grants:
41
+ * https://www.postgresql.org/docs/16/sql-grant.html#SQL-GRANT-DESCRIPTION-ROLES
42
+ * https://www.postgresql.org/docs/16/sql-revoke.html
43
+ * Measured 2026-10-02 on PG 17.11: REVOKE ADMIN OPTION … RESTRICT refuses dependents;
44
+ * without dependents, ADMIN/SET revocation keeps membership and its INHERIT bit intact.
45
+ * INHERIT revocation explicitly clears that bit for a declared NOINHERIT role.
46
+ * GRANTED BY targets the observed row, including a different grantor's row. */
47
+ export declare const buildRepairMembershipSql: (member: string, parent: string, grantor: string, option: 'ADMIN' | 'SET' | 'INHERIT') => string;
41
48
  /** Remove one grantor's row for one parent. Naming the grantor is what makes the revoke
42
49
  * bind: a plain `REVOKE` skips rows other grantors made (`revoke.sgml`). */
43
50
  export declare const buildRevokeGrantorMembershipSql: (member: string, parent: string, grantor: string) => string;
44
- /** Catalog flags this family never declares. A create lands on the server default (all false);
45
- * an absent field on a seeded test row is that default. */
46
- export declare const unsafeMemberships: (live: Omit<PostgresRoleAttributes, 'passwordSeal'>) => ReadonlySet<string>;
51
+ /** ADMIN/SET are always unsafe; per-grant INHERIT is unsafe for declared NOINHERIT.
52
+ * PG 16–18 GRANT docs: the role attribute only defaults NEW membership grants. */
53
+ export declare const unsafeMemberships: (live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
54
+ /** The DECLARED inherit flag (never the live one): a declared NOINHERIT role must see live
55
+ * per-grant INHERIT rows as unsafe even when the live role flag says INHERIT. */
56
+ inherit: boolean) => ReadonlySet<string>;
47
57
  /** Membership drift: sorted set difference between declared and live. Duplicates in the
48
58
  * declaration collapse to one; `undefined` means "not asserted" — nothing to grant or revoke.
49
59
  * A parent already granted `WITH ADMIN`, or with `SET` still at the upstream default TRUE, is
@@ -56,20 +66,13 @@ export declare const membershipDrift: (declared: readonly string[] | undefined,
56
66
  * missing role is `42704` (`user.c@REL_18_6`), and on a create that error arrives only after
57
67
  * `CREATE ROLE` has committed. */
58
68
  export declare const assertParentsExist: (pg: PgExecutor, role: string, parents: readonly string[] | undefined) => Effect.Effect<void, PostgresRoleParentMissing | SqlError>;
59
- /** Make live memberships equal the declaration, never dropping a membership the declaration
60
- * keeps. A wanted parent that is already granted is repaired with one
61
- * `GRANT … WITH ADMIN FALSE, SET FALSE` — `AddRoleMems` updates that grantor's row in place
62
- * (`user.c@REL_18_6` `SearchSysCache3` on role, member, grantor) — and any other grantor's
63
- * unsafe row is then revoked by name.
64
- *
65
- * 1. Rows the declaration does not want are revoked **by their own grantor** (dead-grantor
66
- * rows are skipped and left for the backstop to report).
67
- * 2. Unsafe-but-wanted rows get one full-options repair `GRANT` per parent — atomic, in place,
68
- * so the membership never drops even when another grantor's row also exists.
69
- * 3. A repair leaves other grantors' unsafe rows beside the safe one, so a mid re-read
70
- * revokes every remaining unsafe row with a live grantor.
71
- * 4. Absent-but-wanted parents get a fresh seat grant.
72
- * 5. Backstop re-read: any row that is not wanted, or still unsafe, fails typed — including a
73
- * dead-grantor row, which no statement this family may issue can repair. */
69
+ /** Make live memberships equal the declaration, preserving wanted memberships in place.
70
+ * The previous full-options GRANT updated only its own grantor's row, then revoked other
71
+ * unsafe rows; it could silently strip ADMIN despite dependent grants (PR 336 round 2).
72
+ * Now each unsafe option is revoked by its observed grantor with RESTRICT. Unwanted rows
73
+ * commit first, so a blocked ADMIN repair cannot restore unwanted access (PR 349 review).
74
+ * Each retained row's options share a transaction, attributing a dependency/permission
75
+ * refusal to that parent and grantor. No CASCADE and no replacement grantor row.
76
+ * Unnameable grantors are defensive catalog-corruption cases, left to the typed backstop. */
74
77
  export declare const syncMemberships: (pg: PgExecutor, props: PostgresRoleProps, rows: readonly MembershipRow[]) => Effect.Effect<readonly string[], SqlError | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing>;
75
78
  //# sourceMappingURL=role-membership-sql.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-membership-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-membership-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,gCAAgC,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEjF;;;;+EAI+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAaD,eAAO,MAAM,qBAAqB,OAC5B,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,SAAS,aAAa,EAAE,EAAE,QAAQ,CAC2B,CAAC;AAE/E;8FAC8F;AAC9F,eAAO,MAAM,uBAAuB,WAAY,MAAM,UAAU,MAAM,KAAG,MACF,CAAC;AAExE;;yFAEyF;AACzF,eAAO,MAAM,wBAAwB,WAAY,MAAM,UAAU,MAAM,KAAG,MACU,CAAC;AAErF;4EAC4E;AAC5E,eAAO,MAAM,+BAA+B,WAClC,MAAM,UACN,MAAM,WACL,MAAM,KACd,MAC0F,CAAC;AAE9F;2DAC2D;AAC3D,eAAO,MAAM,iBAAiB,SACtB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,WAAW,CAAC,MAAM,CAC6E,CAAC;AAEnG;;;6FAG6F;AAC7F,eAAO,MAAM,eAAe,aAChB,SAAS,MAAM,EAAE,GAAG,SAAS,QACjC,SAAS,MAAM,EAAE,WACf,WAAW,CAAC,MAAM,CAAC,KAC1B;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAW3E,CAAC;AAEF;;kCAEkC;AAClC,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,WACH,SAAS,MAAM,EAAE,GAAG,SAAS,KACrC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CAQvD,CAAC;AAEL;;;;;;;;;;;;;;+EAc+E;AAC/E,eAAO,MAAM,eAAe,OACtB,UAAU,SACP,iBAAiB,QAClB,SAAS,aAAa,EAAE,KAC7B,MAAM,CAAC,MAAM,CACd,SAAS,MAAM,EAAE,EACjB,QAAQ,GAAG,gCAAgC,GAAG,yBAAyB,CA2DrE,CAAC"}
1
+ {"version":3,"file":"role-membership-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-membership-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,gCAAgC,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEjF;;;;;yFAKyF;AACzF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAcD,eAAO,MAAM,qBAAqB,OAC5B,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,SAAS,aAAa,EAAE,EAAE,QAAQ,CAC2B,CAAC;AAE/E;8FAC8F;AAC9F,eAAO,MAAM,uBAAuB,WAAY,MAAM,UAAU,MAAM,KAAG,MACF,CAAC;AAExE;;;;;;;+EAO+E;AAC/E,eAAO,MAAM,wBAAwB,WAC3B,MAAM,UACN,MAAM,WACL,MAAM,UACP,OAAO,GAAG,KAAK,GAAG,SAAS,KAClC,MACwH,CAAC;AAE5H;4EAC4E;AAC5E,eAAO,MAAM,+BAA+B,WAClC,MAAM,UACN,MAAM,WACL,MAAM,KACd,MAC0F,CAAC;AAE9F;kFACkF;AAClF,eAAO,MAAM,iBAAiB,SACtB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC;AAClD;kFACkF;SACzE,OAAO,KACf,WAAW,CAAC,MAAM,CAKlB,CAAC;AAEJ;;;6FAG6F;AAC7F,eAAO,MAAM,eAAe,aAChB,SAAS,MAAM,EAAE,GAAG,SAAS,QACjC,SAAS,MAAM,EAAE,WACf,WAAW,CAAC,MAAM,CAAC,KAC1B;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAW3E,CAAC;AAEF;;kCAEkC;AAClC,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,WACH,SAAS,MAAM,EAAE,GAAG,SAAS,KACrC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CAQvD,CAAC;AAEL;;;;;;;6FAO6F;AAC7F,eAAO,MAAM,eAAe,OACtB,UAAU,SACP,iBAAiB,QAClB,SAAS,aAAa,EAAE,KAC7B,MAAM,CAAC,MAAM,CACd,SAAS,MAAM,EAAE,EACjB,QAAQ,GAAG,gCAAgC,GAAG,yBAAyB,CAuErE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"role-provider.d.ts","sourceRoot":"","sources":["../../src/postgres/role-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAM7C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAOzC,eAAO,MAAM,oBAAoB,4PAkF/B,CAAC;AAEH,eAAO,MAAM,oBAAoB,0HAA6D,CAAC"}
1
+ {"version":3,"file":"role-provider.d.ts","sourceRoot":"","sources":["../../src/postgres/role-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAM7C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAOzC,eAAO,MAAM,oBAAoB,4PAmF/B,CAAC;AAEH,eAAO,MAAM,oBAAoB,0HAA6D,CAAC"}
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>` for one password. The salt is fresh per
3
3
  * call (same as `psql \password`); tests pass one to make the output deterministic.
4
+ * Callers first run `assertPasswordAscii`: raw non-ASCII may differ from PostgreSQL's
5
+ * SASLprep bytes (https://www.postgresql.org/docs/16/sasl-authentication.html).
4
6
  */
5
7
  export declare const scramSha256Verifier: (password: string, salt?: Buffer) => string;
6
8
  //# sourceMappingURL=role-scram.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-scram.d.ts","sourceRoot":"","sources":["../../src/postgres/role-scram.ts"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,eAAO,MAAM,mBAAmB,aAAc,MAAM,SAAS,MAAM,KAAG,MAMrE,CAAC"}
1
+ {"version":3,"file":"role-scram.d.ts","sourceRoot":"","sources":["../../src/postgres/role-scram.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,aAAc,MAAM,SAAS,MAAM,KAAG,MAMrE,CAAC"}
@@ -11,6 +11,8 @@
11
11
  * written — a plan never re-sends a secret that already matches, and never stores one.
12
12
  */
13
13
  import * as Redacted from 'effect/Redacted';
14
+ import * as Effect from 'effect/Effect';
15
+ import { PostgresRolePasswordNonAsciiRefused } from './role-errors.ts';
14
16
  import { type Environment } from '../secrets/write-only.ts';
15
17
  import type { PostgresRoleProps } from './role-attrs.ts';
16
18
  /** What the environment holds for the declared password. `value` is `undefined` when no
@@ -20,6 +22,12 @@ export interface ResolvedPassword {
20
22
  readonly variable: string | undefined;
21
23
  readonly value: Redacted.Redacted<string> | undefined;
22
24
  }
25
+ /** ⛔ PostgreSQL applies SASLprep before SCRAM, with a raw-byte fallback for prohibited input:
26
+ * https://www.postgresql.org/docs/16/sasl-authentication.html#SASL-SCRAM-SHA-256
27
+ * Non-ASCII can normalize to different bytes (e.g. soft hyphen disappears). Refuse before
28
+ * any write, even with a matching old seal, until we have a vendor-compatible normalizer.
29
+ * ASCII is unchanged, including prohibited ASCII controls via PostgreSQL's fallback. */
30
+ export declare const assertPasswordAscii: (role: string, resolved: ResolvedPassword) => Effect.Effect<void, PostgresRolePasswordNonAsciiRefused>;
23
31
  export declare const resolvePassword: (props: PostgresRoleProps, env?: Environment) => ResolvedPassword;
24
32
  /** `seal({ password })` of the resolved value — the one thing state remembers about it. */
25
33
  export declare const sealPassword: (value: Redacted.Redacted<string>) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"role-secrets.d.ts","sourceRoot":"","sources":["../../src/postgres/role-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,KAAK,WAAW,EAAiC,MAAM,0BAA0B,CAAC;AAC3F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;2FAE2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACvD;AAED,eAAO,MAAM,eAAe,UACnB,iBAAiB,QACnB,WAAW,KACf,gBAQF,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,YAAY,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAG,MACrB,CAAC;AAE5C;;iFAEiF;AACjF,eAAO,MAAM,mBAAmB,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,MAAM,KAAG,OAC7B,CAAC"}
1
+ {"version":3,"file":"role-secrets.d.ts","sourceRoot":"","sources":["../../src/postgres/role-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,mCAAmC,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,KAAK,WAAW,EAAiC,MAAM,0BAA0B,CAAC;AAC3F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;2FAE2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACvD;AAED;;;;wFAIwF;AACxF,eAAO,MAAM,mBAAmB,SACxB,MAAM,YACF,gBAAgB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,mCAAmC,CAGzC,CAAC;AAElB,eAAO,MAAM,eAAe,UACnB,iBAAiB,QACnB,WAAW,KACf,gBAQF,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,YAAY,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAG,MACrB,CAAC;AAE5C;;iFAEiF;AACjF,eAAO,MAAM,mBAAmB,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,MAAM,KAAG,OAC7B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"role-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAC;AAuBzB,eAAO,MAAM,UAAU,OACjB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,GAAG,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAI7F,CAAC;AAEJ;oGACoG;AACpG,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAW/E,CAAC;AAiBL,eAAO,MAAM,kBAAkB,UAAW,iBAAiB,KAAG,MACM,CAAC;AAErE,4FAA4F;AAC5F,eAAO,MAAM,iBAAiB,SACtB,MAAM,UAER;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KACxC,MAYF,CAAC;AAEF;;;;0BAI0B;AAC1B,eAAO,MAAM,mBAAmB,SAAU,MAAM,YAAY,MAAM,KAAG,MACW,CAAC;AAEjF,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,MAAmD,CAAC;AAEpG;sFACsF;AACtF,eAAO,MAAM,qBAAqB,UACzB,iBAAiB,YACd,MAAM,GAAG,SAAS,KAC3B,SAAS,MAAM,EAMjB,CAAC;AAEF;2FAC2F;AAC3F,eAAO,MAAM,WAAW,UACf,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,aAAa,CAAC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAcvD,CAAC;AAEF;;wDAEwD;AACxD,eAAO,MAAM,kBAAkB,UACtB,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,OAcF,CAAC;AAEF;;;+CAG+C;AAC/C,eAAO,MAAM,gBAAgB,SACrB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAG7C,CAAC;AAEF;2DAC2D;AAC3D,eAAO,MAAM,eAAe,SACpB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,SAAS,MAAM,EAQjB,CAAC"}
1
+ {"version":3,"file":"role-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAC;AAuBzB,eAAO,MAAM,UAAU,OACjB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,GAAG,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAI7F,CAAC;AAEJ;oGACoG;AACpG,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAW/E,CAAC;AAiBL,eAAO,MAAM,kBAAkB,UAAW,iBAAiB,KAAG,MACM,CAAC;AAErE,4FAA4F;AAC5F,eAAO,MAAM,iBAAiB,SACtB,MAAM,UAER;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KACxC,MAYF,CAAC;AAEF;;;;0BAI0B;AAC1B,eAAO,MAAM,mBAAmB,SAAU,MAAM,YAAY,MAAM,KAAG,MACW,CAAC;AAEjF,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,MAAmD,CAAC;AAEpG;sFACsF;AACtF,eAAO,MAAM,qBAAqB,UACzB,iBAAiB,YACd,MAAM,GAAG,SAAS,KAC3B,SAAS,MAAM,EAMjB,CAAC;AAEF;2FAC2F;AAC3F,eAAO,MAAM,WAAW,UACf,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,aAAa,CAAC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAcvD,CAAC;AAEF;;wDAEwD;AACxD,eAAO,MAAM,kBAAkB,UACtB,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,OAgBF,CAAC;AAEF;;;+CAG+C;AAC/C,eAAO,MAAM,gBAAgB,SACrB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAG7C,CAAC;AAEF;2DAC2D;AAC3D,eAAO,MAAM,eAAe,SACpB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,SAAS,MAAM,EAQjB,CAAC"}
@@ -31,7 +31,7 @@ import * as Effect from 'effect/Effect';
31
31
  import type { SqlError } from 'effect/unstable/sql/SqlError';
32
32
  import { type Environment } from '../secrets/write-only.ts';
33
33
  import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
34
- import { PostgresRoleCreateVanished, PostgresRoleIdentityRefused, type PostgresRoleMembershipUnrepaired, PostgresRoleNameRefused, type PostgresRoleParentMissing, PostgresRolePasswordEnvUnsetError, PostgresRolePrivilegedRefused, PostgresRoleValidUntilRefused } from './role-errors.ts';
34
+ import { PostgresRoleCreateVanished, PostgresRoleIdentityRefused, type PostgresRoleMembershipUnrepaired, PostgresRoleNameRefused, type PostgresRoleParentMissing, PostgresRolePasswordEnvUnsetError, type PostgresRolePasswordNonAsciiRefused, PostgresRolePrivilegedRefused, PostgresRoleValidUntilRefused } from './role-errors.ts';
35
35
  import { type PostgresConnection } from './connection.ts';
36
36
  import type { PgExecutor } from './database-sql.ts';
37
37
  export interface PostgresRole extends Resource<'Postgres.Role', PostgresRoleProps, PostgresRoleAttributes, never, PostgresConnection> {
@@ -47,7 +47,7 @@ export declare const isPostgresRole: (value: unknown) => value is PostgresRole;
47
47
  * being written for the first time — because `pg_roles` answers no password (S25), so the live
48
48
  * row cannot say whether the environment value still matches the one last written.
49
49
  */
50
- export declare const reconcileWithClient: (pg: PgExecutor, props: PostgresRoleProps, env?: Environment, previousSeal?: string, storedOid?: number) => Effect.Effect<PostgresRoleAttributes, PostgresRolePasswordEnvUnsetError | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused | SqlError>;
50
+ export declare const reconcileWithClient: (pg: PgExecutor, props: PostgresRoleProps, env?: Environment, previousSeal?: string, storedOid?: number) => Effect.Effect<PostgresRoleAttributes, PostgresRolePasswordEnvUnsetError | PostgresRolePasswordNonAsciiRefused | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused | SqlError>;
51
51
  /** The core of `read`. Answers `undefined` when absent; ownership branding and the state-only
52
52
  * `passwordSeal` merge are the caller's. */
53
53
  export declare const readRole: (pg: PgExecutor, name: string) => Effect.Effect<Omit<PostgresRoleAttributes, "passwordSeal"> | undefined, SqlError, never>;
@@ -1 +1 @@
1
- {"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/postgres/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAcjF,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,KAAK,gCAAgC,EACrC,uBAAuB,EACvB,KAAK,yBAAyB,EAC9B,iCAAiC,EACjC,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,kBAAkB,CACnB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;iEACiE;AACjE,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,KAAK,IAAI,YAGD,CAAC;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,OAC1B,UAAU,SACP,iBAAiB,QACnB,WAAW,qCAEJ,MAAM,KACjB,MAAM,CAAC,MAAM,CACd,sBAAsB,EACpB,iCAAiC,GACjC,6BAA6B,GAC7B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,GAC3B,QAAQ,CAyFR,CAAC;AAEL;4CAC4C;AAC5C,eAAO,MAAM,QAAQ,OAAQ,UAAU,QAAQ,MAAM,6FAAiC,CAAC;AAEvF;gDACgD;AAChD,eAAO,MAAM,YAAY,UAChB,iBAAiB,KACvB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,uBAAuB,GAAG,6BAA6B,CAkB1E,CAAC"}
1
+ {"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/postgres/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAmBjF,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,KAAK,gCAAgC,EACrC,uBAAuB,EACvB,KAAK,yBAAyB,EAC9B,iCAAiC,EACjC,KAAK,mCAAmC,EACxC,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,kBAAkB,CACnB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;iEACiE;AACjE,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,KAAK,IAAI,YAGD,CAAC;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,OAC1B,UAAU,SACP,iBAAiB,QACnB,WAAW,qCAEJ,MAAM,KACjB,MAAM,CAAC,MAAM,CACd,sBAAsB,EACpB,iCAAiC,GACjC,mCAAmC,GACnC,6BAA6B,GAC7B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,GAC3B,QAAQ,CA0FR,CAAC;AAEL;4CAC4C;AAC5C,eAAO,MAAM,QAAQ,OAAQ,UAAU,QAAQ,MAAM,6FAAiC,CAAC;AAEvF;gDACgD;AAChD,eAAO,MAAM,YAAY,UAChB,iBAAiB,KACvB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,uBAAuB,GAAG,6BAA6B,CAkB1E,CAAC"}
@@ -26,16 +26,26 @@ run yourself. The `Postgres.Database` resource in this kit already binds `@effec
26
26
  never re-sends a secret that already matches, and never stores one. A create that declares a
27
27
  password but whose variable is unset refuses before any write
28
28
  (`PostgresRolePasswordEnvUnsetError`), including the retry of a create whose password statement
29
- never sealed.
29
+ never sealed. Non-ASCII passwords fail typed before writes (`PostgresRolePasswordNonAsciiRefused`):
30
+ PostgreSQL applies SASLprep, and this provider does not implement that normalization.
30
31
  - **Membership is `pg_auth_members`, compared as a sorted set.** `memberOf` omitted leaves live
31
32
  memberships alone; `[]` ensures none. Moves run through one-parent `GRANT`/`REVOKE`
32
33
  (`alter_role.sgml`: "there are no options for adding or removing memberships; use GRANT and
33
34
  REVOKE"). A plain `REVOKE` only removes the session grantor's row (`plan_single_revoke` matches
34
35
  `grantor`); each unwanted row is `REVOKE … GRANTED BY` its own grantor. A wanted row that is
35
- `WITH ADMIN` or still `SET TRUE` is repaired in place with one
36
- `GRANT … WITH ADMIN FALSE, SET FALSE` (`AddRoleMems` updates that grantor's tuple). Another
37
- grantor's unsafe row is revoked by name afterwards. A row whose grantor role is gone cannot be
38
- named and fails `PostgresRoleMembershipUnrepaired`. A missing parent fails
36
+ `WITH ADMIN`, still `SET TRUE`, or `INHERIT TRUE` when `inherit:false` is declared is repaired
37
+ with `REVOKE ADMIN/SET/INHERIT OPTION FOR … GRANTED BY … RESTRICT`. Unwanted-row revokes commit
38
+ first, then each retained row's options share a repair transaction. SQLSTATE 2BP01/42501 becomes
39
+ `PostgresRoleMembershipUnrepaired` naming that parent and grantor, preserving refused options
40
+ and dependent rows without rolling back unwanted-row revokes. No CASCADE is issued.
41
+ Option repair was measured once by hand on a throwaway PostgreSQL 17.11 (2026-10-02);
42
+ automated tests stub the 2BP01/42501 responses. Semantics: PostgreSQL
43
+ [16 GRANT](https://www.postgresql.org/docs/16/sql-grant.html) / [REVOKE](https://www.postgresql.org/docs/16/sql-revoke.html),
44
+ [17 GRANT](https://www.postgresql.org/docs/17/sql-grant.html) / [REVOKE](https://www.postgresql.org/docs/17/sql-revoke.html),
45
+ [18 GRANT](https://www.postgresql.org/docs/18/sql-grant.html) / [REVOKE](https://www.postgresql.org/docs/18/sql-revoke.html).
46
+ `buildRepairMembershipSql` now requires the grantor and option (`ADMIN`, `SET` or `INHERIT`).
47
+ A null grantor is a defensive inconsistent-catalog case, not an ordinary DROP ROLE outcome,
48
+ and fails `PostgresRoleMembershipUnrepaired`. A missing parent fails
39
49
  `PostgresRoleParentMissing` before any statement.
40
50
  - **`defaultRemovalPolicy: 'retain'`.** A seat group role may own objects and be granted across
41
51
  databases. A destroy is still implemented in full (`DROP ROLE IF EXISTS`, idempotent), opted in
@@ -47,7 +57,9 @@ run yourself. The `Postgres.Database` resource in this kit already binds `@effec
47
57
  says `WITH SET FALSE`. Create runs `CREATE`, the password `ALTER` and the seat `GRANT`s in one
48
58
  transaction, so a later statement failure does not leave a LOGIN role behind. `diff` reads
49
59
  `pg_auth_members` live — stored attributes omit the option rows, and the engine passes stored
50
- attributes — so `ADMIN` or `SET TRUE` plans as `update`. A live oid that is not the stored oid
60
+ attributes — so unsafe `ADMIN`, `SET` or `INHERIT` plans as `update`. Privileged catalog flags also plan
61
+ `update`, reaching the typed reconcile refusal. `read` omits membership option rows just as
62
+ reconcile does, so an unchanged role does not report structural drift. A live oid that is not the stored oid
51
63
  is `PostgresRoleIdentityRefused` on read, diff, reconcile and delete: the name was recreated
52
64
  out of band and is not dropped or altered.
53
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -208,7 +208,7 @@ describe('K-A4 finding 4: configValueProblem never accepts a flag-shaped value',
208
208
  ['a short flag', '-n'],
209
209
  ['a file-overwrite flag glued to its argument', '-o/etc/ceph/ceph.conf'],
210
210
  ['a long flag with =', '--admin-daemon=/var/run/ceph/ceph-mon.a.asok'],
211
- ['another long flag', '--conf=/tmp/x'],
211
+ ['another long flag', '--conf=/tmp/x'], // tmp-allow: refused argv, not a directory this test creates
212
212
  ['the empty string', ''],
213
213
  ['a bare dash', '-'],
214
214
  ])('refuses %s (%j)', (_name, value) => {
@@ -15,7 +15,7 @@ import {
15
15
  } from './sudo-allowlist.ts';
16
16
 
17
17
  const DAEMONS = '/Library/LaunchDaemons';
18
- const context = { prefixes: [DAEMONS, '/opt/example'], staged: '/tmp/hf-sudo-x/staged' };
18
+ const context = { prefixes: [DAEMONS, '/opt/example'], staged: '/tmp/hf-sudo-x/staged' }; // tmp-allow: in-memory staging path, not a directory this test creates
19
19
  const allowed = (argv: string[]) => privilegedProblem(argv, context);
20
20
  const PLIST = `${DAEMONS}/com.example.a.plist`;
21
21
 
@@ -66,7 +66,7 @@ describe('launchctl shapes', () => {
66
66
  ['bootstrap of two plists', [LAUNCHCTL, 'bootstrap', 'system', PLIST, PLIST]],
67
67
  [
68
68
  'bootstrap outside every prefix',
69
- [LAUNCHCTL, 'bootstrap', 'system', '/tmp/com.example.a.plist'],
69
+ [LAUNCHCTL, 'bootstrap', 'system', '/tmp/com.example.a.plist'], // tmp-allow: refused argv, not a directory this test creates
70
70
  ],
71
71
  ['bootstrap of a non-plist', [LAUNCHCTL, 'bootstrap', 'system', `${DAEMONS}/com.example.a`]],
72
72
  // ★ Only where launchd loads daemons at boot: anywhere else is a job that vanishes on restart.
@@ -22,7 +22,7 @@ import {
22
22
  } from './sudo-allowlist.ts';
23
23
 
24
24
  const PREFIXES = ['/etc/systemd/system', '/usr/local/bin'];
25
- const STAGED = '/tmp/hf-sudo-1/staged';
25
+ const STAGED = '/tmp/hf-sudo-1/staged'; // tmp-allow: in-memory staging path, not a directory this test creates
26
26
  const TEMP = '/etc/systemd/system/.thing.service.hf-abcdef012345.tmp';
27
27
  const context: AllowContext = { prefixes: PREFIXES, staged: STAGED, temp: TEMP };
28
28
  const allowed = (argv: string[]) => privilegedProblem(argv, context);
@@ -51,7 +51,7 @@ describe('install shapes', () => {
51
51
  'a target that is not the derived temp',
52
52
  [INSTALL, '-m', '0644', '-T', '--', STAGED, '/etc/systemd/system/x.service'],
53
53
  ],
54
- ['a temp outside every prefix', [INSTALL, '-m', '0644', '-T', '--', STAGED, '/tmp/x.tmp']],
54
+ ['a temp outside every prefix', [INSTALL, '-m', '0644', '-T', '--', STAGED, '/tmp/x.tmp']], // tmp-allow: refused argv, not a directory this test creates
55
55
  ['no -T', [INSTALL, '-m', '0644', '--', STAGED, TEMP]],
56
56
  ['a three-digit mode', [INSTALL, '-m', '644', '-T', '--', STAGED, TEMP]],
57
57
  ['-S (the Mac flag)', [INSTALL, '-S', '-m', '0644', '-T', '--', STAGED, TEMP]],
@@ -103,7 +103,7 @@ describe('mv and rm shapes', () => {
103
103
  ['mv without -T', [MV, '-f', '--', TEMP, '/etc/systemd/system/thing.service']],
104
104
  ['rm -r', [RM, '-rf', '--', TEMP]],
105
105
  ['rm without --', [RM, '-f', TEMP]],
106
- ['rm outside every prefix', [RM, '-f', '--', '/tmp/x']],
106
+ ['rm outside every prefix', [RM, '-f', '--', '/tmp/x']], // tmp-allow: refused argv, not a directory this test creates
107
107
  ['sudo in argv', ['/usr/bin/sudo', '-n', RM, '-f', '--', TEMP]],
108
108
  ])('refuses %s', (_name, argv) => {
109
109
  expect(allowed(argv)).toBeString();
@@ -52,7 +52,7 @@ describe('writeFileAtomic under a prefix', () => {
52
52
  for (const line of logs) expect(line.startsWith('homeflare/linux sudo -n ')).toBe(true);
53
53
  // No temp file left behind, and the staging directory is gone too.
54
54
  expect([...fake.files.keys()].some((path) => path.includes('.hf-'))).toBe(false);
55
- expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false);
55
+ expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false); // tmp-allow: in-memory fake path from fake-sudo, not a directory this test creates
56
56
  });
57
57
 
58
58
  test('a failed mv removes the temp it just installed, then still throws', async () => {
@@ -68,7 +68,7 @@ describe('writeFileAtomic under a prefix', () => {
68
68
  expect(calls[2]?.at(-1)).toBe(calls[0]?.at(-1));
69
69
  expect(fake.files.has(DEST)).toBe(false);
70
70
  expect([...fake.files.keys()].some((path) => path.includes('.hf-'))).toBe(false);
71
- expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false);
71
+ expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false); // tmp-allow: in-memory fake path from fake-sudo, not a directory this test creates
72
72
  });
73
73
 
74
74
  // 🔴 Adversarial review, round 2: a failed CLEANUP rm after a failed mv used to be swallowed
@@ -22,11 +22,14 @@ export interface FakeRoleState {
22
22
  /** Bare role names, backing the `roleExists` check the database family's owner probe uses. */
23
23
  readonly roleNames: Set<string>;
24
24
  readonly roleRows: Map<string, PostgresRoleAttributes>;
25
- /** `member\0parent\0grantor` triples backing `pg_auth_members` (`''` grantor = dropped
25
+ /** `member\0parent\0grantor` triples backing `pg_auth_members` (`''` grantor = unresolvable
26
26
  * grantor); `GRANT`/`REVOKE` mutate it. */
27
27
  readonly memberships: Set<string>;
28
28
  /** Options a name-only compare would hide, keyed `member\0parent\0grantor`. */
29
- readonly membershipOptions: Map<string, { readonly admin: boolean; readonly set: boolean }>;
29
+ readonly membershipOptions: Map<
30
+ string,
31
+ { readonly admin: boolean; readonly set: boolean; readonly inherit?: boolean }
32
+ >;
30
33
  /** Hands out the next `oid` a real cluster would assign. */
31
34
  nextOid(): number;
32
35
  }
@@ -106,6 +109,7 @@ export const applyRoleStatement = <A extends object>(
106
109
  grantor: parts[2] === undefined || parts[2] === '' ? null : (parts[2] as string),
107
110
  admin: options?.admin === true,
108
111
  set: options?.set === true,
112
+ inherit: options?.inherit === true,
109
113
  };
110
114
  })
111
115
  .sort(
@@ -165,17 +169,33 @@ export const applyRoleStatement = <A extends object>(
165
169
  // `grant.sgml` (REL_18_6): altering an existing membership retains every option the new
166
170
  // GRANT omits, so only the full-options form clears ADMIN; `WITH SET FALSE` alone keeps a
167
171
  // current ADMIN. A bare `GRANT` (never issued by this family) leaves options untouched.
168
- const options = state.membershipOptions.get(key) ?? { admin: false, set: false };
172
+ const options = state.membershipOptions.get(key) ?? {
173
+ admin: false,
174
+ set: false,
175
+ inherit: state.roleRows.get(member)?.inherit ?? true,
176
+ };
169
177
  if (text.endsWith('WITH ADMIN FALSE, SET FALSE')) {
170
- state.membershipOptions.delete(key);
178
+ state.membershipOptions.set(key, { ...options, admin: false, set: false });
171
179
  } else if (text.endsWith('WITH SET FALSE')) {
172
- if (options.admin) state.membershipOptions.set(key, { admin: true, set: false });
173
- else state.membershipOptions.delete(key);
180
+ state.membershipOptions.set(key, { ...options, set: false });
174
181
  }
175
182
  return Effect.succeed([] as unknown as ReadonlyArray<A>);
176
183
  }
177
184
 
178
185
  if (text.startsWith('REVOKE')) {
186
+ const option =
187
+ /^REVOKE (ADMIN|SET|INHERIT) OPTION FOR "((?:[^"]|"")*)" FROM "((?:[^"]|"")*)" GRANTED BY "((?:[^"]|"")*)" RESTRICT$/.exec(
188
+ text,
189
+ );
190
+ if (option !== null) {
191
+ const key = `${unquoteIdent(option[3] as string)}\0${unquoteIdent(option[2] as string)}\0${unquoteIdent(option[4] as string)}`;
192
+ const options = state.membershipOptions.get(key) ?? { admin: false, set: false };
193
+ const next = { ...options, [(option[1] as string).toLowerCase()]: false };
194
+ if (next.admin || next.set || next.inherit !== undefined)
195
+ state.membershipOptions.set(key, next);
196
+ else state.membershipOptions.delete(key);
197
+ return Effect.succeed([] as unknown as ReadonlyArray<A>);
198
+ }
179
199
  const granted =
180
200
  /^REVOKE "((?:[^"]|"")*)" FROM "((?:[^"]|"")*)" GRANTED BY "((?:[^"]|"")*)"$/.exec(text);
181
201
  if (granted !== null) {
@@ -35,12 +35,14 @@ export interface FakeSql extends PgExecutor {
35
35
  readonly roleRows: Map<string, PostgresRoleAttributes>;
36
36
  /** `member\0parent\0grantor` triples backing `pg_auth_members` (a plain `pg_auth_members`
37
37
  * read, a grant, a revoke and a DROP ROLE cascade all go through `fake-role-sql.ts`);
38
- * `GRANT`/`REVOKE` mutate it. An empty grantor segment is a grantor role that no longer
39
- * exists. */
38
+ * `GRANT`/`REVOKE` mutate it. An empty grantor segment is an inconsistent catalog/test row. */
40
39
  readonly memberships: Set<string>;
41
40
  /** Options on a membership the name alone hides, keyed `member\0parent\0grantor`. Absent
42
- * means neither ADMIN nor SET. */
43
- readonly membershipOptions: Map<string, { readonly admin: boolean; readonly set: boolean }>;
41
+ * means neither ADMIN, SET nor an explicit per-grant INHERIT. */
42
+ readonly membershipOptions: Map<
43
+ string,
44
+ { readonly admin: boolean; readonly set: boolean; readonly inherit?: boolean }
45
+ >;
44
46
  /** Mutable on purpose: a test seeds a live schema (adoption, drift) or clears one (drop). */
45
47
  readonly schemas: Map<string, PostgresSchemaAttributes>;
46
48
  /** Mutable on purpose: names of schemas this fake pretends hold at least one relation, so a
@@ -85,7 +87,10 @@ export const makeFakeSql = (options: FakeSqlOptions = {}): FakeSql => {
85
87
  const roleRows = new Map(options.roleRows?.map((r) => [r.name, r] as const) ?? []);
86
88
  const databases = new Map(options.databases?.map((d) => [d.name, d] as const) ?? []);
87
89
  const memberships = new Set<string>();
88
- const membershipOptions = new Map<string, { readonly admin: boolean; readonly set: boolean }>();
90
+ const membershipOptions = new Map<
91
+ string,
92
+ { readonly admin: boolean; readonly set: boolean; readonly inherit?: boolean }
93
+ >();
89
94
  const schemas = seedSchemas(options.schemas, options.database ?? 'postgres');
90
95
  const relationsIn = new Set(options.schemasWithRelations ?? []);
91
96
  let raceRemaining = options.raceNextCreate === true ? 1 : 0;
@@ -147,6 +147,7 @@ export {
147
147
  PostgresRoleNameRefused,
148
148
  PostgresRoleParentMissing,
149
149
  PostgresRolePasswordEnvUnsetError,
150
+ PostgresRolePasswordNonAsciiRefused,
150
151
  PostgresRolePrivilegedRefused,
151
152
  PostgresRoleRenameRefused,
152
153
  PostgresRoleValidUntilRefused,
@@ -62,7 +62,8 @@ export interface PostgresRoleProps {
62
62
  readonly login: boolean;
63
63
  /** `CONNECTION LIMIT n`, `-1` for unlimited — maps to `pg_roles.rolconnlimit`. */
64
64
  readonly connectionLimit: number;
65
- /** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit`. */
65
+ /** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit`; false also clears per-grant
66
+ * INHERIT on declared memberships (PG 16+ uses the role flag only to default new grants). */
66
67
  readonly inherit: boolean;
67
68
  /**
68
69
  * `VALID UNTIL 'timestamp'` — maps to `pg_roles.rolvaliduntil`. ISO 8601 without a timezone is
@@ -112,18 +113,20 @@ export interface PostgresRoleAttributes {
112
113
  */
113
114
  readonly memberOf: readonly string[] | null;
114
115
  /**
115
- * The membership rows behind `memberOf`, including `admin_option` and `set_option`. Present
116
+ * The membership rows behind `memberOf`, including `admin_option`, `set_option` and
117
+ * `inherit_option`. Present
116
118
  * on a catalog read; stripped before attributes are stored (`storedAttributes`), because the
117
119
  * engine's diff receives stored attributes and would never see them there. The provider's
118
120
  * diff reads these rows live. `grantor` is the role that made the grant
119
- * (`pg_auth_members.grantor`), null when that role was dropped — a dropped-grantor row cannot
120
- * be named in `REVOKE … GRANTED BY`, so reconcile can only fail typed on it.
121
+ * (`pg_auth_members.grantor`). Null is a defensive inconsistent-catalog case, not an ordinary
122
+ * DROP ROLE outcome; such a row cannot be named in `REVOKE … GRANTED BY`.
121
123
  */
122
124
  readonly memberships?: readonly {
123
125
  readonly parent: string;
124
126
  readonly grantor: string | null;
125
127
  readonly admin: boolean;
126
128
  readonly set: boolean;
129
+ readonly inherit: boolean;
127
130
  }[];
128
131
  /**
129
132
  * A seal of the last password value written, or `''` if none was ever written (`seal()` from