@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/dist/index.js ADDED
@@ -0,0 +1,1482 @@
1
+ import { D as DEFAULT_ROLES, n as normalizeEmail, i as isValidEmail, P as PermissionCollection, p as parsePermissionSlug, a as isValidPermissionSlug, b as DEFAULT_TENANT_POLICY, T as TenantCollection, M as MembershipCollection, c as DEFAULT_ROLE_SLUGS } from "./chunks/TerminalAuthService-DoAMQ_yn.js";
2
+ import { U, d, e, f, g, h, G, j, k, l, m, o, q, r, O, s, t, u, R, v, S, w, x, y, z, A, B, C, E, F, H, I, U as U2, d as d2, J, K, L, N, Q, V, W, X } from "./chunks/TerminalAuthService-DoAMQ_yn.js";
3
+ import { foreignKey, smrt, SmrtObject, SmrtCollection, field, ObjectRegistry, findManifestEntryByQualifiedName } from "@happyvertical/smrt-core";
4
+ import { getPackageConfig } from "@happyvertical/smrt-config";
5
+ import { createHash } from "node:crypto";
6
+ import { MembershipStatus } from "@happyvertical/smrt-types";
7
+ import { MembershipStatus as MembershipStatus2, OverrideEffect, SessionStatus, TenantPermissionEffect, TenantStatus, UserStatus } from "@happyvertical/smrt-types";
8
+ var __defProp$2 = Object.defineProperty;
9
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
10
+ var __decorateClass$2 = (decorators, target, key, kind) => {
11
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
12
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
13
+ if (decorator = decorators[i])
14
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
15
+ if (kind && result) __defProp$2(target, key, result);
16
+ return result;
17
+ };
18
+ let Group = class extends SmrtObject {
19
+ tenantId;
20
+ /**
21
+ * Display name for the group
22
+ */
23
+ name = "";
24
+ /**
25
+ * Description of the group
26
+ */
27
+ description = "";
28
+ constructor(options = {}) {
29
+ super(options);
30
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
31
+ if (options.name !== void 0) this.name = options.name;
32
+ if (options.description !== void 0)
33
+ this.description = options.description;
34
+ }
35
+ };
36
+ __decorateClass$2([
37
+ foreignKey("Tenant", { required: true })
38
+ ], Group.prototype, "tenantId", 2);
39
+ Group = __decorateClass$2([
40
+ smrt({
41
+ // #1400: read-only generated surface — RBAC/identity writes go through
42
+ // permission-gated services, not auth-only generated CRUD.
43
+ api: { include: ["list", "get"] },
44
+ mcp: { include: ["list", "get"] },
45
+ cli: true
46
+ })
47
+ ], Group);
48
+ class GroupCollection extends SmrtCollection {
49
+ static _itemClass = Group;
50
+ /**
51
+ * Find all groups in a tenant
52
+ */
53
+ async findByTenant(tenantId) {
54
+ return await this.list({
55
+ where: { tenantId },
56
+ orderBy: "name ASC"
57
+ });
58
+ }
59
+ /**
60
+ * Find group by slug within a tenant
61
+ */
62
+ async findBySlug(slug, tenantId) {
63
+ const results = await this.list({
64
+ where: { slug, tenantId },
65
+ limit: 1
66
+ });
67
+ return results.length > 0 ? results[0] : null;
68
+ }
69
+ }
70
+ var __defProp$1 = Object.defineProperty;
71
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
72
+ var __decorateClass$1 = (decorators, target, key, kind) => {
73
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
74
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
75
+ if (decorator = decorators[i])
76
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
77
+ if (kind && result) __defProp$1(target, key, result);
78
+ return result;
79
+ };
80
+ const DEFAULT_TOKEN_EXPIRY_SECONDS = 10 * 60;
81
+ let UsersMagicLinkToken = class extends SmrtObject {
82
+ nonce = "";
83
+ /** Email address this token was generated for */
84
+ email = "";
85
+ /** Whether this token has been used */
86
+ used = false;
87
+ /** When this token expires */
88
+ expiresAt = new Date(Date.now() + DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3);
89
+ constructor(options = {}) {
90
+ super(options);
91
+ if (options.nonce !== void 0) this.nonce = options.nonce;
92
+ if (options.email !== void 0) this.email = options.email;
93
+ if (options.used !== void 0) this.used = options.used;
94
+ if (options.expiresAt !== void 0) {
95
+ this.expiresAt = options.expiresAt instanceof Date ? options.expiresAt : new Date(options.expiresAt);
96
+ }
97
+ }
98
+ /** Check if the token has expired */
99
+ isExpired() {
100
+ return /* @__PURE__ */ new Date() > this.expiresAt;
101
+ }
102
+ /** Check if the token is still valid (unused and not expired) */
103
+ isValid() {
104
+ return !this.used && !this.isExpired();
105
+ }
106
+ };
107
+ __decorateClass$1([
108
+ field({ required: true, unique: true })
109
+ ], UsersMagicLinkToken.prototype, "nonce", 2);
110
+ UsersMagicLinkToken = __decorateClass$1([
111
+ smrt({
112
+ tableName: "users_magic_link_tokens",
113
+ // Magic link tokens are security-sensitive — no public API
114
+ api: { include: [] },
115
+ mcp: { include: [] },
116
+ cli: true
117
+ })
118
+ ], UsersMagicLinkToken);
119
+ class UsersMagicLinkTokenCollection extends SmrtCollection {
120
+ static _itemClass = UsersMagicLinkToken;
121
+ /**
122
+ * Find a token by its nonce
123
+ */
124
+ async findByNonce(nonce) {
125
+ return this.findOne({
126
+ where: { nonce }
127
+ });
128
+ }
129
+ /**
130
+ * Atomically mark a token as used (single-use enforcement).
131
+ *
132
+ * Returns true if the nonce was successfully claimed (transitioned from
133
+ * unused to used). Returns false if the nonce was already used, expired,
134
+ * or doesn't exist — preventing race conditions in concurrent verify() calls.
135
+ */
136
+ async markUsed(nonce) {
137
+ const now = (/* @__PURE__ */ new Date()).toISOString();
138
+ const { rowCount } = await this.db.query(
139
+ `UPDATE ${this.tableName}
140
+ SET used = ?, updated_at = ?
141
+ WHERE nonce = ? AND used = ? AND expires_at > ?`,
142
+ true,
143
+ now,
144
+ nonce,
145
+ false,
146
+ now
147
+ );
148
+ return rowCount > 0;
149
+ }
150
+ /**
151
+ * Delete expired tokens (cleanup job)
152
+ */
153
+ async deleteExpired() {
154
+ const now = /* @__PURE__ */ new Date();
155
+ const tokens = await this.list({
156
+ where: {
157
+ "expiresAt <": now.toISOString()
158
+ }
159
+ });
160
+ let count = 0;
161
+ for (const token of tokens) {
162
+ await token.delete();
163
+ count++;
164
+ }
165
+ return count;
166
+ }
167
+ }
168
+ var __defProp = Object.defineProperty;
169
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
170
+ var __decorateClass = (decorators, target, key, kind) => {
171
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
172
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
173
+ if (decorator = decorators[i])
174
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
175
+ if (kind && result) __defProp(target, key, result);
176
+ return result;
177
+ };
178
+ let Role = class extends SmrtObject {
179
+ tenantId;
180
+ /**
181
+ * Display name for the role
182
+ */
183
+ name = "";
184
+ /**
185
+ * Description of the role
186
+ */
187
+ description = "";
188
+ /**
189
+ * Whether this is a system role (cannot be deleted)
190
+ */
191
+ isSystem = false;
192
+ constructor(options = {}) {
193
+ super(options);
194
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
195
+ if (options.name !== void 0) this.name = options.name;
196
+ if (options.description !== void 0)
197
+ this.description = options.description;
198
+ if (options.isSystem !== void 0) this.isSystem = options.isSystem;
199
+ }
200
+ /**
201
+ * Check if this is a system-wide role
202
+ */
203
+ isSystemRole() {
204
+ return this.tenantId === null || this.tenantId === void 0;
205
+ }
206
+ /**
207
+ * Check if this is a tenant-specific role
208
+ */
209
+ isTenantRole() {
210
+ return this.tenantId !== null && this.tenantId !== void 0;
211
+ }
212
+ /**
213
+ * Check if this role can be deleted.
214
+ * System roles (isSystem = true) cannot be deleted.
215
+ * @returns true if the role can be deleted
216
+ */
217
+ canDelete() {
218
+ return !this.isSystem;
219
+ }
220
+ /**
221
+ * Delete guard - prevents deletion of system roles.
222
+ * Override the delete method to check isSystem flag first.
223
+ */
224
+ async delete() {
225
+ if (this.isSystem) {
226
+ throw new Error(
227
+ `Cannot delete system role '${this.slug}'. System roles are protected.`
228
+ );
229
+ }
230
+ return super.delete();
231
+ }
232
+ };
233
+ __decorateClass([
234
+ foreignKey("Tenant", { nullable: true })
235
+ ], Role.prototype, "tenantId", 2);
236
+ Role = __decorateClass([
237
+ smrt({
238
+ // #1400: read-only generated surface — RBAC/identity writes go through
239
+ // permission-gated services, not auth-only generated CRUD.
240
+ api: { include: ["list", "get"] },
241
+ mcp: { include: ["list", "get"] },
242
+ cli: true
243
+ })
244
+ ], Role);
245
+ class RoleCollection extends SmrtCollection {
246
+ static _itemClass = Role;
247
+ /**
248
+ * Find all system roles (tenantId is null)
249
+ */
250
+ async findSystemRoles() {
251
+ return await this.query(
252
+ `SELECT * FROM ${this.tableName} WHERE tenant_id IS NULL ORDER BY name ASC`
253
+ );
254
+ }
255
+ /**
256
+ * Find roles available for a tenant (system + tenant-specific)
257
+ */
258
+ async findByTenant(tenantId) {
259
+ return await this.query(
260
+ `SELECT * FROM ${this.tableName}
261
+ WHERE tenant_id IS NULL OR tenant_id = ?
262
+ ORDER BY is_system DESC, name ASC`,
263
+ [tenantId]
264
+ );
265
+ }
266
+ /**
267
+ * Find tenant-specific roles only
268
+ */
269
+ async findTenantRoles(tenantId) {
270
+ return await this.list({
271
+ where: { tenantId },
272
+ orderBy: "name ASC"
273
+ });
274
+ }
275
+ /**
276
+ * Find role by slug within a tenant context
277
+ */
278
+ async findBySlug(slug, tenantId) {
279
+ if (tenantId) {
280
+ const tenantRoles = await this.list({
281
+ where: { slug, tenantId },
282
+ limit: 1
283
+ });
284
+ if (tenantRoles.length > 0) {
285
+ return tenantRoles[0];
286
+ }
287
+ }
288
+ const systemRoles = await this.query(
289
+ `SELECT * FROM ${this.tableName} WHERE slug = ? AND tenant_id IS NULL LIMIT 1`,
290
+ [slug]
291
+ );
292
+ return systemRoles.length > 0 ? systemRoles[0] : null;
293
+ }
294
+ /**
295
+ * Seed default system roles
296
+ */
297
+ async seedSystemRoles() {
298
+ const roles = [];
299
+ for (const roleDef of DEFAULT_ROLES) {
300
+ const existing = await this.findBySlug(roleDef.slug);
301
+ if (existing) {
302
+ roles.push(existing);
303
+ continue;
304
+ }
305
+ const role = await this.create({
306
+ slug: roleDef.slug,
307
+ name: roleDef.name,
308
+ description: roleDef.description,
309
+ tenantId: null,
310
+ isSystem: true
311
+ });
312
+ await role.save();
313
+ roles.push(role);
314
+ }
315
+ return roles;
316
+ }
317
+ }
318
+ class MagicLinkError extends Error {
319
+ constructor(message) {
320
+ super(message);
321
+ this.name = "MagicLinkError";
322
+ }
323
+ }
324
+ class MagicLinkService {
325
+ tokenCollection;
326
+ signingKey = null;
327
+ secret;
328
+ tokenExpiry;
329
+ issuer;
330
+ options;
331
+ constructor(options) {
332
+ if (!options.secret) {
333
+ throw new Error("MagicLinkService requires a secret for token signing");
334
+ }
335
+ this.secret = options.secret;
336
+ this.tokenExpiry = options.tokenExpiry ?? DEFAULT_TOKEN_EXPIRY_SECONDS;
337
+ this.issuer = options.issuer ?? "smrt:magiclink";
338
+ this.options = options;
339
+ }
340
+ /**
341
+ * Initialize collections
342
+ */
343
+ async initialize() {
344
+ this.tokenCollection = await UsersMagicLinkTokenCollection.create(
345
+ this.options
346
+ );
347
+ }
348
+ /**
349
+ * Derive the HMAC signing key from the secret
350
+ */
351
+ async getSigningKey() {
352
+ if (this.signingKey) return this.signingKey;
353
+ const encoder = new TextEncoder();
354
+ const data = encoder.encode(`magiclink:${this.secret}`);
355
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
356
+ this.signingKey = new Uint8Array(hashBuffer);
357
+ return this.signingKey;
358
+ }
359
+ /**
360
+ * Generate a magic link token for the given email.
361
+ *
362
+ * Stores a nonce in the database for replay protection.
363
+ * The caller is responsible for emailing the token to the user.
364
+ */
365
+ async generate(email) {
366
+ const { SignJWT } = await import("./chunks/index-DkoYIvIu.js");
367
+ const key = await this.getSigningKey();
368
+ const nonce = crypto.randomUUID();
369
+ const normalizedEmail = normalizeEmail(email);
370
+ if (!isValidEmail(normalizedEmail)) {
371
+ throw new MagicLinkError("Invalid email address");
372
+ }
373
+ const expiresAt = new Date(Date.now() + this.tokenExpiry * 1e3);
374
+ await this.tokenCollection.create({
375
+ nonce,
376
+ email: normalizedEmail,
377
+ used: false,
378
+ expiresAt
379
+ });
380
+ const token = await new SignJWT({
381
+ email: normalizedEmail,
382
+ nonce
383
+ }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${this.tokenExpiry}s`).setIssuer(this.issuer).sign(key);
384
+ return { token, expiresAt };
385
+ }
386
+ /**
387
+ * Verify a magic link token.
388
+ *
389
+ * Checks JWT signature, expiry, and that the nonce hasn't been used.
390
+ * Marks the nonce as used on success (single-use enforcement).
391
+ *
392
+ * @throws {MagicLinkError} If the token is invalid, expired, or already used
393
+ */
394
+ async verify(token) {
395
+ const { jwtVerify, errors } = await import("./chunks/index-DkoYIvIu.js");
396
+ const key = await this.getSigningKey();
397
+ let payload;
398
+ try {
399
+ const result = await jwtVerify(token, key, {
400
+ issuer: this.issuer
401
+ });
402
+ payload = result.payload;
403
+ } catch (err) {
404
+ if (err instanceof errors.JWTExpired) {
405
+ throw new MagicLinkError("Token has expired");
406
+ }
407
+ throw new MagicLinkError("Invalid token");
408
+ }
409
+ const email = payload.email;
410
+ const nonce = payload.nonce;
411
+ if (typeof email !== "string" || typeof nonce !== "string") {
412
+ throw new MagicLinkError("Invalid token payload");
413
+ }
414
+ const claimed = await this.tokenCollection.markUsed(nonce);
415
+ if (!claimed) {
416
+ throw new MagicLinkError("Token has already been used or has expired");
417
+ }
418
+ return { email: normalizeEmail(email), nonce };
419
+ }
420
+ /**
421
+ * Clean up expired tokens (run periodically)
422
+ */
423
+ async cleanupExpiredTokens() {
424
+ return this.tokenCollection.deleteExpired();
425
+ }
426
+ /**
427
+ * Static factory method
428
+ */
429
+ static async create(options) {
430
+ const service = new MagicLinkService(options);
431
+ await service.initialize();
432
+ return service;
433
+ }
434
+ }
435
+ function getRuntimePermissionRegistrations() {
436
+ globalThis.__smrtUsersPermissionRegistrations ??= /* @__PURE__ */ new Map();
437
+ return globalThis.__smrtUsersPermissionRegistrations;
438
+ }
439
+ function toSnakeCase$1(value) {
440
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
441
+ }
442
+ function pluralize(word) {
443
+ if (word.endsWith("y") && !/[aeiou]y$/i.test(word)) {
444
+ return `${word.slice(0, -1)}ies`;
445
+ }
446
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
447
+ return `${word}es`;
448
+ }
449
+ return `${word}s`;
450
+ }
451
+ function deriveCollectionName(className) {
452
+ return pluralize(toSnakeCase$1(className));
453
+ }
454
+ function humanizeResource(resource) {
455
+ return resource.replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
456
+ }
457
+ function capitalize(value) {
458
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
459
+ }
460
+ function defaultPermissionName(slug) {
461
+ const parsed = parsePermissionSlug(slug);
462
+ if (!parsed.isValid) {
463
+ return humanizeResource(slug);
464
+ }
465
+ return `${capitalize(parsed.action)} ${humanizeResource(parsed.resource)}`;
466
+ }
467
+ function defaultPermissionDescription(slug) {
468
+ const parsed = parsePermissionSlug(slug);
469
+ if (!parsed.isValid) {
470
+ return `Allows ${slug}`;
471
+ }
472
+ return `Allows ${parsed.action} access for ${humanizeResource(parsed.resource).toLowerCase()}`;
473
+ }
474
+ function isCollectionManifestEntry(objectDef) {
475
+ return objectDef?.extends === "SmrtCollection" || objectDef?.extendsTypeArg !== void 0;
476
+ }
477
+ function getPublicCustomMethodNames(methodEntries, standardActions) {
478
+ return Array.from(
479
+ new Set(
480
+ methodEntries.filter(
481
+ (method) => Boolean(method?.name) && method?.isPublic === true && !standardActions.includes(method.name)
482
+ ).map((method) => method.name)
483
+ )
484
+ );
485
+ }
486
+ function getCustomMethodExposureNames(config, availableCustomMethods) {
487
+ if (!config || config === false) {
488
+ return /* @__PURE__ */ new Set();
489
+ }
490
+ if (config === true || typeof config !== "object") {
491
+ return new Set(availableCustomMethods);
492
+ }
493
+ const rawInclude = config.include;
494
+ const include = Array.isArray(rawInclude) ? [...rawInclude] : void 0;
495
+ const rawExclude = config.exclude;
496
+ const exclude = Array.isArray(rawExclude) ? [...rawExclude] : [];
497
+ if (!include) {
498
+ return new Set(
499
+ availableCustomMethods.filter(
500
+ (methodName) => !exclude.includes(methodName)
501
+ )
502
+ );
503
+ }
504
+ const baseMethods = include.filter(
505
+ (methodName) => availableCustomMethods.includes(methodName)
506
+ );
507
+ return new Set(
508
+ baseMethods.filter((methodName) => !exclude.includes(methodName))
509
+ );
510
+ }
511
+ function isOperationEnabled(config, action) {
512
+ if (config === false) {
513
+ return false;
514
+ }
515
+ if (config && typeof config === "object") {
516
+ const include = Array.isArray(config.include) ? config.include : void 0;
517
+ const rawExclude = config.exclude;
518
+ const exclude = Array.isArray(rawExclude) ? [...rawExclude] : [];
519
+ if (include && !include.includes(action)) {
520
+ return false;
521
+ }
522
+ if (exclude.includes(action)) {
523
+ return false;
524
+ }
525
+ }
526
+ return true;
527
+ }
528
+ function normalizePostgresAction(action) {
529
+ const normalized = action.toUpperCase();
530
+ if (normalized === "SELECT" || normalized === "INSERT" || normalized === "UPDATE" || normalized === "DELETE") {
531
+ return normalized;
532
+ }
533
+ throw new Error(
534
+ `Unsupported Postgres permission action '${action}'. Expected SELECT, INSERT, UPDATE, or DELETE.`
535
+ );
536
+ }
537
+ function normalizeBinding(binding, fallbackPermission) {
538
+ return {
539
+ action: normalizePostgresAction(binding.action),
540
+ permission: binding.permission || fallbackPermission,
541
+ schemaName: binding.schemaName,
542
+ tableName: binding.tableName,
543
+ tenantField: binding.tenantField
544
+ };
545
+ }
546
+ function mergeStringField(fieldName, existing, incoming, slug) {
547
+ const existingValue = existing[fieldName];
548
+ const incomingValue = incoming[fieldName];
549
+ if (!incomingValue) {
550
+ return;
551
+ }
552
+ if (!existingValue) {
553
+ existing[fieldName] = incomingValue;
554
+ return;
555
+ }
556
+ if (existingValue !== incomingValue) {
557
+ throw new Error(
558
+ `Conflicting permission metadata for '${slug}' field '${fieldName}': '${existingValue}' !== '${incomingValue}'`
559
+ );
560
+ }
561
+ }
562
+ function mergeBindings(existing, incoming) {
563
+ const existingBindings = existing.postgres?.bindings ?? [];
564
+ const incomingBindings = incoming.postgres?.bindings ?? [];
565
+ if (incomingBindings.length === 0) {
566
+ return;
567
+ }
568
+ const seen = new Set(
569
+ existingBindings.map(
570
+ (binding) => [
571
+ binding.permission,
572
+ binding.action,
573
+ binding.schemaName ?? "",
574
+ binding.tableName,
575
+ binding.tenantField ?? ""
576
+ ].join("|")
577
+ )
578
+ );
579
+ const mergedBindings = [...existingBindings];
580
+ for (const binding of incomingBindings) {
581
+ const normalized = normalizeBinding(binding, incoming.slug);
582
+ const key = [
583
+ normalized.permission,
584
+ normalized.action,
585
+ normalized.schemaName ?? "",
586
+ normalized.tableName,
587
+ normalized.tenantField ?? ""
588
+ ].join("|");
589
+ if (!seen.has(key)) {
590
+ seen.add(key);
591
+ mergedBindings.push(normalized);
592
+ }
593
+ }
594
+ existing.postgres = {
595
+ bindings: mergedBindings
596
+ };
597
+ }
598
+ function normalizeDefinition(definition, source) {
599
+ if (!definition.slug || !isValidPermissionSlug(definition.slug.trim())) {
600
+ throw new Error(
601
+ `Invalid permission slug '${definition.slug}'. Expected 'resource.action'.`
602
+ );
603
+ }
604
+ const slug = definition.slug.trim();
605
+ return {
606
+ category: definition.category ?? parsePermissionSlug(slug).resource,
607
+ className: definition.className,
608
+ collection: definition.collection,
609
+ description: definition.description ?? defaultPermissionDescription(slug),
610
+ name: definition.name ?? defaultPermissionName(slug),
611
+ postgres: definition.postgres?.bindings ? {
612
+ bindings: definition.postgres.bindings.map(
613
+ (binding) => normalizeBinding(binding, slug)
614
+ )
615
+ } : void 0,
616
+ qualifiedName: definition.qualifiedName,
617
+ slug,
618
+ source
619
+ };
620
+ }
621
+ function mergeDefinitionSet(current, incomingDefinitions, source) {
622
+ for (const rawDefinition of incomingDefinitions) {
623
+ const definition = normalizeDefinition(rawDefinition, source);
624
+ const existing = current.get(definition.slug);
625
+ if (!existing) {
626
+ current.set(definition.slug, definition);
627
+ continue;
628
+ }
629
+ mergeStringField("category", existing, definition, definition.slug);
630
+ mergeStringField("className", existing, definition, definition.slug);
631
+ mergeStringField("collection", existing, definition, definition.slug);
632
+ mergeStringField("description", existing, definition, definition.slug);
633
+ mergeStringField("name", existing, definition, definition.slug);
634
+ mergeStringField("qualifiedName", existing, definition, definition.slug);
635
+ mergeBindings(existing, definition);
636
+ }
637
+ }
638
+ function registerPermissionDefinitions(definitions) {
639
+ globalThis.__smrtUsersPermissionRegistrationCounter = (globalThis.__smrtUsersPermissionRegistrationCounter ?? 0) + 1;
640
+ const registrationId = globalThis.__smrtUsersPermissionRegistrationCounter;
641
+ getRuntimePermissionRegistrations().set(registrationId, definitions);
642
+ return () => {
643
+ getRuntimePermissionRegistrations().delete(registrationId);
644
+ };
645
+ }
646
+ class PermissionCatalogService {
647
+ constructor(options = {}) {
648
+ this.options = options;
649
+ }
650
+ options;
651
+ getUsersConfig() {
652
+ return getPackageConfig("users", {});
653
+ }
654
+ getRuntimePermissionDefinitions() {
655
+ return Array.from(getRuntimePermissionRegistrations().values()).flat();
656
+ }
657
+ getCustomPermissionDefinitions() {
658
+ return this.getUsersConfig().permissions?.custom ?? [];
659
+ }
660
+ getCatalog() {
661
+ const manifestPermissions = this.getManifestPermissionDefinitions();
662
+ const customPermissions = this.getCustomPermissionDefinitions();
663
+ const runtimePermissions = this.getRuntimePermissionDefinitions();
664
+ const merged = /* @__PURE__ */ new Map();
665
+ mergeDefinitionSet(merged, manifestPermissions, "manifest");
666
+ mergeDefinitionSet(merged, customPermissions, "config");
667
+ mergeDefinitionSet(merged, runtimePermissions, "runtime");
668
+ return {
669
+ customPermissions: customPermissions.map(
670
+ (definition) => normalizeDefinition(definition, "config")
671
+ ),
672
+ manifestPermissions: manifestPermissions.map(
673
+ (definition) => normalizeDefinition(definition, "manifest")
674
+ ),
675
+ permissions: Array.from(merged.values()).sort(
676
+ (left, right) => left.slug.localeCompare(right.slug)
677
+ ),
678
+ runtimePermissions: runtimePermissions.map(
679
+ (definition) => normalizeDefinition(definition, "runtime")
680
+ )
681
+ };
682
+ }
683
+ async syncPermissionCatalog() {
684
+ const catalog = this.getCatalog();
685
+ const permissions = await PermissionCollection.create(this.options);
686
+ const created = [];
687
+ const unchanged = [];
688
+ const updated = [];
689
+ for (const definition of catalog.permissions) {
690
+ const existing = await permissions.findBySlug(definition.slug);
691
+ if (!existing) {
692
+ const permission = await permissions.create({
693
+ category: definition.category ?? parsePermissionSlug(definition.slug).resource,
694
+ description: definition.description ?? "",
695
+ name: definition.name ?? definition.slug,
696
+ slug: definition.slug
697
+ });
698
+ await permission.save();
699
+ created.push(definition.slug);
700
+ continue;
701
+ }
702
+ const nextName = definition.name ?? existing.name;
703
+ const nextDescription = definition.description ?? existing.description;
704
+ const nextCategory = definition.category ?? existing.category;
705
+ if (existing.name === nextName && existing.description === nextDescription && existing.category === nextCategory) {
706
+ unchanged.push(definition.slug);
707
+ continue;
708
+ }
709
+ existing.name = nextName;
710
+ existing.description = nextDescription;
711
+ existing.category = nextCategory;
712
+ await existing.save();
713
+ updated.push(definition.slug);
714
+ }
715
+ return {
716
+ catalog,
717
+ created,
718
+ unchanged,
719
+ updated
720
+ };
721
+ }
722
+ getManifestPermissionDefinitions() {
723
+ const standardActions = ["list", "get", "create", "update", "delete"];
724
+ const definitions = /* @__PURE__ */ new Map();
725
+ for (const metadata of ObjectRegistry.getAllObjectMetadata()) {
726
+ const registered = ObjectRegistry.getClassByConstructor(metadata.constructor) ?? ObjectRegistry.getClass(metadata.name);
727
+ const manifestEntry = registered?.qualifiedName ? findManifestEntryByQualifiedName(registered.qualifiedName) : void 0;
728
+ if (isCollectionManifestEntry(manifestEntry)) {
729
+ continue;
730
+ }
731
+ const className = metadata.name;
732
+ const qualifiedName = registered?.qualifiedName;
733
+ const objectConfig = manifestEntry?.decoratorConfig ?? metadata.config;
734
+ const rawCollection = objectConfig?.collection;
735
+ const configuredCollection = typeof rawCollection === "string" && rawCollection.length > 0 ? rawCollection : void 0;
736
+ const collection = configuredCollection ?? manifestEntry?.collection ?? deriveCollectionName(metadata.name);
737
+ const readExposed = isOperationEnabled(objectConfig.api, "list") || isOperationEnabled(objectConfig.api, "get") || isOperationEnabled(objectConfig.cli, "list") || isOperationEnabled(objectConfig.cli, "get") || isOperationEnabled(objectConfig.mcp, "list") || isOperationEnabled(objectConfig.mcp, "get");
738
+ if (readExposed) {
739
+ definitions.set(`${collection}.read`, {
740
+ className,
741
+ collection,
742
+ qualifiedName,
743
+ slug: `${collection}.read`
744
+ });
745
+ }
746
+ for (const action of ["create", "update", "delete"]) {
747
+ const exposed = isOperationEnabled(objectConfig.api, action) || isOperationEnabled(objectConfig.cli, action) || isOperationEnabled(objectConfig.mcp, action);
748
+ if (!exposed) {
749
+ continue;
750
+ }
751
+ definitions.set(`${collection}.${action}`, {
752
+ className,
753
+ collection,
754
+ qualifiedName,
755
+ slug: `${collection}.${action}`
756
+ });
757
+ }
758
+ const methodEntries = manifestEntry?.methods ? Object.values(manifestEntry.methods) : Array.from(metadata.methods.values());
759
+ const publicCustomMethodNames = getPublicCustomMethodNames(
760
+ methodEntries,
761
+ standardActions
762
+ );
763
+ const customApiMethods = /* @__PURE__ */ new Set();
764
+ const customCliMethods = getCustomMethodExposureNames(
765
+ objectConfig.cli,
766
+ publicCustomMethodNames
767
+ );
768
+ const customMcpMethods = getCustomMethodExposureNames(
769
+ objectConfig.mcp,
770
+ publicCustomMethodNames
771
+ );
772
+ for (const methodName of publicCustomMethodNames) {
773
+ if (isOperationEnabled(objectConfig.api, methodName)) {
774
+ customApiMethods.add(methodName);
775
+ }
776
+ }
777
+ const customMethods = /* @__PURE__ */ new Set([
778
+ ...customApiMethods,
779
+ ...customCliMethods,
780
+ ...customMcpMethods
781
+ ]);
782
+ for (const methodName of customMethods) {
783
+ definitions.set(`${collection}.${methodName}`, {
784
+ className,
785
+ collection,
786
+ description: `Allows ${methodName} on ${humanizeResource(collection).toLowerCase()}`,
787
+ name: `${capitalize(methodName)} ${humanizeResource(collection)}`,
788
+ qualifiedName,
789
+ slug: `${collection}.${methodName}`
790
+ });
791
+ }
792
+ }
793
+ return Array.from(definitions.values()).sort(
794
+ (left, right) => left.slug.localeCompare(right.slug)
795
+ );
796
+ }
797
+ static create(options = {}) {
798
+ return new PermissionCatalogService(options);
799
+ }
800
+ }
801
+ async function syncPermissionCatalog(options = {}) {
802
+ return PermissionCatalogService.create(options).syncPermissionCatalog();
803
+ }
804
+ function toSnakeCase(value) {
805
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
806
+ }
807
+ function quoteIdent(identifier) {
808
+ return `"${identifier.replaceAll('"', '""')}"`;
809
+ }
810
+ function quoteLiteral(value) {
811
+ return `'${value.replaceAll("'", "''")}'`;
812
+ }
813
+ function isProbablyPostgres(configDb, database) {
814
+ if (configDb && typeof configDb === "object" && !("query" in configDb) && "type" in configDb && configDb.type === "postgres") {
815
+ return true;
816
+ }
817
+ if (typeof database.url === "string" && database.url.startsWith("postgres")) {
818
+ return true;
819
+ }
820
+ return (database.constructor?.name || "").toLowerCase().includes("postgres");
821
+ }
822
+ function normalizePostgresPermissionAction(action) {
823
+ const normalized = action.toUpperCase();
824
+ if (normalized === "SELECT" || normalized === "INSERT" || normalized === "UPDATE" || normalized === "DELETE") {
825
+ return normalized;
826
+ }
827
+ throw new Error(
828
+ `Invalid Postgres permission binding action "${action}". Expected one of SELECT, INSERT, UPDATE, DELETE.`
829
+ );
830
+ }
831
+ function normalizePostgresPermissionBinding(binding, fallbackPermission) {
832
+ const tableName = binding.tableName?.trim();
833
+ if (!tableName) {
834
+ throw new Error(
835
+ "Postgres permission binding is missing a tableName value."
836
+ );
837
+ }
838
+ const permission = binding.permission ?? fallbackPermission;
839
+ if (!permission) {
840
+ throw new Error(
841
+ "Postgres permission binding is missing a permission value."
842
+ );
843
+ }
844
+ return {
845
+ action: normalizePostgresPermissionAction(binding.action),
846
+ permission,
847
+ schemaName: binding.schemaName,
848
+ tableName,
849
+ tenantField: binding.tenantField
850
+ };
851
+ }
852
+ function parseTableReference(binding) {
853
+ if (binding.tableName.includes(".")) {
854
+ const [schemaName, tableName] = binding.tableName.split(".", 2);
855
+ return {
856
+ schemaName,
857
+ tableName
858
+ };
859
+ }
860
+ return {
861
+ schemaName: binding.schemaName ?? "public",
862
+ tableName: binding.tableName
863
+ };
864
+ }
865
+ function buildPolicyName(tableName, action) {
866
+ const actionSegment = action.toLowerCase();
867
+ const hash = createHash("sha1").update(`${tableName}:${action}`).digest("hex").slice(0, 8);
868
+ const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
869
+ const prefix = "smrt_";
870
+ const separatorLength = 2;
871
+ const maxTableSegmentLength = 63 - prefix.length - actionSegment.length - hash.length - separatorLength;
872
+ const tableSegment = (sanitizedTable || "table").slice(
873
+ 0,
874
+ Math.max(maxTableSegmentLength, 1)
875
+ );
876
+ return `${prefix}${tableSegment}_${actionSegment}_${hash}`;
877
+ }
878
+ function buildPermissionExpression(permissionSlugs) {
879
+ if (permissionSlugs.length === 0) {
880
+ return "FALSE";
881
+ }
882
+ return permissionSlugs.map((permission) => `smrt_has_permission(${quoteLiteral(permission)})`).join(" OR ");
883
+ }
884
+ function buildTenantMatchExpression(tenantField) {
885
+ return `${quoteIdent(tenantField)}::text = smrt_current_tenant_id()`;
886
+ }
887
+ function buildSelectPolicySql(target, permissions) {
888
+ const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;
889
+ const policyName = buildPolicyName(target.tableName, "SELECT");
890
+ const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;
891
+ return [
892
+ `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,
893
+ `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR SELECT USING (${condition})`
894
+ ];
895
+ }
896
+ function buildInsertPolicySql(target, permissions) {
897
+ const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;
898
+ const policyName = buildPolicyName(target.tableName, "INSERT");
899
+ const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;
900
+ return [
901
+ `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,
902
+ `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR INSERT WITH CHECK (${condition})`
903
+ ];
904
+ }
905
+ function buildUpdatePolicySql(target, permissions) {
906
+ const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;
907
+ const policyName = buildPolicyName(target.tableName, "UPDATE");
908
+ const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;
909
+ return [
910
+ `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,
911
+ `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR UPDATE USING (${condition}) WITH CHECK (${condition})`
912
+ ];
913
+ }
914
+ function buildDeletePolicySql(target, permissions) {
915
+ const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;
916
+ const policyName = buildPolicyName(target.tableName, "DELETE");
917
+ const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;
918
+ return [
919
+ `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,
920
+ `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR DELETE USING (${condition})`
921
+ ];
922
+ }
923
+ function buildHelperStatements() {
924
+ return [
925
+ [
926
+ "CREATE OR REPLACE FUNCTION smrt_rls_bypass()",
927
+ "RETURNS boolean",
928
+ "LANGUAGE sql",
929
+ "STABLE",
930
+ "AS $$",
931
+ " SELECT COALESCE(NULLIF(current_setting('smrt.system_context', true), ''), 'false')::boolean",
932
+ " OR COALESCE(NULLIF(current_setting('smrt.super_admin_bypass', true), ''), 'false')::boolean",
933
+ "$$"
934
+ ].join("\n"),
935
+ [
936
+ "CREATE OR REPLACE FUNCTION smrt_current_tenant_id()",
937
+ "RETURNS text",
938
+ "LANGUAGE sql",
939
+ "STABLE",
940
+ "AS $$",
941
+ " SELECT NULLIF(current_setting('smrt.tenant_id', true), '')",
942
+ "$$"
943
+ ].join("\n"),
944
+ [
945
+ "CREATE OR REPLACE FUNCTION smrt_has_permission(required_permission text)",
946
+ "RETURNS boolean",
947
+ "LANGUAGE sql",
948
+ "STABLE",
949
+ "AS $$",
950
+ " SELECT smrt_rls_bypass()",
951
+ " OR jsonb_exists(COALESCE(NULLIF(current_setting('smrt.permissions', true), ''), '[]')::jsonb, required_permission)",
952
+ "$$"
953
+ ].join("\n")
954
+ ];
955
+ }
956
+ function addBindingToTarget(targets, binding, source = {}) {
957
+ const { schemaName, tableName } = parseTableReference(binding);
958
+ const targetKey = `${schemaName}.${tableName}`;
959
+ const existing = targets.get(targetKey);
960
+ const tenantField = binding.tenantField ?? "tenant_id";
961
+ if (existing && existing.tenantField !== tenantField) {
962
+ throw new Error(
963
+ `Conflicting tenant fields for table '${targetKey}': '${existing.tenantField}' !== '${tenantField}'`
964
+ );
965
+ }
966
+ const target = existing ?? {
967
+ actions: /* @__PURE__ */ new Map(),
968
+ ...source,
969
+ schemaName,
970
+ tableName,
971
+ tenantField
972
+ };
973
+ const action = normalizePostgresPermissionAction(binding.action);
974
+ const permissions = target.actions.get(action) ?? /* @__PURE__ */ new Set();
975
+ if (binding.permission) {
976
+ permissions.add(binding.permission);
977
+ }
978
+ target.actions.set(action, permissions);
979
+ targets.set(targetKey, target);
980
+ }
981
+ function generatePostgresPermissionSql(options = {}) {
982
+ const catalogService = PermissionCatalogService.create(options);
983
+ const catalog = catalogService.getCatalog();
984
+ const config = catalogService.getUsersConfig();
985
+ const candidateTargets = /* @__PURE__ */ new Map();
986
+ const skipped = [];
987
+ const autoCandidates = /* @__PURE__ */ new Map();
988
+ for (const metadata of ObjectRegistry.getAllObjectMetadata()) {
989
+ const registered = ObjectRegistry.getClassByConstructor(metadata.constructor) ?? ObjectRegistry.getClass(metadata.name);
990
+ const tenantScoped = registered?.tenantScopedConfig;
991
+ const manifestEntry = registered?.qualifiedName ? findManifestEntryByQualifiedName(registered.qualifiedName) : void 0;
992
+ if (!tenantScoped) {
993
+ skipped.push({
994
+ className: metadata.name,
995
+ qualifiedName: registered?.qualifiedName,
996
+ reason: "not tenant-scoped"
997
+ });
998
+ continue;
999
+ }
1000
+ if (tenantScoped.mode !== "required") {
1001
+ skipped.push({
1002
+ className: metadata.name,
1003
+ qualifiedName: registered?.qualifiedName,
1004
+ reason: `tenant mode '${tenantScoped.mode}' is not supported for automatic Postgres RLS generation`
1005
+ });
1006
+ continue;
1007
+ }
1008
+ const rawTableName = registered?.schema?.tableName ?? manifestEntry?.schema?.tableName;
1009
+ if (!rawTableName) {
1010
+ skipped.push({
1011
+ className: metadata.name,
1012
+ qualifiedName: registered?.qualifiedName,
1013
+ reason: "no schema table name available"
1014
+ });
1015
+ continue;
1016
+ }
1017
+ const parsedTable = parseTableReference({
1018
+ tableName: rawTableName
1019
+ });
1020
+ const tableKey = `${parsedTable.schemaName}.${parsedTable.tableName}`;
1021
+ const objectConfig = manifestEntry?.decoratorConfig ?? metadata.config;
1022
+ const rawCollection = objectConfig?.collection;
1023
+ const configuredCollection = typeof rawCollection === "string" && rawCollection.length > 0 ? rawCollection : void 0;
1024
+ const collection = configuredCollection ?? manifestEntry?.collection ?? `${toSnakeCase(metadata.name)}s`;
1025
+ const entries = autoCandidates.get(tableKey) ?? [];
1026
+ entries.push({
1027
+ className: metadata.name,
1028
+ collection,
1029
+ qualifiedName: registered?.qualifiedName,
1030
+ schemaName: parsedTable.schemaName,
1031
+ tableName: parsedTable.tableName,
1032
+ tenantField: toSnakeCase(tenantScoped.field)
1033
+ });
1034
+ autoCandidates.set(tableKey, entries);
1035
+ }
1036
+ for (const [tableKey, entries] of autoCandidates) {
1037
+ if (entries.length > 1) {
1038
+ for (const entry2 of entries) {
1039
+ skipped.push({
1040
+ className: entry2.className,
1041
+ collection: entry2.collection,
1042
+ qualifiedName: entry2.qualifiedName,
1043
+ reason: `table '${tableKey}' is shared by multiple objects, so automatic policy generation was skipped`,
1044
+ schemaName: entry2.schemaName,
1045
+ tableName: entry2.tableName
1046
+ });
1047
+ }
1048
+ continue;
1049
+ }
1050
+ const entry = entries[0];
1051
+ addBindingToTarget(
1052
+ candidateTargets,
1053
+ {
1054
+ action: "SELECT",
1055
+ permission: `${entry.collection}.read`,
1056
+ schemaName: entry.schemaName,
1057
+ tableName: entry.tableName,
1058
+ tenantField: entry.tenantField
1059
+ },
1060
+ entry
1061
+ );
1062
+ addBindingToTarget(
1063
+ candidateTargets,
1064
+ {
1065
+ action: "INSERT",
1066
+ permission: `${entry.collection}.create`,
1067
+ schemaName: entry.schemaName,
1068
+ tableName: entry.tableName,
1069
+ tenantField: entry.tenantField
1070
+ },
1071
+ entry
1072
+ );
1073
+ addBindingToTarget(
1074
+ candidateTargets,
1075
+ {
1076
+ action: "UPDATE",
1077
+ permission: `${entry.collection}.update`,
1078
+ schemaName: entry.schemaName,
1079
+ tableName: entry.tableName,
1080
+ tenantField: entry.tenantField
1081
+ },
1082
+ entry
1083
+ );
1084
+ addBindingToTarget(
1085
+ candidateTargets,
1086
+ {
1087
+ action: "DELETE",
1088
+ permission: `${entry.collection}.delete`,
1089
+ schemaName: entry.schemaName,
1090
+ tableName: entry.tableName,
1091
+ tenantField: entry.tenantField
1092
+ },
1093
+ entry
1094
+ );
1095
+ }
1096
+ const explicitBindings = [];
1097
+ for (const definition of catalog.permissions) {
1098
+ for (const binding of definition.postgres?.bindings ?? []) {
1099
+ explicitBindings.push(
1100
+ normalizePostgresPermissionBinding(binding, definition.slug)
1101
+ );
1102
+ }
1103
+ }
1104
+ for (const binding of config.permissions?.postgres?.bindings ?? []) {
1105
+ explicitBindings.push(normalizePostgresPermissionBinding(binding));
1106
+ }
1107
+ for (const binding of explicitBindings) {
1108
+ addBindingToTarget(candidateTargets, binding);
1109
+ }
1110
+ const statements = [...buildHelperStatements()];
1111
+ const targets = Array.from(candidateTargets.values()).sort(
1112
+ (left, right) => `${left.schemaName}.${left.tableName}`.localeCompare(
1113
+ `${right.schemaName}.${right.tableName}`
1114
+ )
1115
+ ).map((target) => ({
1116
+ actions: Object.fromEntries(
1117
+ Array.from(target.actions.entries()).map(([action, permissions]) => [
1118
+ action,
1119
+ Array.from(permissions).sort()
1120
+ ])
1121
+ ),
1122
+ className: target.className,
1123
+ collection: target.collection,
1124
+ qualifiedName: target.qualifiedName,
1125
+ schemaName: target.schemaName,
1126
+ tableName: target.tableName,
1127
+ tenantField: target.tenantField
1128
+ }));
1129
+ for (const target of Array.from(candidateTargets.values())) {
1130
+ const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;
1131
+ statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY`);
1132
+ statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY`);
1133
+ const selectPermissions = Array.from(
1134
+ target.actions.get("SELECT") ?? []
1135
+ ).sort();
1136
+ const insertPermissions = Array.from(
1137
+ target.actions.get("INSERT") ?? []
1138
+ ).sort();
1139
+ const updatePermissions = Array.from(
1140
+ target.actions.get("UPDATE") ?? []
1141
+ ).sort();
1142
+ const deletePermissions = Array.from(
1143
+ target.actions.get("DELETE") ?? []
1144
+ ).sort();
1145
+ if (selectPermissions.length > 0) {
1146
+ statements.push(...buildSelectPolicySql(target, selectPermissions));
1147
+ }
1148
+ if (insertPermissions.length > 0) {
1149
+ statements.push(...buildInsertPolicySql(target, insertPermissions));
1150
+ }
1151
+ if (updatePermissions.length > 0) {
1152
+ statements.push(...buildUpdatePolicySql(target, updatePermissions));
1153
+ }
1154
+ if (deletePermissions.length > 0) {
1155
+ statements.push(...buildDeletePolicySql(target, deletePermissions));
1156
+ }
1157
+ }
1158
+ return {
1159
+ bindings: explicitBindings,
1160
+ skipped,
1161
+ sql: `${statements.join(";\n")};
1162
+ `,
1163
+ statements,
1164
+ targets
1165
+ };
1166
+ }
1167
+ async function applyPostgresPermissionPolicies(options = {}) {
1168
+ const permissions = await PermissionCollection.create(options);
1169
+ const databaseOptions = options.db ?? options.persistence;
1170
+ if (!isProbablyPostgres(databaseOptions, permissions.db)) {
1171
+ throw new Error(
1172
+ "applyPostgresPermissionPolicies() requires a Postgres database connection."
1173
+ );
1174
+ }
1175
+ const result = generatePostgresPermissionSql(options);
1176
+ for (const statement of result.statements) {
1177
+ try {
1178
+ await permissions.db.query(statement);
1179
+ } catch (error) {
1180
+ throw new Error(
1181
+ `Failed to apply Postgres permission policy statement:
1182
+ ${statement}`,
1183
+ {
1184
+ cause: error
1185
+ }
1186
+ );
1187
+ }
1188
+ }
1189
+ return result;
1190
+ }
1191
+ class TenantService {
1192
+ options;
1193
+ policy;
1194
+ tenantCollection;
1195
+ membershipCollection;
1196
+ roleCollection;
1197
+ constructor(options, policy) {
1198
+ this.options = options;
1199
+ this.policy = policy ?? DEFAULT_TENANT_POLICY;
1200
+ }
1201
+ /**
1202
+ * Initialize collections
1203
+ */
1204
+ async initialize() {
1205
+ this.tenantCollection = await TenantCollection.create(
1206
+ this.options
1207
+ );
1208
+ this.membershipCollection = await MembershipCollection.create(
1209
+ this.options
1210
+ );
1211
+ this.roleCollection = await RoleCollection.create(this.options);
1212
+ await this.roleCollection.seedSystemRoles();
1213
+ }
1214
+ /**
1215
+ * Get the current policy
1216
+ */
1217
+ getPolicy() {
1218
+ return { ...this.policy };
1219
+ }
1220
+ /**
1221
+ * Create a tenant and make the user the owner
1222
+ *
1223
+ * @param userId - The user to make owner
1224
+ * @param name - Tenant name
1225
+ * @param options - Optional slug override
1226
+ * @returns The created tenant and membership
1227
+ */
1228
+ async createTenantWithOwnership(userId, name, options) {
1229
+ if (!await this.canCreateTenant(userId)) {
1230
+ throw new Error(
1231
+ `User has reached maximum tenant limit (${this.policy.maxTenants})`
1232
+ );
1233
+ }
1234
+ const tenant = await this.tenantCollection.create({
1235
+ name,
1236
+ slug: options?.slug
1237
+ });
1238
+ await tenant.save();
1239
+ const ownerRole = await this.roleCollection.findBySlug(
1240
+ DEFAULT_ROLE_SLUGS.OWNER
1241
+ );
1242
+ if (!ownerRole) {
1243
+ throw new Error("Owner role not found - run seedSystemRoles first");
1244
+ }
1245
+ const membership = await this.membershipCollection.create({
1246
+ userId,
1247
+ tenantId: tenant.id,
1248
+ roleId: ownerRole.id,
1249
+ status: MembershipStatus.ACTIVE
1250
+ });
1251
+ await membership.save();
1252
+ return { tenant, membership };
1253
+ }
1254
+ /**
1255
+ * Check if a user can create a new tenant
1256
+ *
1257
+ * Returns false if maxTenants limit is reached (0 = unlimited)
1258
+ */
1259
+ async canCreateTenant(userId) {
1260
+ if (this.policy.maxTenants === 0) {
1261
+ return true;
1262
+ }
1263
+ const ownerRole = await this.roleCollection.findBySlug(
1264
+ DEFAULT_ROLE_SLUGS.OWNER
1265
+ );
1266
+ if (!ownerRole) {
1267
+ return false;
1268
+ }
1269
+ const memberships = await this.membershipCollection.findActiveByUser(userId);
1270
+ const ownedCount = memberships.filter(
1271
+ (m2) => m2.roleId === ownerRole.id
1272
+ ).length;
1273
+ return ownedCount < this.policy.maxTenants;
1274
+ }
1275
+ /**
1276
+ * Get a specific error message explaining why a tenant cannot be deleted.
1277
+ *
1278
+ * @returns Error message if deletion is not allowed, or null if allowed.
1279
+ */
1280
+ async getDeleteTenantError(userId, tenantId) {
1281
+ const membership = await this.membershipCollection.findByUserAndTenant(
1282
+ userId,
1283
+ tenantId
1284
+ );
1285
+ if (!membership || membership.status !== MembershipStatus.ACTIVE) {
1286
+ return "You are not a member of this tenant or it does not exist.";
1287
+ }
1288
+ const ownerRole = await this.roleCollection.findBySlug(
1289
+ DEFAULT_ROLE_SLUGS.OWNER
1290
+ );
1291
+ if (!ownerRole) {
1292
+ return "Owner role is not configured. Cannot determine deletion permissions.";
1293
+ }
1294
+ if (membership.roleId !== ownerRole.id) {
1295
+ return "Only the tenant owner can delete this tenant.";
1296
+ }
1297
+ if (this.policy.mode === "required") {
1298
+ const allMemberships = await this.membershipCollection.findActiveByUser(userId);
1299
+ const ownerMemberships = allMemberships.filter(
1300
+ (m2) => m2.roleId === ownerRole.id
1301
+ );
1302
+ if (ownerMemberships.length <= 1) {
1303
+ return "Cannot delete your last tenant. Policy requires at least one tenant.";
1304
+ }
1305
+ }
1306
+ return null;
1307
+ }
1308
+ /**
1309
+ * Check if a user can delete a specific tenant
1310
+ *
1311
+ * Returns false if:
1312
+ * - User is not an active member
1313
+ * - User is not the owner
1314
+ * - Policy is 'required' and this is the last tenant
1315
+ */
1316
+ async canDeleteTenant(userId, tenantId) {
1317
+ const error = await this.getDeleteTenantError(userId, tenantId);
1318
+ return error === null;
1319
+ }
1320
+ /**
1321
+ * Delete a tenant
1322
+ *
1323
+ * Note: This does not cascade delete related records (memberships, etc.).
1324
+ * The caller should handle cleanup of related data if needed.
1325
+ *
1326
+ * @throws Error if user cannot delete the tenant (with specific reason)
1327
+ */
1328
+ async deleteTenant(userId, tenantId) {
1329
+ const error = await this.getDeleteTenantError(userId, tenantId);
1330
+ if (error) {
1331
+ throw new Error(error);
1332
+ }
1333
+ const tenant = await this.tenantCollection.get(tenantId);
1334
+ if (tenant) {
1335
+ await tenant.delete();
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Ensure a tenant exists for the user based on policy
1340
+ *
1341
+ * Called during OIDC login to apply tenant policy:
1342
+ * - `flexible`: Returns first existing tenant or null (no auto-create)
1343
+ * - `personal`/`required`: Creates default tenant if none exists
1344
+ *
1345
+ * @param userId - The user ID
1346
+ * @param userInfo - User info for naming the auto-created tenant
1347
+ * @returns Tenant and membership (may be null in flexible mode)
1348
+ */
1349
+ async ensureTenantForUser(userId, userInfo) {
1350
+ const memberships = await this.membershipCollection.findActiveByUser(userId);
1351
+ if (memberships.length > 0) {
1352
+ const firstMembership = memberships[0];
1353
+ const tenant2 = await this.tenantCollection.get(
1354
+ firstMembership.tenantId
1355
+ );
1356
+ return {
1357
+ tenant: tenant2 ?? null,
1358
+ membership: firstMembership,
1359
+ created: false
1360
+ };
1361
+ }
1362
+ if (this.policy.mode === "flexible") {
1363
+ return {
1364
+ tenant: null,
1365
+ membership: null,
1366
+ created: false
1367
+ };
1368
+ }
1369
+ const tenantName = userInfo.name ? `${userInfo.name}'s Workspace` : this.policy.defaultName;
1370
+ const { tenant, membership } = await this.createTenantWithOwnership(
1371
+ userId,
1372
+ tenantName
1373
+ );
1374
+ return {
1375
+ tenant,
1376
+ membership,
1377
+ created: true
1378
+ };
1379
+ }
1380
+ /**
1381
+ * Get all tenants for a user (where they are owner)
1382
+ */
1383
+ async getOwnedTenants(userId) {
1384
+ const ownerRole = await this.roleCollection.findBySlug(
1385
+ DEFAULT_ROLE_SLUGS.OWNER
1386
+ );
1387
+ if (!ownerRole) {
1388
+ return [];
1389
+ }
1390
+ const memberships = await this.membershipCollection.findActiveByUser(userId);
1391
+ const ownerMemberships = memberships.filter(
1392
+ (m2) => m2.roleId === ownerRole.id
1393
+ );
1394
+ const tenantPromises = ownerMemberships.map(
1395
+ (m2) => this.tenantCollection.get(m2.tenantId)
1396
+ );
1397
+ const tenantsOrNull = await Promise.all(tenantPromises);
1398
+ return tenantsOrNull.filter((tenant) => tenant !== null);
1399
+ }
1400
+ /**
1401
+ * Static factory method
1402
+ */
1403
+ static async create(options, policy) {
1404
+ const service = new TenantService(options, policy);
1405
+ await service.initialize();
1406
+ return service;
1407
+ }
1408
+ }
1409
+ export {
1410
+ U as CliAuthRequest,
1411
+ d as CliAuthRequestCollection,
1412
+ e as DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS,
1413
+ f as DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS,
1414
+ g as DEFAULT_CLI_SESSION_TTL_SECONDS,
1415
+ DEFAULT_ROLES,
1416
+ DEFAULT_ROLE_SLUGS,
1417
+ h as DEFAULT_SESSION_TTL,
1418
+ DEFAULT_TENANT_POLICY,
1419
+ DEFAULT_TOKEN_EXPIRY_SECONDS,
1420
+ Group,
1421
+ GroupCollection,
1422
+ G as GroupMember,
1423
+ j as GroupMemberCollection,
1424
+ k as GroupRole,
1425
+ l as GroupRoleCollection,
1426
+ m as MAX_TENANT_HIERARCHY_DEPTH,
1427
+ MagicLinkError,
1428
+ MagicLinkService,
1429
+ UsersMagicLinkToken as MagicLinkToken,
1430
+ UsersMagicLinkTokenCollection as MagicLinkTokenCollection,
1431
+ o as Membership,
1432
+ MembershipCollection,
1433
+ q as MembershipOverride,
1434
+ r as MembershipOverrideCollection,
1435
+ MembershipStatus2 as MembershipStatus,
1436
+ O as OidcLoginError,
1437
+ s as OidcLoginService,
1438
+ OverrideEffect,
1439
+ t as Permission,
1440
+ PermissionCatalogService,
1441
+ PermissionCollection,
1442
+ u as PermissionResolver,
1443
+ Role,
1444
+ RoleCollection,
1445
+ R as RolePermission,
1446
+ v as RolePermissionCollection,
1447
+ S as Session,
1448
+ w as SessionCollection,
1449
+ x as SessionService,
1450
+ SessionStatus,
1451
+ y as Tenant,
1452
+ TenantCollection,
1453
+ z as TenantHierarchyError,
1454
+ TenantPermissionEffect,
1455
+ A as TenantPermissionOverride,
1456
+ B as TenantPermissionOverrideCollection,
1457
+ TenantService,
1458
+ TenantStatus,
1459
+ C as TerminalAuthError,
1460
+ E as TerminalAuthRateLimitError,
1461
+ F as TerminalAuthService,
1462
+ H as User,
1463
+ I as UserCollection,
1464
+ UserStatus,
1465
+ U2 as UsersCliAuthRequest,
1466
+ d2 as UsersCliAuthRequestCollection,
1467
+ UsersMagicLinkToken,
1468
+ UsersMagicLinkTokenCollection,
1469
+ applyPostgresPermissionPolicies,
1470
+ J as decodeOidcTransaction,
1471
+ K as encodeOidcTransaction,
1472
+ generatePostgresPermissionSql,
1473
+ L as generateSessionId,
1474
+ N as getCurrentSessionPermissionContext,
1475
+ Q as getRequestScopedDatabase,
1476
+ V as getUsersOidcConfig,
1477
+ registerPermissionDefinitions,
1478
+ W as resolveOidcProviderConfig,
1479
+ syncPermissionCatalog,
1480
+ X as withSessionPermissionContext
1481
+ };
1482
+ //# sourceMappingURL=index.js.map