@happyvertical/smrt-users 0.30.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 (150) hide show
  1. package/AGENTS.md +85 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +459 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/chunks/TerminalAuthService-DoAMQ_yn.js +5118 -0
  8. package/dist/chunks/TerminalAuthService-DoAMQ_yn.js.map +1 -0
  9. package/dist/chunks/index-DkoYIvIu.js +169 -0
  10. package/dist/chunks/index-DkoYIvIu.js.map +1 -0
  11. package/dist/collections/CliAuthRequestCollection.d.ts +19 -0
  12. package/dist/collections/CliAuthRequestCollection.d.ts.map +1 -0
  13. package/dist/collections/GroupCollection.d.ts +17 -0
  14. package/dist/collections/GroupCollection.d.ts.map +1 -0
  15. package/dist/collections/GroupMemberCollection.d.ts +43 -0
  16. package/dist/collections/GroupMemberCollection.d.ts.map +1 -0
  17. package/dist/collections/GroupRoleCollection.d.ts +33 -0
  18. package/dist/collections/GroupRoleCollection.d.ts.map +1 -0
  19. package/dist/collections/MagicLinkTokenCollection.d.ts +26 -0
  20. package/dist/collections/MagicLinkTokenCollection.d.ts.map +1 -0
  21. package/dist/collections/MembershipCollection.d.ts +38 -0
  22. package/dist/collections/MembershipCollection.d.ts.map +1 -0
  23. package/dist/collections/MembershipOverrideCollection.d.ts +55 -0
  24. package/dist/collections/MembershipOverrideCollection.d.ts.map +1 -0
  25. package/dist/collections/PermissionCollection.d.ts +34 -0
  26. package/dist/collections/PermissionCollection.d.ts.map +1 -0
  27. package/dist/collections/RoleCollection.d.ts +29 -0
  28. package/dist/collections/RoleCollection.d.ts.map +1 -0
  29. package/dist/collections/RolePermissionCollection.d.ts +33 -0
  30. package/dist/collections/RolePermissionCollection.d.ts.map +1 -0
  31. package/dist/collections/SessionCollection.d.ts +82 -0
  32. package/dist/collections/SessionCollection.d.ts.map +1 -0
  33. package/dist/collections/TenantCollection.d.ts +119 -0
  34. package/dist/collections/TenantCollection.d.ts.map +1 -0
  35. package/dist/collections/TenantPermissionOverrideCollection.d.ts +111 -0
  36. package/dist/collections/TenantPermissionOverrideCollection.d.ts.map +1 -0
  37. package/dist/collections/UserCollection.d.ts +116 -0
  38. package/dist/collections/UserCollection.d.ts.map +1 -0
  39. package/dist/collections/index.d.ts +19 -0
  40. package/dist/collections/index.d.ts.map +1 -0
  41. package/dist/index.d.ts +5 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +1482 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/manifest.json +5216 -0
  46. package/dist/models/CliAuthRequest.d.ts +25 -0
  47. package/dist/models/CliAuthRequest.d.ts.map +1 -0
  48. package/dist/models/Group.d.ts +34 -0
  49. package/dist/models/Group.d.ts.map +1 -0
  50. package/dist/models/GroupMember.d.ts +29 -0
  51. package/dist/models/GroupMember.d.ts.map +1 -0
  52. package/dist/models/GroupRole.d.ts +29 -0
  53. package/dist/models/GroupRole.d.ts.map +1 -0
  54. package/dist/models/MagicLinkToken.d.ts +22 -0
  55. package/dist/models/MagicLinkToken.d.ts.map +1 -0
  56. package/dist/models/Membership.d.ts +48 -0
  57. package/dist/models/Membership.d.ts.map +1 -0
  58. package/dist/models/MembershipOverride.d.ts +50 -0
  59. package/dist/models/MembershipOverride.d.ts.map +1 -0
  60. package/dist/models/Permission.d.ts +79 -0
  61. package/dist/models/Permission.d.ts.map +1 -0
  62. package/dist/models/Role.d.ts +67 -0
  63. package/dist/models/Role.d.ts.map +1 -0
  64. package/dist/models/RolePermission.d.ts +29 -0
  65. package/dist/models/RolePermission.d.ts.map +1 -0
  66. package/dist/models/Session.d.ts +105 -0
  67. package/dist/models/Session.d.ts.map +1 -0
  68. package/dist/models/Tenant.d.ts +138 -0
  69. package/dist/models/Tenant.d.ts.map +1 -0
  70. package/dist/models/TenantPermissionOverride.d.ts +74 -0
  71. package/dist/models/TenantPermissionOverride.d.ts.map +1 -0
  72. package/dist/models/User.d.ts +72 -0
  73. package/dist/models/User.d.ts.map +1 -0
  74. package/dist/models/index.d.ts +19 -0
  75. package/dist/models/index.d.ts.map +1 -0
  76. package/dist/playground.d.ts +2 -0
  77. package/dist/playground.d.ts.map +1 -0
  78. package/dist/playground.js +139 -0
  79. package/dist/playground.js.map +1 -0
  80. package/dist/services/MagicLinkService.d.ts +84 -0
  81. package/dist/services/MagicLinkService.d.ts.map +1 -0
  82. package/dist/services/OidcLoginService.d.ts +134 -0
  83. package/dist/services/OidcLoginService.d.ts.map +1 -0
  84. package/dist/services/PermissionCatalogService.d.ts +62 -0
  85. package/dist/services/PermissionCatalogService.d.ts.map +1 -0
  86. package/dist/services/PermissionResolver.d.ts +150 -0
  87. package/dist/services/PermissionResolver.d.ts.map +1 -0
  88. package/dist/services/PostgresPermissionPolicies.d.ts +29 -0
  89. package/dist/services/PostgresPermissionPolicies.d.ts.map +1 -0
  90. package/dist/services/SessionPermissionContext.d.ts +43 -0
  91. package/dist/services/SessionPermissionContext.d.ts.map +1 -0
  92. package/dist/services/SessionService.d.ts +139 -0
  93. package/dist/services/SessionService.d.ts.map +1 -0
  94. package/dist/services/TenantService.d.ts +135 -0
  95. package/dist/services/TenantService.d.ts.map +1 -0
  96. package/dist/services/TerminalAuthService.d.ts +189 -0
  97. package/dist/services/TerminalAuthService.d.ts.map +1 -0
  98. package/dist/services/index.d.ts +14 -0
  99. package/dist/services/index.d.ts.map +1 -0
  100. package/dist/smrt-knowledge.json +2744 -0
  101. package/dist/svelte/components/InviteUserModal.svelte +351 -0
  102. package/dist/svelte/components/InviteUserModal.svelte.d.ts +17 -0
  103. package/dist/svelte/components/InviteUserModal.svelte.d.ts.map +1 -0
  104. package/dist/svelte/components/UserAvatar.svelte +105 -0
  105. package/dist/svelte/components/UserAvatar.svelte.d.ts +10 -0
  106. package/dist/svelte/components/UserAvatar.svelte.d.ts.map +1 -0
  107. package/dist/svelte/components/UserCard.svelte +179 -0
  108. package/dist/svelte/components/UserCard.svelte.d.ts +18 -0
  109. package/dist/svelte/components/UserCard.svelte.d.ts.map +1 -0
  110. package/dist/svelte/components/UserForm.svelte +194 -0
  111. package/dist/svelte/components/UserForm.svelte.d.ts +18 -0
  112. package/dist/svelte/components/UserForm.svelte.d.ts.map +1 -0
  113. package/dist/svelte/components/UserList.svelte +107 -0
  114. package/dist/svelte/components/UserList.svelte.d.ts +20 -0
  115. package/dist/svelte/components/UserList.svelte.d.ts.map +1 -0
  116. package/dist/svelte/components/UserMenu.svelte +326 -0
  117. package/dist/svelte/components/UserMenu.svelte.d.ts +33 -0
  118. package/dist/svelte/components/UserMenu.svelte.d.ts.map +1 -0
  119. package/dist/svelte/components/__tests__/InviteUserModal.test.js +54 -0
  120. package/dist/svelte/components/__tests__/UserAvatar.test.js +31 -0
  121. package/dist/svelte/components/__tests__/UserCard.test.js +39 -0
  122. package/dist/svelte/components/__tests__/UserForm.test.js +50 -0
  123. package/dist/svelte/components/__tests__/UserList.test.js +48 -0
  124. package/dist/svelte/components/__tests__/UserMenu.test.js +38 -0
  125. package/dist/svelte/i18n.d.ts +15 -0
  126. package/dist/svelte/i18n.d.ts.map +1 -0
  127. package/dist/svelte/i18n.js +15 -0
  128. package/dist/svelte/index.d.ts +23 -0
  129. package/dist/svelte/index.d.ts.map +1 -0
  130. package/dist/svelte/index.js +27 -0
  131. package/dist/svelte/playground.d.ts +151 -0
  132. package/dist/svelte/playground.d.ts.map +1 -0
  133. package/dist/svelte/playground.js +134 -0
  134. package/dist/sveltekit/index.d.ts +379 -0
  135. package/dist/sveltekit/index.d.ts.map +1 -0
  136. package/dist/sveltekit/resource-list-handler.d.ts +127 -0
  137. package/dist/sveltekit/resource-list-handler.d.ts.map +1 -0
  138. package/dist/sveltekit/types.d.ts +31 -0
  139. package/dist/sveltekit/types.d.ts.map +1 -0
  140. package/dist/sveltekit.d.ts +2 -0
  141. package/dist/sveltekit.d.ts.map +1 -0
  142. package/dist/sveltekit.js +978 -0
  143. package/dist/sveltekit.js.map +1 -0
  144. package/dist/types/index.d.ts +61 -0
  145. package/dist/types/index.d.ts.map +1 -0
  146. package/dist/ui.d.ts +10 -0
  147. package/dist/ui.d.ts.map +1 -0
  148. package/dist/ui.js +75 -0
  149. package/dist/ui.js.map +1 -0
  150. package/package.json +97 -0
package/AGENTS.md ADDED
@@ -0,0 +1,85 @@
1
+ # @happyvertical/smrt-users
2
+
3
+ Multi-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.
4
+
5
+ ## Models (13)
6
+
7
+ | Model | Key Pattern |
8
+ |-------|-------------|
9
+ | User | Auth identity. `profileId` is plain string (not FK) to smrt-profiles. Email auto-lowercased. |
10
+ | Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |
11
+ | Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |
12
+ | MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |
13
+ | Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. |
14
+ | Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |
15
+ | Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |
16
+ | Group | Team within a tenant. Multiple roles via GroupRole. |
17
+ | GroupMember, GroupRole, RolePermission | Join tables. |
18
+ | MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |
19
+ | TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |
20
+
21
+ ## Permission Resolution — 4-Level Cascade
22
+
23
+ PermissionResolver evaluates in order (each level can add/remove permissions):
24
+
25
+ 1. **Tenant hierarchy** — walk ancestors, apply TenantPermissionOverride at each level
26
+ 2. **Membership role** — base permissions from user's role in tenant
27
+ 3. **Group roles** — permissions from all groups user belongs to **in that tenant**
28
+ 4. **Membership overrides** — final GRANT/DENY per-user (DENY takes absolute precedence)
29
+
30
+ **Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.
31
+
32
+ ## Hierarchical Tenants
33
+
34
+ - `TenantCollection.createChild()` auto-calculates hierarchy fields, enforces depth limit
35
+ - `moveToParent()` updates tenant + ALL descendants' paths/levels
36
+ - `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true
37
+ - `getTree(rootId?)` returns nested structure for UI
38
+
39
+ ## SvelteKit Integration
40
+
41
+ ```typescript
42
+ // hooks.server.ts
43
+ export const handle = createSessionHandler({ db, ttl: 604800, skipPaths: ['/api/public'] });
44
+ // Populates event.locals: { user, membership, permissions: string[], tenantId, sessionId }
45
+
46
+ // +page.server.ts
47
+ await createSessionCookie(event, userId, tenantId, { db });
48
+ await destroySessionCookie(event, { db });
49
+ await switchSessionTenant(event, tenantId, { db });
50
+ ```
51
+
52
+ ## Security (S5 #1400)
53
+
54
+ - **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**
55
+ User, Tenant, Group, Membership, MembershipOverride, Role, Permission,
56
+ RolePermission, GroupRole, GroupMember, and TenantPermissionOverride generate
57
+ `list`/`get` only — `create`/`update`/`delete` are intentionally NOT
58
+ generated. The merged `requireRouteAuth` gate (#1540) enforces *authentication*,
59
+ not *authorization*, and these models are not `@TenantScoped`, so an
60
+ auto-generated mutating route would let any authenticated user self-grant a
61
+ role/permission, flip a tenant's cascade flags, or change another user's auth
62
+ identity. Mutate them through the permission-gated services (`TenantService`,
63
+ collection helpers) or consumer-owned, permission-checked handlers. A
64
+ structural regression test (`security-audit-1400.test.ts`) enumerates the
65
+ registry to assert no authority model exposes a mutating op. (`cli` stays
66
+ enabled — local-operator surface, outside the network/agent threat model.)
67
+ - **`switchTenant` is fail-closed.** `SessionService.switchTenant` /
68
+ `switchSessionTenant` verify the session's user has an ACTIVE membership in the
69
+ target tenant before writing `session.tenantId` (the tenant-isolation key for
70
+ every `@TenantScoped` query). A non-member switch returns `false` without
71
+ mutating the session; `null` clears the context and is always allowed. The
72
+ low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive —
73
+ never call it with an untrusted tenant id.
74
+ - **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`
75
+ refuses to provision a user when the IdP explicitly returns
76
+ `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An
77
+ absent claim makes no assertion and is not enforced.
78
+
79
+ ## Gotchas
80
+
81
+ - **seedSystemRoles() required**: call `RoleCollection.seedSystemRoles()` at app init (creates owner/admin/member/viewer)
82
+ - **PermissionResolver casts `as any`**: collections have protected constructors — known framework limitation
83
+ - **Session TTL in seconds**: `DEFAULT_SESSION_TTL = 7 * 24 * 60 * 60` (not milliseconds)
84
+ - **Users are cross-tenant**: one user, many tenants via Membership. Email globally unique.
85
+ - **Batch permission queries**: resolver fetches all permission IDs in one query, then maps to slugs (avoids N+1)
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,459 @@
1
+ # @happyvertical/smrt-users
2
+
3
+ Multi-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @happyvertical/smrt-users
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Roles and permissions
14
+
15
+ ```typescript
16
+ import {
17
+ RoleCollection, MembershipCollection, PermissionResolver,
18
+ } from '@happyvertical/smrt-users';
19
+
20
+ const db = { db: { type: 'sqlite', url: 'app.db' } };
21
+
22
+ // Seed system roles (owner, admin, member, viewer) — required at app init
23
+ const roles = await RoleCollection.create(db);
24
+ await roles.seedSystemRoles();
25
+
26
+ // Assign a user to a tenant with the admin role
27
+ const memberships = await MembershipCollection.create(db);
28
+ const adminRole = await roles.findBySlug('admin');
29
+ await (await memberships.create({
30
+ userId: user.id, tenantId: tenant.id, roleId: adminRole.id,
31
+ })).save();
32
+
33
+ // Check permissions
34
+ const resolver = await PermissionResolver.create(db);
35
+ await resolver.hasPermission(user.id, tenant.id, 'articles.create');
36
+ ```
37
+
38
+ ### Manifest-derived permission catalog
39
+
40
+ SMRT objects now contribute permissions automatically based on their public
41
+ surface area.
42
+
43
+ ```typescript
44
+ import { SmrtObject, smrt } from '@happyvertical/smrt-core';
45
+
46
+ @smrt({
47
+ api: { include: ['list', 'create', 'publish'] },
48
+ cli: { include: ['get', 'archive'] },
49
+ collection: 'articles',
50
+ mcp: { include: ['update'] },
51
+ tenantScoped: { mode: 'required' },
52
+ })
53
+ class Article extends SmrtObject {
54
+ tenantId: string = '';
55
+ title: string = '';
56
+
57
+ async publish(): Promise<boolean> {
58
+ return true;
59
+ }
60
+
61
+ async archive(): Promise<boolean> {
62
+ return true;
63
+ }
64
+ }
65
+ ```
66
+
67
+ This produces the following permission slugs:
68
+
69
+ - `articles.read` from `list` or `get`
70
+ - `articles.create`
71
+ - `articles.update`
72
+ - `articles.publish`
73
+ - `articles.archive`
74
+
75
+ Non-public methods and actions that are not exposed through API, CLI, or MCP are
76
+ not added to the catalog.
77
+
78
+ ### Sync the permission catalog
79
+
80
+ Use `syncPermissionCatalog()` during bootstrapping, migrations, or deploy hooks
81
+ to upsert discovered permissions into the `Permission` table.
82
+
83
+ ```typescript
84
+ import { syncPermissionCatalog } from '@happyvertical/smrt-users';
85
+
86
+ const db = {
87
+ db: {
88
+ type: 'postgres' as const,
89
+ url: process.env.DATABASE_URL!,
90
+ },
91
+ };
92
+
93
+ const result = await syncPermissionCatalog(db);
94
+
95
+ console.log('created', result.created);
96
+ console.log('updated', result.updated);
97
+ console.log('unchanged', result.unchanged);
98
+ ```
99
+
100
+ Catalog sync is additive and fail-closed:
101
+
102
+ - it creates missing `Permission` rows
103
+ - it updates `name`, `description`, and `category` by slug
104
+ - it does not auto-grant permissions to roles
105
+ - it does not delete stale permissions in v1
106
+
107
+ ### App-defined permissions in `smrt.config.ts`
108
+
109
+ Use package config for permissions that do not come from the manifest.
110
+
111
+ ```typescript
112
+ // smrt.config.ts
113
+ import { defineConfig } from '@happyvertical/smrt-config';
114
+
115
+ export default defineConfig({
116
+ packages: {
117
+ users: {
118
+ permissions: {
119
+ custom: [
120
+ {
121
+ category: 'app',
122
+ description: 'Allows access to the operations dashboard',
123
+ name: 'View Operations Dashboard',
124
+ slug: 'operations.dashboard',
125
+ },
126
+ {
127
+ category: 'audits',
128
+ name: 'Inspect Audit Rows',
129
+ postgres: {
130
+ bindings: [
131
+ {
132
+ action: 'select',
133
+ tableName: 'audit_logs',
134
+ },
135
+ {
136
+ action: 'insert',
137
+ tableName: 'audit_logs',
138
+ },
139
+ ],
140
+ },
141
+ slug: 'audits.inspect',
142
+ },
143
+ ],
144
+ postgres: {
145
+ enabled: true,
146
+ },
147
+ },
148
+ },
149
+ },
150
+ });
151
+ ```
152
+
153
+ Custom permissions merge with manifest-derived permissions by slug. If the same
154
+ slug is registered with conflicting metadata, SMRT throws so the mismatch is
155
+ visible early.
156
+
157
+ ### Runtime permission registration
158
+
159
+ Use `registerPermissionDefinitions()` when a package or integration needs to
160
+ declare permissions at runtime.
161
+
162
+ ```typescript
163
+ import {
164
+ registerPermissionDefinitions,
165
+ syncPermissionCatalog,
166
+ } from '@happyvertical/smrt-users';
167
+
168
+ const unregister = registerPermissionDefinitions([
169
+ {
170
+ category: 'billing',
171
+ description: 'Allows exporting invoices',
172
+ name: 'Export Invoices',
173
+ slug: 'invoices.export',
174
+ },
175
+ ]);
176
+
177
+ try {
178
+ await syncPermissionCatalog({
179
+ db: { type: 'sqlite', url: 'app.db' },
180
+ });
181
+ } finally {
182
+ unregister();
183
+ }
184
+ ```
185
+
186
+ ### Postgres RLS enforcement
187
+
188
+ For Postgres, SMRT can generate and apply row-level security policies directly
189
+ from the permission catalog.
190
+
191
+ ```typescript
192
+ import {
193
+ applyPostgresPermissionPolicies,
194
+ generatePostgresPermissionSql,
195
+ syncPermissionCatalog,
196
+ } from '@happyvertical/smrt-users';
197
+
198
+ const db = {
199
+ db: {
200
+ type: 'postgres' as const,
201
+ url: process.env.DATABASE_URL!,
202
+ },
203
+ };
204
+
205
+ await syncPermissionCatalog(db);
206
+
207
+ const preview = generatePostgresPermissionSql(db);
208
+ console.log(preview.targets);
209
+ console.log(preview.skipped);
210
+
211
+ await applyPostgresPermissionPolicies(db);
212
+ ```
213
+
214
+ Automatic policy generation currently applies only to objects that are:
215
+
216
+ - tenant-scoped with `tenantScoped: { mode: 'required' }`
217
+ - backed by a real Postgres table
218
+ - mapped to a single tenant field
219
+
220
+ Automatic CRUD policy mapping is fixed in v1:
221
+
222
+ - `SELECT` -> `<collection>.read`
223
+ - `INSERT` -> `<collection>.create`
224
+ - `UPDATE` -> `<collection>.update`
225
+ - `DELETE` -> `<collection>.delete`
226
+
227
+ Optional-tenancy and global tables are skipped and returned in
228
+ `result.skipped` instead of generating unsafe policies. Custom permissions can
229
+ participate in RLS by adding explicit Postgres bindings as shown above.
230
+
231
+ ### SvelteKit hooks
232
+
233
+ ```typescript
234
+ // hooks.server.ts
235
+ import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';
236
+
237
+ export const handle = createSessionHandler({
238
+ db: { type: 'postgres', url: process.env.DATABASE_URL },
239
+ enterTenantContext: true,
240
+ postgresRls: true,
241
+ ttl: 7 * 24 * 60 * 60, // 7 days in seconds
242
+ skipPaths: ['/api/health'],
243
+ });
244
+ // Populates event.locals: { user, permissions, tenantId, sessionId }
245
+
246
+ // +page.server.ts
247
+ import { createSessionCookie, destroySessionCookie } from '@happyvertical/smrt-users/sveltekit';
248
+
249
+ await createSessionCookie(event, userId, tenantId, { db }); // login
250
+ await destroySessionCookie(event, { db }); // logout
251
+ ```
252
+
253
+ ### OIDC login with Kanidm or Dex
254
+
255
+ Kanidm and Dex both work through the generic SMRT OIDC flow. Configure one or
256
+ more providers under `packages.users.auth.oidc.providers`, then add login and
257
+ callback route handlers.
258
+
259
+ ```typescript
260
+ // smrt.config.ts
261
+ import { defineConfig } from '@happyvertical/smrt-config';
262
+
263
+ export default defineConfig({
264
+ packages: {
265
+ users: {
266
+ auth: {
267
+ oidc: {
268
+ defaultProvider: 'kanidm',
269
+ providers: {
270
+ kanidm: {
271
+ kind: 'kanidm',
272
+ issuer: process.env.KANIDM_ISSUER!,
273
+ clientId: process.env.KANIDM_CLIENT_ID!,
274
+ clientSecret: process.env.KANIDM_CLIENT_SECRET,
275
+ redirectUri: 'http://localhost:5173/auth/kanidm/callback',
276
+ },
277
+ dex: {
278
+ kind: 'dex',
279
+ issuer: process.env.DEX_ISSUER!,
280
+ clientId: process.env.DEX_CLIENT_ID!,
281
+ clientSecret: process.env.DEX_CLIENT_SECRET,
282
+ redirectUri: 'http://localhost:5173/auth/dex/callback',
283
+ },
284
+ },
285
+ },
286
+ },
287
+ },
288
+ },
289
+ });
290
+ ```
291
+
292
+ ```typescript
293
+ // src/routes/auth/[provider]/login/+server.ts
294
+ import { createOidcLoginHandler } from '@happyvertical/smrt-users/sveltekit';
295
+
296
+ export const GET = createOidcLoginHandler({
297
+ db: { type: 'postgres', url: process.env.DATABASE_URL! },
298
+ });
299
+ ```
300
+
301
+ ```typescript
302
+ // src/routes/auth/[provider]/callback/+server.ts
303
+ import { createOidcCallbackHandler } from '@happyvertical/smrt-users/sveltekit';
304
+
305
+ export const GET = createOidcCallbackHandler({
306
+ db: { type: 'postgres', url: process.env.DATABASE_URL! },
307
+ successRedirect: '/dashboard',
308
+ });
309
+ ```
310
+
311
+ The callback verifies `state`, PKCE, issuer, audience, nonce, and the provider
312
+ JWKS-signed ID token, falling back to the OIDC UserInfo endpoint when the ID
313
+ token omits required profile claims like `email`. Temporary transaction cookies
314
+ are HMAC-signed with the provider `clientSecret` when present; public clients
315
+ can pass `transactionCookieSecret` to the route helpers. On success it creates
316
+ or reuses a SMRT `Profile`, links an `OidcIdentity`, creates or reuses a `User`,
317
+ records `lastLoginAt`, and sets the standard SMRT session cookie.
318
+
319
+ With `postgresRls: true`, SMRT opens a request-scoped Postgres transaction,
320
+ loads the session, resolves permissions, and sets session variables used by the
321
+ generated RLS helpers:
322
+
323
+ - `smrt.tenant_id`
324
+ - `smrt.user_id`
325
+ - `smrt.session_id`
326
+ - `smrt.permissions`
327
+ - `smrt.super_admin_bypass`
328
+ - `smrt.system_context`
329
+
330
+ With `enterTenantContext: true`, the same request also enters
331
+ `@happyvertical/smrt-tenancy` context so regular collection access is scoped to
332
+ the current tenant in application code.
333
+
334
+ ### Request-scoped database access
335
+
336
+ Generated SvelteKit helpers and custom server code can read the current
337
+ request-scoped database, which is especially useful when Postgres RLS is enabled
338
+ and you want collection operations to use the active transaction.
339
+
340
+ ```typescript
341
+ import {
342
+ getRequestScopedDatabase,
343
+ withSessionPermissionContext,
344
+ } from '@happyvertical/smrt-users';
345
+
346
+ const response = await withSessionPermissionContext(
347
+ {
348
+ db: { type: 'postgres', url: process.env.DATABASE_URL! },
349
+ enterTenantContext: true,
350
+ postgresRls: true,
351
+ sessionId,
352
+ },
353
+ async (context) => {
354
+ const database = getRequestScopedDatabase();
355
+
356
+ console.log(context.permissions);
357
+ console.log(database === context.database); // true
358
+
359
+ return new Response('ok');
360
+ },
361
+ );
362
+ ```
363
+
364
+ ## Key Concepts
365
+
366
+ ### Permission cascade (4 levels)
367
+
368
+ PermissionResolver evaluates permissions in order, where each level can add or remove grants:
369
+
370
+ 1. **Tenant hierarchy** -- walk ancestors, apply TenantPermissionOverride at each level
371
+ 2. **Membership role** -- base permissions from the user's role in the tenant
372
+ 3. **Group roles** -- permissions from all groups the user belongs to in that tenant
373
+ 4. **Membership overrides** -- per-user GRANT/DENY (DENY always wins)
374
+
375
+ Tenant-level inherited permissions are part of the effective permission set
376
+ returned by `resolvePermissions()` and `SessionService.loadSessionContext()`.
377
+
378
+ ### Hierarchical tenants
379
+
380
+ Tenants support parent-child trees (max depth 10). Two flags control inheritance: `cascadePermissions` (parent pushes down) and `inheritPermissions` (child accepts). Both must be true for permissions to flow.
381
+
382
+ ### Tenant policies
383
+
384
+ TenantService supports three modes: `flexible` (no auto-create), `personal` (auto-create on first login, deletable), `required` (auto-create, must keep at least one).
385
+
386
+ ## API
387
+
388
+ ### Models
389
+
390
+ | Export | Description |
391
+ |--------|-------------|
392
+ | `User` | Auth identity. Email auto-lowercased. `profileId` links to smrt-profiles (plain string). |
393
+ | `Tenant` | Organizational boundary. STI. Hierarchical via `parentTenantId`/`hierarchyPath`. |
394
+ | `Role` | Permission template. `tenantId = null` for system roles. `isSystem` blocks deletion. |
395
+ | `Permission` | Named capability. Slug format: `resource.action`. |
396
+ | `Session` | Server-side session. Secure UUID. TTL in seconds. |
397
+ | `Group` | Team within a tenant. Gains permissions via GroupRole. |
398
+ | `Membership` | User + Tenant + Role junction. UNIQUE(userId, tenantId). |
399
+ | `MembershipOverride` | Per-user permission grant/deny on a membership. |
400
+ | `TenantPermissionOverride` | Tenant-level permission override (INHERIT/GRANT/DENY). |
401
+ | `GroupMember`, `GroupRole`, `RolePermission` | Junction tables for groups and role-permission assignments. |
402
+
403
+ ### Collections
404
+
405
+ | Export | Description |
406
+ |--------|-------------|
407
+ | `UserCollection`, `TenantCollection`, `RoleCollection` | Core CRUD. TenantCollection adds `createChild()`, `getTree()`. RoleCollection adds `seedSystemRoles()`. |
408
+ | `PermissionCollection`, `SessionCollection` | Permission CRUD with `findByIds()`. Session CRUD with `findValidSession()`, `deleteExpired()`. |
409
+ | `MembershipCollection` | Membership CRUD, `findByUserAndTenant()` |
410
+ | `MembershipOverrideCollection`, `TenantPermissionOverrideCollection` | Override management at membership and tenant levels |
411
+ | `GroupCollection`, `GroupMemberCollection`, `GroupRoleCollection`, `RolePermissionCollection` | Group and role-permission junction management |
412
+
413
+ ### Services
414
+
415
+ | Export | Description |
416
+ |--------|-------------|
417
+ | `PermissionResolver` | Resolves effective permissions via 4-level cascade. `hasPermission()`, `resolvePermissions()`. |
418
+ | `PermissionCatalogService`, `syncPermissionCatalog()` | Discovers manifest/config/runtime permissions and upserts them into `Permission` rows. |
419
+ | `registerPermissionDefinitions()` | Register app or integration permissions at runtime and receive an unregister cleanup function. |
420
+ | `generatePostgresPermissionSql()`, `applyPostgresPermissionPolicies()` | Preview or apply Postgres RLS helper functions and table policies. |
421
+ | `SessionService` | High-level session management. `createSession()`, `loadSessionContext()`, `destroySession()`. |
422
+ | `OidcLoginService` | Generic OIDC authorization-code login with PKCE for Kanidm, Dex, and other standards-compliant providers. |
423
+ | `withSessionPermissionContext()` | Loads a session, optionally enters tenancy context, and exposes a request-scoped database/permission context. |
424
+ | `getCurrentSessionPermissionContext()`, `getRequestScopedDatabase()` | Read the active request/session context inside app code. |
425
+ | `TenantService` | Policy-driven tenant lifecycle. `ensureTenantForUser()`, `createTenantWithOwnership()`. |
426
+
427
+ ### SvelteKit (`@happyvertical/smrt-users/sveltekit`)
428
+
429
+ | Export | Description |
430
+ |--------|-------------|
431
+ | `createSessionHandler` | SvelteKit handle hook that populates `event.locals`, and can also enter tenancy context and Postgres RLS request transactions |
432
+ | `createSessionCookie` | Set session cookie after login |
433
+ | `destroySessionCookie` | Clear session cookie on logout |
434
+ | `switchSessionTenant` | Change tenant context for current session |
435
+ | `beginOidcLogin`, `completeOidcLogin` | Low-level SvelteKit helpers for custom OIDC login routes |
436
+ | `createOidcLoginHandler`, `createOidcCallbackHandler` | Ready-to-use SvelteKit route handlers for OIDC login and callback |
437
+ | `SessionLocals` | Type for `event.locals` (extend in `app.d.ts`) |
438
+
439
+ ### Types & Constants
440
+
441
+ | Export | Description |
442
+ |--------|-------------|
443
+ | `UserStatus`, `TenantStatus`, `SessionStatus`, `MembershipStatus` | Status enums |
444
+ | `OverrideEffect`, `TenantPermissionEffect` | Override effect enums |
445
+ | `DEFAULT_ROLE_SLUGS`, `DEFAULT_ROLES`, `DEFAULT_TENANT_POLICY` | System role slugs, role configs, default tenant policy |
446
+ | `DEFAULT_SESSION_TTL`, `MAX_TENANT_HIERARCHY_DEPTH` | 604800 (7 days in seconds), 10 |
447
+ | `TenantHierarchyError` | Thrown when hierarchy depth limit is exceeded |
448
+
449
+ ## Dependencies
450
+
451
+ - `@happyvertical/smrt-core` -- ORM, `@smrt()` decorator, SmrtObject/SmrtCollection
452
+ - `@happyvertical/smrt-types` -- shared enums (UserStatus, SessionStatus, etc.)
453
+ - `@happyvertical/smrt-profiles` -- optional peer dependency for profile linking
454
+ - `jose` -- JWT/JWKS verification for OIDC and magic-link tokens
455
+ - `svelte` -- optional peer dependency for Svelte components
456
+
457
+ ## License
458
+
459
+ MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=__smrt-register__.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}