@happyvertical/smrt-users 0.53.1 → 0.53.2

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 (28) hide show
  1. package/README.md +40 -0
  2. package/agents/permissions.md +7 -0
  3. package/dist/chunks/{TerminalAuthService-DKDfXpDG.js → TerminalAuthService-DG8HwPgq.js} +49 -26
  4. package/dist/chunks/TerminalAuthService-DG8HwPgq.js.map +1 -0
  5. package/dist/collections/RoleCollection.d.ts.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +40 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/manifest.json +26 -18
  11. package/dist/migrations/deduplicateRolePermissions.d.ts +23 -0
  12. package/dist/migrations/deduplicateRolePermissions.d.ts.map +1 -0
  13. package/dist/models/RolePermission.d.ts.map +1 -1
  14. package/dist/smrt-knowledge.json +9 -5
  15. package/dist/sveltekit.js +1 -1
  16. package/package.json +11 -11
  17. package/dist/__tests__/helpers/oidc-test-server.d.ts +0 -45
  18. package/dist/__tests__/helpers/oidc-test-server.d.ts.map +0 -1
  19. package/dist/__tests__/helpers/postgres-rls-reset.d.ts +0 -15
  20. package/dist/__tests__/helpers/postgres-rls-reset.d.ts.map +0 -1
  21. package/dist/chunks/TerminalAuthService-DKDfXpDG.js.map +0 -1
  22. package/dist/svelte/components/__tests__/InviteUserModal.test.js +0 -65
  23. package/dist/svelte/components/__tests__/UserAvatar.test.js +0 -31
  24. package/dist/svelte/components/__tests__/UserCard.test.js +0 -39
  25. package/dist/svelte/components/__tests__/UserForm.test.js +0 -50
  26. package/dist/svelte/components/__tests__/UserList.test.js +0 -48
  27. package/dist/svelte/components/__tests__/UserMenu.test.js +0 -83
  28. package/dist/svelte/components/__tests__/UserStatus.test.js +0 -36
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-users",
5
- "packageVersion": "0.53.1",
5
+ "packageVersion": "0.53.2",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-users:AccessRequestCollection": {
8
8
  "name": "accessrequestcollection",
@@ -2469,23 +2469,24 @@
2469
2469
  },
2470
2470
  "indexes": [
2471
2471
  {
2472
- "name": "role_permissions_slug_context_idx",
2472
+ "name": "role_permissions_role_id_permission_id_idx",
2473
2473
  "columns": [
2474
- "slug",
2475
- "context"
2474
+ "role_id",
2475
+ "permission_id"
2476
2476
  ],
2477
2477
  "unique": true
2478
2478
  },
2479
2479
  {
2480
- "name": "role_permissions_created_at_idx",
2480
+ "name": "role_permissions_slug_context_idx",
2481
2481
  "columns": [
2482
- "created_at"
2482
+ "slug",
2483
+ "context"
2483
2484
  ]
2484
2485
  },
2485
2486
  {
2486
- "name": "role_permissions_role_id_idx",
2487
+ "name": "role_permissions_created_at_idx",
2487
2488
  "columns": [
2488
- "role_id"
2489
+ "created_at"
2489
2490
  ]
2490
2491
  },
2491
2492
  {
@@ -6201,7 +6202,8 @@
6201
6202
  "required": true,
6202
6203
  "related": "Role",
6203
6204
  "_meta": {
6204
- "required": true
6205
+ "required": true,
6206
+ "onDelete": "NO ACTION"
6205
6207
  }
6206
6208
  },
6207
6209
  "permissionId": {
@@ -6209,7 +6211,8 @@
6209
6211
  "required": true,
6210
6212
  "related": "Permission",
6211
6213
  "_meta": {
6212
- "required": true
6214
+ "required": true,
6215
+ "onDelete": "NO ACTION"
6213
6216
  }
6214
6217
  }
6215
6218
  },
@@ -6227,7 +6230,11 @@
6227
6230
  "get"
6228
6231
  ]
6229
6232
  },
6230
- "cli": true
6233
+ "cli": true,
6234
+ "conflictColumns": [
6235
+ "role_id",
6236
+ "permission_id"
6237
+ ]
6231
6238
  },
6232
6239
  "extends": "SmrtObject",
6233
6240
  "exportName": "RolePermission",
@@ -6300,23 +6307,24 @@
6300
6307
  },
6301
6308
  "indexes": [
6302
6309
  {
6303
- "name": "role_permissions_slug_context_idx",
6310
+ "name": "role_permissions_role_id_permission_id_idx",
6304
6311
  "columns": [
6305
- "slug",
6306
- "context"
6312
+ "role_id",
6313
+ "permission_id"
6307
6314
  ],
6308
6315
  "unique": true
6309
6316
  },
6310
6317
  {
6311
- "name": "role_permissions_created_at_idx",
6318
+ "name": "role_permissions_slug_context_idx",
6312
6319
  "columns": [
6313
- "created_at"
6320
+ "slug",
6321
+ "context"
6314
6322
  ]
6315
6323
  },
6316
6324
  {
6317
- "name": "role_permissions_role_id_idx",
6325
+ "name": "role_permissions_created_at_idx",
6318
6326
  "columns": [
6319
- "role_id"
6327
+ "created_at"
6320
6328
  ]
6321
6329
  },
6322
6330
  {
@@ -0,0 +1,23 @@
1
+ import type { DatabaseInterface } from '@happyvertical/sql';
2
+ /** Options for the explicit, maintenance-window grant migration. */
3
+ export interface DeduplicateRolePermissionsOptions {
4
+ /** Inspect the duplicate count without changing rows or indexes. */
5
+ dryRun?: boolean;
6
+ /** Confirm all application writers and seeders are stopped. */
7
+ maintenanceConfirmed?: boolean;
8
+ }
9
+ /** Result of the grant migration; removed is zero for a dry run. */
10
+ export interface DeduplicateRolePermissionsResult {
11
+ duplicates: number;
12
+ removed: number;
13
+ }
14
+ /**
15
+ * Keep the oldest grant for each role/permission pair and install its unique
16
+ * index. Run BEFORE ordinary schema migration, with every writer stopped;
17
+ * old application versions cannot safely seed against the new constraint.
18
+ * Equal timestamps use the lowest id; null timestamps sort last. The surviving
19
+ * row is unchanged. Deletion, index creation and change notification commit
20
+ * together, so failure leaves the old grants intact. Re-running is safe.
21
+ */
22
+ export declare function deduplicateRolePermissions(db: DatabaseInterface, options?: DeduplicateRolePermissionsOptions): Promise<DeduplicateRolePermissionsResult>;
23
+ //# sourceMappingURL=deduplicateRolePermissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deduplicateRolePermissions.d.ts","sourceRoot":"","sources":["../../src/migrations/deduplicateRolePermissions.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,oEAAoE;AACpE,MAAM,WAAW,iCAAiC;IAChD,oEAAoE;IACpE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,oEAAoE;AACpE,MAAM,WAAW,gCAAgC;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AASD;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,gCAAgC,CAAC,CA0C3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"RolePermission.d.ts","sourceRoot":"","sources":["../../src/models/RolePermission.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAQa,cAAe,SAAQ,UAAU;IAC5C;;OAEG;IAEH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IAEH,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEV,OAAO,GAAE,qBAA0B;CAMhD"}
1
+ {"version":3,"file":"RolePermission.d.ts","sourceRoot":"","sources":["../../src/models/RolePermission.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAUa,cAAe,SAAQ,UAAU;IAC5C;;OAEG;IAEH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IAEH,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEV,OAAO,GAAE,qBAA0B;CAMhD"}
@@ -3,14 +3,14 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-users",
6
- "packageVersion": "0.53.1",
6
+ "packageVersion": "0.53.2",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "fc68ca0cea3daeb52ea7c4e0fd986ecb9be9990bf55a8a6172012c05b1ad0bf7",
11
- "packageJson": "630c651ff5e1838c32455ff53bcdd11cf356421b0fe342fcf717b86b449c0929",
10
+ "manifest": "5797e206986b4434600dc15517fbdad2edb04b0aabccc666df3d2c86c49beb16",
11
+ "packageJson": "885471aa0e4cb4c00f2b10dcc39f9474efeffcdc297ddef079442434bfed5923",
12
12
  "agents": "c1bd1dcae5f1662f31d01d0cff19cb3f774715eb71ecfd249eb429e5f64a3eb7",
13
- "moduleDoc:agents/permissions.md": "6797e8c1cac7f1af34611b7b0a7a53e61806cab250ea1afb1a820356b7d2b233",
13
+ "moduleDoc:agents/permissions.md": "5f6c0afab7e1fd0887b0d29b5a5b5ab2635694207bade70145d3cbfdb62ba78c",
14
14
  "moduleDoc:agents/oidc-provisioning.md": "5efa607ff23b9a693391e0c02670324a8cea4114d4968c9718f10a763da919c6",
15
15
  "moduleDoc:agents/mobile-auth.md": "25e85e83f43e3a46383a92fa809f3a003b4545c3026e78dbe1bccbbc35161e18",
16
16
  "moduleDoc:agents/terminal-auth-testing.md": "8a84c16a19e381427aed81d3b1882344c427fe22a70e87f5ac2db836b1a6822d",
@@ -5665,6 +5665,10 @@
5665
5665
  }
5666
5666
  ],
5667
5667
  "methods": [],
5668
+ "conflictColumns": [
5669
+ "role_id",
5670
+ "permission_id"
5671
+ ],
5668
5672
  "surfaces": [
5669
5673
  {
5670
5674
  "kind": "api",
@@ -10591,7 +10595,7 @@
10591
10595
  {
10592
10596
  "path": "agents/permissions.md",
10593
10597
  "module": "permissions",
10594
- "content": "# Permissions and tenant inheritance\n\nRead for `src/services/PermissionResolver.ts`, operation guards, role seeding,\nRLS, and tenant-hierarchy changes. Integration examples live in README's\nmanifest-derived permission catalog and PostgreSQL RLS sections.\n\n## Membership selection\n\n1. A direct target-tenant membership pins resolution: active resolves normally;\n pending/suspended returns empty. Never union direct and ancestor roles.\n2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor\n membership whose role has inheritsToDescendants. Skip inactive/unflagged\n ancestors; they neither grant nor block. No qualifying ancestor means empty.\n3. Run later layers against the target tenant. Only target-tenant groups apply;\n membership overrides travel with the chosen ancestor membership.\n inheritedFromTenantId reports that ancestor, null for direct resolution.\n\nVerify hierarchyPath link-by-link against loaded parentTenantId rows before\ntrusting it: excess depth, self-reference, duplicates or inconsistent paths\nfail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not\nconsulted, matching direct resolution. Long-lived user/tenant caches must\ninvalidate ancestor membership changes and inheritsToDescendants flips;\nrequest-scoped caches do not survive those requests.\n\n`loadSessionContext().tenantAuthorization` is authoritative for required-tenant\nconsumers; membership null can mean inherited authority.\n\n4. If no membership resolved at all, apply the opt-in ancestor-read policy\n (smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the\n target, whose role slug is declared in `permissions.ancestorRead.roles` and\n within `maxDepth` hops, contribute `<collection>.read` for declared\n collections, intersected with BOTH that role's own catalog grants (so a\n descendant-side tenant GRANT, group role, or membership GRANT cannot widen\n it) AND the principal's effective permissions in that descendant (so its\n DENY cascade and membership DENY apply). Only a SYSTEM\n role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are\n not unique across a hierarchy, so a tenant-scoped custom role of the same\n name must not opt its tenant in. Off by default;\n read-only; never lateral; never reached when a direct or inherited\n membership already decided. The target tenant's DENY still subtracts.\n `ancestorReadFromTenantIds` reports the contributing descendants. The grant\n carries no membership, so `membershipId`/`tenantAuthorization.membershipId`\n stay null and a consumer requiring a non-empty membership still refuses.\n\n## Permission precedence\n\nApply these layers in order, later layers overriding earlier ones:\n\n1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).\n2. Selected membership role permissions.\n3. Target-tenant group-role permissions.\n4. Tenant cascade's net DENY removes role/group grants.\n5. Membership GRANT may re-add a tenant-denied slug.\n6. Membership DENY removes last and always wins.\n\nThe tenant block is the cascade's net result, not the union of historical DENYs:\na child's more-specific GRANT can override a parent DENY. Use a direct child\nmembership or tenant DENY to attenuate inherited role authority.\n`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.\n\n## Hierarchies and seeding\n\n- `Tenant.save()` derives hierarchyPath/hierarchyLevel from the REAL\n parentTenantId chain on every save (never from an ancestor's stored path),\n refuses missing parents, cycles and over-depth before writing, and\n re-materializes descendants by raw column update (so STI subclass columns\n are never clobbered). createChild/moveToParent/makeRoot are thin wrappers.\n `materializeTenantHierarchy(db, { dryRun })` / `smrt\n db:materialize-tenant-hierarchy` backfills legacy rows: idempotent, one\n transaction, refuses a broken chain without writing (#3036). A NEW or\n CHANGED parent link is authority-bearing, so `Tenant.save()` first loads the\n parent through TenantCollection (the caller's tenancy scope) and refuses an\n invisible one on every path — helpers, `create({ parentTenantId })`, or a\n plain assignment + save; system context bypasses as usual, and an unchanged\n link is never re-checked. getTree(rootId?)\n returns UI structure. Maximum depth is 10.\n- Tenant override cascade requires parent cascadePermissions and child\n inheritPermissions. These flags do not gate the independent, per-role\n inheritsToDescendants membership flow.\n- `permissions.ancestorRead` in the `users` package config declares upward,\n read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are\n required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is\n no partially valid declaration. `isAncestorReadableSlug()` is the single place\n the read-only bound is enforced; only a two-segment `<collection>.<action>`\n slug whose action normalizes to `read` on a declared collection passes.\n `PermissionResolver.create(options, { ancestorReadPolicy })` binds one\n resolver; `null` forces it off. It grants the OPERATION at the ancestor, never\n row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.\n- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed\n slugs additively, never unflags omitted ones, and rejects unknown slugs.\n Default seeds are exact-tenant.\n- RolePermissionCollection.seedRolePermissions() or\n seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog\n permissions, member to ordinary-resource read/create, viewer to read. Member\n create excludes identity/RBAC/security resources and their joins/overrides.\n Seeding is additive/idempotent; removal requires prune: true. Catalog sync\n and grants plan from one read, then commit in `SEEDING_BATCH_SIZE` batches\n (#3323): never one durable commit per row (an fsync each on local SQLite),\n and never one catalog-wide transaction (`@happyvertical/sql` transaction\n scopes cost superlinearly per statement). Pass a caller transaction's handle.\n- When a package adds built-in self-personalization permissions after role\n creation, explicitly call seedDefaultRolePersonalizationPermissions(). It\n upgrades owner/admin/member/viewer idempotently and never grants custom roles.\n\n## Guards and RLS\n\nPermissionCatalogService derives collection.action slugs from manifests,\nincluding custom actions; list/get map to read. Guards derive a class's slug\nthrough the same resolution (explicit `collection`, manifest entry, then its\nSTI base's), so an STI subtype resolves its STI base's collection\n(`Network extends Tenant` -> `tenants.update`) in every build, including a\nbundle decorated before its manifest loads (#3125). Hand-written mutations in form\nhandlers, endpoints, CLI and jobs use assertOperationPermission(). It requires\ncatalog presence then resolves permissions, throwing fail-closed by default.\nUse onDeny: 'return', checkOperationPermission or hasOperationPermission only\nwhen the caller handles a structured/boolean denial.\n\nGenerated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating\ncustom actions) enforce the same `collection.action` slug (custom actions use\n`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's\nsession permission snapshot (`locals.permissions`/`permissionSet`/\n`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only\n`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.\n\nResource-anchored guards receive the resource tenant ID. When it differs from\nthe session tenant, omit the session membership so the resolver selects the\nappropriate authority; mismatched supplied membership/tenant fails closed.\nInherited root-admin authority can then authorize descendant resources without\napplication-side membership fan-out.\n\nResolution cost must not scale with catalog size in objects (#3047): role →\npermission ids and ids → slugs are `list({ select })` projection reads (the\ncollections' beforeList interceptors still run; no RolePermission/Permission\nis hydrated), so an owner mapped to the whole catalog costs two queries and\nno per-permission objects. Keep new resolver reads projections when only ids\nor slugs are needed, and do not add public collection methods for them — each\nbecomes a custom-action slug in the manifest-derived catalog. The resolver's\ncollections never inherit `defaultListLimit`/`maxListLimit` from the caller's\noptions, and id-list reads chunk at `IN_LIST_CHUNK_SIZE` with an explicit\nlimit: a truncated authorization read would narrow the result silently.\n`issue-3047-permission-resolution-cost-postgres.test.ts` pins the hydration\nbudget and a golden equivalence matrix recorded on the hydrating resolver.\n\nThe same rule holds for every framework-internal authorization read (#3048):\ncollections that back a decision — the resolver's, the ResourceGrant exact-tuple\nread in `checkResourceOperationPermission()` (DENY wins only if every row is\nread), and TenantService's owned-tenant quota count — are created through\n`withoutListBounds()` (`src/services/authorization-read-options.ts`), which\nstrips only the caller's list bounds. Use it for any new decision read;\n`issue-3048-authorization-list-bounds-postgres.test.ts` pins it.\n\nPermissionResolver runs every resolution inside smrt-tenancy's system context\n(#3036). Its reads are deliberately cross-tenant and framework-owned — the\nancestor TenantPermissionOverride batch, ancestor memberships for\ninheritsToDescendants, descendant memberships for ancestor-read — and are keyed\nby the explicit (userId, tenantId); with those classes registered tenant-scoped\n(`autoFilter`) the consumer's filter otherwise threw TenantIsolationError or\nsilently narrowed them. It returns slugs and ids only, never rows, and the\ncaller's own reads stay filtered. Do not move a resolver read back under the\nambient tenant; the Postgres regression suite\n`issue-3036-hierarchy-resolution-postgres.test.ts` pins this. Because nothing\nbackstops those reads any more, the tenant-override cascade verifies its chain\ntoo: it uses hierarchyPath only when it agrees link-by-link with\nparentTenantId, otherwise walks the real parent links (so a forged path cannot\ncascade an unrelated tenant in, and a never-materialized row still gets its real\nancestors' DENYs — and GRANTs), and throws TenantHierarchyError on a broken\nreal chain: a per-request failure for that tenant's whole subtree, which\n`smrt db:materialize-tenant-hierarchy --dry-run` reports ahead of time (see\nREADME \"Upgrading an existing fleet\").\n`getTenantInheritanceChain()` returns Tenant rows, so it is NOT run in system\ncontext; it reports the same verified chain the cascade applies when the\ncaller can read every ancestor. Otherwise it never reports PARENT_NOT_FOUND\nfor a row the caller simply cannot see: an interceptor refusal surfaces as\nTenantIsolationError, and a silently filtered row (e.g. RLS) ends the chain at\nthe nearest readable ancestor. Descendant path rewrites are raw column updates recorded through\n`bumpChangeFeed`; `updated_at` is intentionally not bumped (derived columns;\nevery save recomputes them). They are not atomic with the moving row's save —\nan interrupted subtree fails closed until re-saved or backfilled.\n\nSystem context and super-admin bypass are honored. Pass\nallowSuperAdminBypass: false for money or separation-of-duties operations that\nrequire an explicit grant.\n\ngeneratePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated\nRLS using smrt.permissions and smrt.tenant_id, installed with set_config by\nwithSessionPermissionContext. This bounds REST, MCP and in-process database\naccess once principal context is set. Child-tenant sessions receive resolved\ninherited permissions automatically, but RLS row filtering remains bound to the\nsession tenant: a root session's app-level guard authorization does not itself\npermit child rows through RLS. Preserve this distinction.\n"
10598
+ "content": "# Permissions and tenant inheritance\n\nRead for `src/services/PermissionResolver.ts`, operation guards, role seeding,\nRLS, and tenant-hierarchy changes. Integration examples live in README's\nmanifest-derived permission catalog and PostgreSQL RLS sections.\n\n## Membership selection\n\n1. A direct target-tenant membership pins resolution: active resolves normally;\n pending/suspended returns empty. Never union direct and ancestor roles.\n2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor\n membership whose role has inheritsToDescendants. Skip inactive/unflagged\n ancestors; they neither grant nor block. No qualifying ancestor means empty.\n3. Run later layers against the target tenant. Only target-tenant groups apply;\n membership overrides travel with the chosen ancestor membership.\n inheritedFromTenantId reports that ancestor, null for direct resolution.\n\nVerify hierarchyPath link-by-link against loaded parentTenantId rows before\ntrusting it: excess depth, self-reference, duplicates or inconsistent paths\nfail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not\nconsulted, matching direct resolution. Long-lived user/tenant caches must\ninvalidate ancestor membership changes and inheritsToDescendants flips;\nrequest-scoped caches do not survive those requests.\n\n`loadSessionContext().tenantAuthorization` is authoritative for required-tenant\nconsumers; membership null can mean inherited authority.\n\n4. If no membership resolved at all, apply the opt-in ancestor-read policy\n (smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the\n target, whose role slug is declared in `permissions.ancestorRead.roles` and\n within `maxDepth` hops, contribute `<collection>.read` for declared\n collections, intersected with BOTH that role's own catalog grants (so a\n descendant-side tenant GRANT, group role, or membership GRANT cannot widen\n it) AND the principal's effective permissions in that descendant (so its\n DENY cascade and membership DENY apply). Only a SYSTEM\n role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are\n not unique across a hierarchy, so a tenant-scoped custom role of the same\n name must not opt its tenant in. Off by default;\n read-only; never lateral; never reached when a direct or inherited\n membership already decided. The target tenant's DENY still subtracts.\n `ancestorReadFromTenantIds` reports the contributing descendants. The grant\n carries no membership, so `membershipId`/`tenantAuthorization.membershipId`\n stay null and a consumer requiring a non-empty membership still refuses.\n\n## Permission precedence\n\nApply these layers in order, later layers overriding earlier ones:\n\n1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).\n2. Selected membership role permissions.\n3. Target-tenant group-role permissions.\n4. Tenant cascade's net DENY removes role/group grants.\n5. Membership GRANT may re-add a tenant-denied slug.\n6. Membership DENY removes last and always wins.\n\nThe tenant block is the cascade's net result, not the union of historical DENYs:\na child's more-specific GRANT can override a parent DENY. Use a direct child\nmembership or tenant DENY to attenuate inherited role authority.\n`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.\n\n## Hierarchies and seeding\n\n- `Tenant.save()` derives hierarchyPath/hierarchyLevel from the REAL\n parentTenantId chain on every save (never from an ancestor's stored path),\n refuses missing parents, cycles and over-depth before writing, and\n re-materializes descendants by raw column update (so STI subclass columns\n are never clobbered). createChild/moveToParent/makeRoot are thin wrappers.\n `materializeTenantHierarchy(db, { dryRun })` / `smrt\n db:materialize-tenant-hierarchy` backfills legacy rows: idempotent, one\n transaction, refuses a broken chain without writing (#3036). A NEW or\n CHANGED parent link is authority-bearing, so `Tenant.save()` first loads the\n parent through TenantCollection (the caller's tenancy scope) and refuses an\n invisible one on every path — helpers, `create({ parentTenantId })`, or a\n plain assignment + save; system context bypasses as usual, and an unchanged\n link is never re-checked. getTree(rootId?)\n returns UI structure. Maximum depth is 10.\n- Tenant override cascade requires parent cascadePermissions and child\n inheritPermissions. These flags do not gate the independent, per-role\n inheritsToDescendants membership flow.\n- `permissions.ancestorRead` in the `users` package config declares upward,\n read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are\n required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is\n no partially valid declaration. `isAncestorReadableSlug()` is the single place\n the read-only bound is enforced; only a two-segment `<collection>.<action>`\n slug whose action normalizes to `read` on a declared collection passes.\n `PermissionResolver.create(options, { ancestorReadPolicy })` binds one\n resolver; `null` forces it off. It grants the OPERATION at the ancestor, never\n row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.\n- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed\n slugs additively, never unflags omitted ones, and rejects unknown slugs.\n Default seeds are exact-tenant.\n- RolePermissionCollection.seedRolePermissions() or\n seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog\n permissions, member to ordinary-resource read/create, viewer to read. Member\n create excludes identity/RBAC/security resources and their joins/overrides.\n Seeding is additive/idempotent; removal requires prune: true. Catalog sync\n and grants plan from one read, then commit in `SEEDING_BATCH_SIZE` batches\n (#3323): never one durable commit per row (an fsync each on local SQLite),\n and never one catalog-wide transaction (`@happyvertical/sql` transaction\n scopes cost superlinearly per statement). Pass a caller transaction's handle.\n- When a package adds built-in self-personalization permissions after role\n creation, explicitly call seedDefaultRolePersonalizationPermissions(). It\n upgrades owner/admin/member/viewer idempotently and never grants custom roles.\n\n## Guards and RLS\n\nPermissionCatalogService derives collection.action slugs from manifests,\nincluding custom actions; list/get map to read. Guards derive a class's slug\nthrough the same resolution (explicit `collection`, manifest entry, then its\nSTI base's), so an STI subtype resolves its STI base's collection\n(`Network extends Tenant` -> `tenants.update`) in every build, including a\nbundle decorated before its manifest loads (#3125). Hand-written mutations in form\nhandlers, endpoints, CLI and jobs use assertOperationPermission(). It requires\ncatalog presence then resolves permissions, throwing fail-closed by default.\nUse onDeny: 'return', checkOperationPermission or hasOperationPermission only\nwhen the caller handles a structured/boolean denial.\n\nGenerated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating\ncustom actions) enforce the same `collection.action` slug (custom actions use\n`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's\nsession permission snapshot (`locals.permissions`/`permissionSet`/\n`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only\n`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.\n\nResource-anchored guards receive the resource tenant ID. When it differs from\nthe session tenant, omit the session membership so the resolver selects the\nappropriate authority; mismatched supplied membership/tenant fails closed.\nInherited root-admin authority can then authorize descendant resources without\napplication-side membership fan-out.\n\nResolution cost must not scale with catalog size in objects (#3047): role →\npermission ids and ids → slugs are `list({ select })` projection reads (the\ncollections' beforeList interceptors still run; no RolePermission/Permission\nis hydrated), so an owner mapped to the whole catalog costs two queries and\nno per-permission objects. Keep new resolver reads projections when only ids\nor slugs are needed, and do not add public collection methods for them — each\nbecomes a custom-action slug in the manifest-derived catalog. The resolver's\ncollections never inherit `defaultListLimit`/`maxListLimit` from the caller's\noptions, and id-list reads chunk at `IN_LIST_CHUNK_SIZE` with an explicit\nlimit: a truncated authorization read would narrow the result silently.\n`issue-3047-permission-resolution-cost-postgres.test.ts` pins the hydration\nbudget and a golden equivalence matrix recorded on the hydrating resolver.\n\nThe same rule holds for every framework-internal authorization read (#3048):\ncollections that back a decision — the resolver's, the ResourceGrant exact-tuple\nread in `checkResourceOperationPermission()` (DENY wins only if every row is\nread), and TenantService's owned-tenant quota count — are created through\n`withoutListBounds()` (`src/services/authorization-read-options.ts`), which\nstrips only the caller's list bounds. Use it for any new decision read;\n`issue-3048-authorization-list-bounds-postgres.test.ts` pins it.\n\nPermissionResolver runs every resolution inside smrt-tenancy's system context\n(#3036). Its reads are deliberately cross-tenant and framework-owned — the\nancestor TenantPermissionOverride batch, ancestor memberships for\ninheritsToDescendants, descendant memberships for ancestor-read — and are keyed\nby the explicit (userId, tenantId); with those classes registered tenant-scoped\n(`autoFilter`) the consumer's filter otherwise threw TenantIsolationError or\nsilently narrowed them. It returns slugs and ids only, never rows, and the\ncaller's own reads stay filtered. Do not move a resolver read back under the\nambient tenant; the Postgres regression suite\n`issue-3036-hierarchy-resolution-postgres.test.ts` pins this. Because nothing\nbackstops those reads any more, the tenant-override cascade verifies its chain\ntoo: it uses hierarchyPath only when it agrees link-by-link with\nparentTenantId, otherwise walks the real parent links (so a forged path cannot\ncascade an unrelated tenant in, and a never-materialized row still gets its real\nancestors' DENYs — and GRANTs), and throws TenantHierarchyError on a broken\nreal chain: a per-request failure for that tenant's whole subtree, which\n`smrt db:materialize-tenant-hierarchy --dry-run` reports ahead of time (see\nREADME \"Upgrading an existing fleet\").\n`getTenantInheritanceChain()` returns Tenant rows, so it is NOT run in system\ncontext; it reports the same verified chain the cascade applies when the\ncaller can read every ancestor. Otherwise it never reports PARENT_NOT_FOUND\nfor a row the caller simply cannot see: an interceptor refusal surfaces as\nTenantIsolationError, and a silently filtered row (e.g. RLS) ends the chain at\nthe nearest readable ancestor. Descendant path rewrites are raw column updates recorded through\n`bumpChangeFeed`; `updated_at` is intentionally not bumped (derived columns;\nevery save recomputes them). They are not atomic with the moving row's save —\nan interrupted subtree fails closed until re-saved or backfilled.\n\nSystem context and super-admin bypass are honored. Pass\nallowSuperAdminBypass: false for money or separation-of-duties operations that\nrequire an explicit grant.\n\ngeneratePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated\nRLS using smrt.permissions and smrt.tenant_id, installed with set_config by\nwithSessionPermissionContext. This bounds REST, MCP and in-process database\naccess once principal context is set. Child-tenant sessions receive resolved\ninherited permissions automatically, but RLS row filtering remains bound to the\nsession tenant: a root session's app-level guard authorization does not itself\npermit child rows through RLS. Preserve this distinction.\n\nRolePermission's natural key is `(role_id, permission_id)` (#3329); repeated or\nconcurrent inserts converge without changing the grant ID. Existing deployments\nmust run `deduplicateRolePermissions()` with stopped writers before ordinary\nschema migration; see README's \"Upgrade duplicate role grants\". It keeps the\noldest row and installs uniqueness atomically. Runtime seeding never performs\nthis schema repair.\n"
10595
10599
  },
10596
10600
  {
10597
10601
  "path": "agents/oidc-provisioning.md",
package/dist/sveltekit.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as OidcLoginService, D as resolveOidcProviderConfig, E as getUsersOidcConfig, S as OidcLoginError, T as encodeOidcTransaction, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as decodeOidcTransaction, y as SessionService } from "./chunks/TerminalAuthService-DKDfXpDG.js";
1
+ import { C as OidcLoginService, D as resolveOidcProviderConfig, E as getUsersOidcConfig, S as OidcLoginError, T as encodeOidcTransaction, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as decodeOidcTransaction, y as SessionService } from "./chunks/TerminalAuthService-DG8HwPgq.js";
2
2
  import { DiscoveryArtifactValidationError, SMRT_APP_RESULT_CONTRACT, SMRT_APP_RESULT_SCHEMA, SMRT_APP_RESULT_VERSION, SMRT_DISCOVERY_CONFORMANCE_ARTIFACT_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_VERSION, SMRT_MCP_RESULT_METADATA_KEY, canonicalizeDiscoveryArtifact, createDiscoveryConformanceArtifact, deriveCommandRequirements, validateDiscoveryConformanceArtifact } from "./app-contract.js";
3
3
  import { ObjectRegistry, createClassNamePredicate, resolveCustomActionMetadata, resolveEffectiveActionMetadata } from "@happyvertical/smrt-core";
4
4
  import { createLogger } from "@happyvertical/logger";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-users",
3
- "version": "0.53.1",
3
+ "version": "0.53.2",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Multi-tenant user management for the SMRT framework - users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -48,18 +48,18 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@happyvertical/logger": "^0.98.0",
51
- "@happyvertical/smrt-config": "0.53.1",
52
- "@happyvertical/smrt-core": "0.53.1",
53
- "@happyvertical/smrt-mobile-contract": "0.53.1",
54
- "@happyvertical/smrt-profiles": "0.53.1",
55
- "@happyvertical/smrt-tenancy": "0.53.1",
56
- "@happyvertical/smrt-types": "0.53.1",
57
- "@happyvertical/smrt-ui": "0.53.1",
51
+ "@happyvertical/smrt-config": "0.53.2",
52
+ "@happyvertical/smrt-core": "0.53.2",
53
+ "@happyvertical/smrt-mobile-contract": "0.53.2",
54
+ "@happyvertical/smrt-profiles": "0.53.2",
55
+ "@happyvertical/smrt-tenancy": "0.53.2",
56
+ "@happyvertical/smrt-types": "0.53.2",
57
+ "@happyvertical/smrt-ui": "0.53.2",
58
58
  "@happyvertical/sql": "^0.98.0",
59
59
  "jose": "^6.2.12"
60
60
  },
61
61
  "devDependencies": {
62
- "@happyvertical/smrt-vitest": "0.53.1",
62
+ "@happyvertical/smrt-vitest": "0.53.2",
63
63
  "@sveltejs/package": "^2.5.8",
64
64
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
65
65
  "@types/node": "24.13.6",
@@ -92,12 +92,12 @@
92
92
  "access": "public"
93
93
  },
94
94
  "scripts": {
95
- "build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json",
95
+ "build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json && node ../../scripts/prune-svelte-package-artifacts.js dist",
96
96
  "build:watch": "vite build --mode library --watch",
97
97
  "clean": "rm -rf dist",
98
98
  "dev": "npm run build:watch",
99
99
  "test": "vitest run",
100
- "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/permission-postgres-rls.test.ts src/__tests__/principal-permission-context-postgres.test.ts src/__tests__/oidc-provisioning-postgres.test.ts src/__tests__/legacy-user-profile-backfill-postgres.test.ts src/__tests__/terminal-auth-postgres.test.ts src/__tests__/resource-grant-postgres.test.ts src/__tests__/issue-3036-hierarchy-resolution-postgres.test.ts src/__tests__/issue-3047-permission-resolution-cost-postgres.test.ts src/__tests__/issue-3048-authorization-list-bounds-postgres.test.ts",
100
+ "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/permission-postgres-rls.test.ts src/__tests__/principal-permission-context-postgres.test.ts src/__tests__/oidc-provisioning-postgres.test.ts src/__tests__/legacy-user-profile-backfill-postgres.test.ts src/__tests__/terminal-auth-postgres.test.ts src/__tests__/resource-grant-postgres.test.ts src/__tests__/issue-3036-hierarchy-resolution-postgres.test.ts src/__tests__/issue-3047-permission-resolution-cost-postgres.test.ts src/__tests__/issue-3048-authorization-list-bounds-postgres.test.ts src/__tests__/issue-3329-concurrent-seeding-postgres.test.ts src/__tests__/issue-3329-grant-dedup.test.ts",
101
101
  "typecheck": "tsc --noEmit && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",
102
102
  "check": "pnpm exec svelte-check --tsconfig ./tsconfig.svelte.json",
103
103
  "verify:pack": "node ../../scripts/verify-package-types-exports.js ."
@@ -1,45 +0,0 @@
1
- /**
2
- * Shared OIDC test-IdP harness: a real HTTP server implementing discovery,
3
- * JWKS, token, and userinfo endpoints with jose-signed ID tokens. Extracted
4
- * from oidc-login-service.test.ts so the mobile handler suite (issue #1748)
5
- * exercises the same provider-generic PKCE surface instead of inventing a
6
- * second mock.
7
- */
8
- import { type DatabaseInterface } from '@happyvertical/smrt-core/migrations';
9
- export interface OidcTestServerOptions {
10
- authorizationResponseIssuerSupported?: boolean;
11
- audience?: string | string[];
12
- authorizedParty?: string;
13
- idTokenEmailVerified?: boolean;
14
- includeEmailInIdToken?: boolean;
15
- userInfoEmailVerified?: boolean;
16
- }
17
- export interface OidcTestServer {
18
- close: () => Promise<void>;
19
- issuer: string;
20
- /** Nonce stamped into subsequently issued ID tokens. */
21
- setNonce: (value: string) => void;
22
- tokenRequests: Array<{
23
- authorization?: string;
24
- params: URLSearchParams;
25
- }>;
26
- userinfoRequests: Array<{
27
- authorization?: string;
28
- }>;
29
- }
30
- /**
31
- * Schema for the tables OIDC user provisioning touches
32
- * (`UserCollection.getOrCreateFromOidc`): the smrt-profiles trio plus users
33
- * and sessions. Extend with further DDL per suite as needed.
34
- *
35
- * Tenant-scoped tables (`profile_types`, `profiles`) key on
36
- * `(tenant_id, slug, context, _meta_type)` since smrt#2360 — keep this DDL in
37
- * step with the framework schema or `save()`'s ON CONFLICT cannot bind.
38
- */
39
- export declare const OIDC_USERS_TEST_SCHEMA = "\nCREATE TABLE IF NOT EXISTS \"_smrt_backfills\" (\n \"name\" TEXT PRIMARY KEY NOT NULL,\n \"applied_at\" TIMESTAMP DEFAULT current_timestamp,\n \"description\" TEXT,\n \"package_name\" TEXT\n);\n\nCREATE TABLE IF NOT EXISTS \"profile_types\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"_meta_type\" TEXT NOT NULL,\n \"_meta_data\" JSON,\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"profile_types_slug_context_meta_type_idx\" ON \"profile_types\" (\"tenant_id\", \"slug\", \"context\", \"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profile_types_meta_type_idx\" ON \"profile_types\" (\"_meta_type\");\n\nCREATE TABLE IF NOT EXISTS \"profiles\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"_meta_type\" TEXT NOT NULL,\n \"_meta_data\" JSON,\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT,\n \"type_id\" TEXT,\n \"email\" TEXT,\n \"email_key\" TEXT,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT,\n \"is_public\" BOOLEAN DEFAULT FALSE\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"profiles_slug_context_meta_type_idx\" ON \"profiles\" (\"tenant_id\", \"slug\", \"context\", \"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profiles_meta_type_idx\" ON \"profiles\" (\"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profiles_email_key_idx\" ON \"profiles\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"oidc_profile_email_reservations\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT NOT NULL,\n \"email_key\" TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_slug_context_idx\" ON \"oidc_profile_email_reservations\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_profile_id_idx\" ON \"oidc_profile_email_reservations\" (\"profile_id\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_email_key_idx\" ON \"oidc_profile_email_reservations\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"oidc_identities\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT NOT NULL,\n \"provider\" TEXT DEFAULT '',\n \"issuer\" TEXT DEFAULT '',\n \"subject\" TEXT DEFAULT '',\n \"identity_key\" TEXT,\n \"email\" TEXT DEFAULT '',\n \"last_used_at\" TIMESTAMP\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_identities_slug_context_idx\" ON \"oidc_identities\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_identities_identity_key_idx\" ON \"oidc_identities\" (\"identity_key\");\n\nCREATE TABLE IF NOT EXISTS \"users\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT DEFAULT '',\n \"email\" TEXT DEFAULT '',\n \"email_key\" TEXT,\n \"status\" TEXT,\n \"last_login_at\" TIMESTAMP\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_slug_context_idx\" ON \"users\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_profile_id_idx\" ON \"users\" (\"profile_id\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_email_key_idx\" ON \"users\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"sessions\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"user_id\" TEXT DEFAULT '',\n \"tenant_id\" TEXT,\n \"status\" TEXT,\n \"expires_at\" TIMESTAMP,\n \"user_agent\" TEXT DEFAULT '',\n \"ip_address\" TEXT DEFAULT '',\n \"last_accessed_at\" TIMESTAMP,\n \"data\" JSON DEFAULT '{}'\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"sessions_slug_context_idx\" ON \"sessions\" (\"slug\", \"context\");\n";
40
- /** Mark a freshly-created, empty test schema ready for indexed OIDC reads. */
41
- export declare function prepareOidcEmailKeyBackfills(db: DatabaseInterface): Promise<void>;
42
- /** Close every server started by this harness (call from `afterAll`). */
43
- export declare function closeAllOidcServers(): Promise<void>;
44
- export declare function startOidcServer(options?: OidcTestServerOptions): Promise<OidcTestServer>;
45
- //# sourceMappingURL=oidc-test-server.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"oidc-test-server.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/oidc-test-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,qCAAqC,CAAC;AAK7C,MAAM,WAAW,qBAAqB;IACpC,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,aAAa,EAAE,KAAK,CAAC;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;IAC1E,gBAAgB,EAAE,KAAK,CAAC;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,g/IAyGlC,CAAC;AAEF,8EAA8E;AAC9E,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,iBAAiB,GACpB,OAAO,CAAC,IAAI,CAAC,CAUf;AAaD,yEAAyE;AACzE,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEzD;AAiBD,wBAAsB,eAAe,CACnC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CAgIzB"}
@@ -1,15 +0,0 @@
1
- import type { DatabaseInterface } from '@happyvertical/sql';
2
- /**
3
- * Remove every row-level-security policy `applyPostgresPermissionPolicies()`
4
- * installed in the current schema, and turn RLS back off on those tables.
5
- *
6
- * The policies are DDL: they commit on the shared per-package database and
7
- * outlive the isolated transaction a suite rolls back. Left behind, they
8
- * FORCE RLS onto every tenant-scoped table for each later suite in the
9
- * package. Under a superuser connection that was invisible (superusers bypass
10
- * RLS); under the CI lane's NOSUPERUSER/NOBYPASSRLS role it rejects ordinary
11
- * writes in unrelated suites, e.g. `resource_grants` in
12
- * `resource-grant-postgres.test.ts`.
13
- */
14
- export declare function resetPostgresPermissionPolicies(db: DatabaseInterface): Promise<void>;
15
- //# sourceMappingURL=postgres-rls-reset.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"postgres-rls-reset.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/postgres-rls-reset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,iBAAiB,GACpB,OAAO,CAAC,IAAI,CAAC,CA2Bf"}