@spfn/auth 0.1.0-alpha.0 → 0.1.0-alpha.86

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +320 -12
  3. package/dist/adapters/nextjs/api.d.ts +446 -0
  4. package/dist/adapters/nextjs/api.js +3279 -0
  5. package/dist/adapters/nextjs/api.js.map +1 -0
  6. package/dist/adapters/nextjs/server.d.ts +246 -0
  7. package/dist/adapters/nextjs/server.js +3645 -0
  8. package/dist/adapters/nextjs/server.js.map +1 -0
  9. package/dist/client.d.ts +2 -0
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/index.d.ts +14 -0
  13. package/dist/index.js +9098 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lib/api/auth-codes-verify.d.ts +37 -0
  16. package/dist/lib/api/auth-codes-verify.js +2949 -0
  17. package/dist/lib/api/auth-codes-verify.js.map +1 -0
  18. package/dist/lib/api/auth-codes.d.ts +37 -0
  19. package/dist/lib/api/auth-codes.js +2949 -0
  20. package/dist/lib/api/auth-codes.js.map +1 -0
  21. package/dist/lib/api/auth-exists.d.ts +38 -0
  22. package/dist/lib/api/auth-exists.js +2949 -0
  23. package/dist/lib/api/auth-exists.js.map +1 -0
  24. package/dist/lib/api/auth-invitations-accept.d.ts +38 -0
  25. package/dist/lib/api/auth-invitations-accept.js +2883 -0
  26. package/dist/lib/api/auth-invitations-accept.js.map +1 -0
  27. package/dist/lib/api/auth-invitations-cancel.d.ts +37 -0
  28. package/dist/lib/api/auth-invitations-cancel.js +2883 -0
  29. package/dist/lib/api/auth-invitations-cancel.js.map +1 -0
  30. package/dist/lib/api/auth-invitations-delete.d.ts +36 -0
  31. package/dist/lib/api/auth-invitations-delete.js +2883 -0
  32. package/dist/lib/api/auth-invitations-delete.js.map +1 -0
  33. package/dist/lib/api/auth-invitations-resend.d.ts +37 -0
  34. package/dist/lib/api/auth-invitations-resend.js +2883 -0
  35. package/dist/lib/api/auth-invitations-resend.js.map +1 -0
  36. package/dist/lib/api/auth-invitations.d.ts +109 -0
  37. package/dist/lib/api/auth-invitations.js +2887 -0
  38. package/dist/lib/api/auth-invitations.js.map +1 -0
  39. package/dist/lib/api/auth-keys-rotate.d.ts +37 -0
  40. package/dist/lib/api/auth-keys-rotate.js +2949 -0
  41. package/dist/lib/api/auth-keys-rotate.js.map +1 -0
  42. package/dist/lib/api/auth-login.d.ts +39 -0
  43. package/dist/lib/api/auth-login.js +2949 -0
  44. package/dist/lib/api/auth-login.js.map +1 -0
  45. package/dist/lib/api/auth-logout.d.ts +36 -0
  46. package/dist/lib/api/auth-logout.js +2949 -0
  47. package/dist/lib/api/auth-logout.js.map +1 -0
  48. package/dist/lib/api/auth-me.d.ts +50 -0
  49. package/dist/lib/api/auth-me.js +2949 -0
  50. package/dist/lib/api/auth-me.js.map +1 -0
  51. package/dist/lib/api/auth-password.d.ts +36 -0
  52. package/dist/lib/api/auth-password.js +2949 -0
  53. package/dist/lib/api/auth-password.js.map +1 -0
  54. package/dist/lib/api/auth-register.d.ts +38 -0
  55. package/dist/lib/api/auth-register.js +2949 -0
  56. package/dist/lib/api/auth-register.js.map +1 -0
  57. package/dist/lib/api/index.d.ts +356 -0
  58. package/dist/lib/api/index.js +3261 -0
  59. package/dist/lib/api/index.js.map +1 -0
  60. package/dist/lib/config.d.ts +70 -0
  61. package/dist/lib/config.js +64 -0
  62. package/dist/lib/config.js.map +1 -0
  63. package/dist/lib/contracts/auth.d.ts +302 -0
  64. package/dist/lib/contracts/auth.js +2951 -0
  65. package/dist/lib/contracts/auth.js.map +1 -0
  66. package/dist/lib/contracts/index.d.ts +3 -0
  67. package/dist/lib/contracts/index.js +3190 -0
  68. package/dist/lib/contracts/index.js.map +1 -0
  69. package/dist/lib/contracts/invitation.d.ts +243 -0
  70. package/dist/lib/contracts/invitation.js +2883 -0
  71. package/dist/lib/contracts/invitation.js.map +1 -0
  72. package/dist/lib/crypto.d.ts +76 -0
  73. package/dist/lib/crypto.js +127 -0
  74. package/dist/lib/crypto.js.map +1 -0
  75. package/dist/lib/index.d.ts +4 -0
  76. package/dist/lib/index.js +313 -0
  77. package/dist/lib/index.js.map +1 -0
  78. package/dist/lib/session.d.ts +68 -0
  79. package/dist/lib/session.js +126 -0
  80. package/dist/lib/session.js.map +1 -0
  81. package/dist/lib/types/api.d.ts +45 -0
  82. package/dist/lib/types/api.js +1 -0
  83. package/dist/lib/types/api.js.map +1 -0
  84. package/dist/lib/types/index.d.ts +3 -0
  85. package/dist/lib/types/index.js +2647 -0
  86. package/dist/lib/types/index.js.map +1 -0
  87. package/dist/lib/types/schemas.d.ts +45 -0
  88. package/dist/lib/types/schemas.js +2647 -0
  89. package/dist/lib/types/schemas.js.map +1 -0
  90. package/dist/lib.d.ts +2 -0
  91. package/dist/lib.js +1 -0
  92. package/dist/lib.js.map +1 -0
  93. package/dist/plugin.d.ts +12 -0
  94. package/dist/plugin.js +9081 -0
  95. package/dist/plugin.js.map +1 -0
  96. package/dist/server/entities/index.d.ts +11 -0
  97. package/dist/server/entities/index.js +395 -0
  98. package/dist/server/entities/index.js.map +1 -0
  99. package/dist/server/entities/invitations.d.ts +241 -0
  100. package/dist/server/entities/invitations.js +184 -0
  101. package/dist/server/entities/invitations.js.map +1 -0
  102. package/dist/server/entities/permissions.d.ts +196 -0
  103. package/dist/server/entities/permissions.js +49 -0
  104. package/dist/server/entities/permissions.js.map +1 -0
  105. package/dist/server/entities/role-permissions.d.ts +107 -0
  106. package/dist/server/entities/role-permissions.js +115 -0
  107. package/dist/server/entities/role-permissions.js.map +1 -0
  108. package/dist/server/entities/roles.d.ts +196 -0
  109. package/dist/server/entities/roles.js +50 -0
  110. package/dist/server/entities/roles.js.map +1 -0
  111. package/dist/server/entities/schema.d.ts +14 -0
  112. package/dist/server/entities/schema.js +7 -0
  113. package/dist/server/entities/schema.js.map +1 -0
  114. package/dist/server/entities/user-permissions.d.ts +163 -0
  115. package/dist/server/entities/user-permissions.js +193 -0
  116. package/dist/server/entities/user-permissions.js.map +1 -0
  117. package/dist/server/entities/user-public-keys.d.ts +227 -0
  118. package/dist/server/entities/user-public-keys.js +156 -0
  119. package/dist/server/entities/user-public-keys.js.map +1 -0
  120. package/dist/server/entities/user-social-accounts.d.ts +189 -0
  121. package/dist/server/entities/user-social-accounts.js +149 -0
  122. package/dist/server/entities/user-social-accounts.js.map +1 -0
  123. package/dist/server/entities/users.d.ts +235 -0
  124. package/dist/server/entities/users.js +117 -0
  125. package/dist/server/entities/users.js.map +1 -0
  126. package/dist/server/entities/verification-codes.d.ts +191 -0
  127. package/dist/server/entities/verification-codes.js +49 -0
  128. package/dist/server/entities/verification-codes.js.map +1 -0
  129. package/dist/server/routes/auth/index.d.ts +10 -0
  130. package/dist/server/routes/auth/index.js +4458 -0
  131. package/dist/server/routes/auth/index.js.map +1 -0
  132. package/dist/server/routes/index.d.ts +6 -0
  133. package/dist/server/routes/index.js +6582 -0
  134. package/dist/server/routes/index.js.map +1 -0
  135. package/dist/server/routes/invitations/index.d.ts +10 -0
  136. package/dist/server/routes/invitations/index.js +4395 -0
  137. package/dist/server/routes/invitations/index.js.map +1 -0
  138. package/dist/server.d.ts +1272 -0
  139. package/dist/server.js +2274 -0
  140. package/dist/server.js.map +1 -0
  141. package/migrations/0000_complex_swordsman.sql +167 -0
  142. package/migrations/meta/0000_snapshot.json +1397 -0
  143. package/migrations/meta/_journal.json +13 -0
  144. package/package.json +59 -24
package/dist/server.js ADDED
@@ -0,0 +1,2274 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/server/entities/schema.ts
12
+ import { createFunctionSchema } from "@spfn/core/db";
13
+ var authSchema;
14
+ var init_schema = __esm({
15
+ "src/server/entities/schema.ts"() {
16
+ "use strict";
17
+ authSchema = createFunctionSchema("@spfn/auth");
18
+ }
19
+ });
20
+
21
+ // src/server/entities/roles.ts
22
+ import { text, boolean, integer, index } from "drizzle-orm/pg-core";
23
+ import { id, timestamps } from "@spfn/core/db";
24
+ var roles;
25
+ var init_roles = __esm({
26
+ "src/server/entities/roles.ts"() {
27
+ "use strict";
28
+ init_schema();
29
+ roles = authSchema.table(
30
+ "roles",
31
+ {
32
+ // Primary key
33
+ id: id(),
34
+ // Role identifier (used in code, e.g., 'admin', 'editor')
35
+ // Must be unique, lowercase, kebab-case recommended
36
+ name: text("name").notNull().unique(),
37
+ // Display name for UI (e.g., 'Administrator', 'Content Editor')
38
+ displayName: text("display_name").notNull(),
39
+ // Role description
40
+ description: text("description"),
41
+ // Built-in role flag
42
+ // true: Core package roles (user, admin, superadmin) - cannot be deleted
43
+ // false: Custom or preset roles - can be deleted
44
+ isBuiltin: boolean("is_builtin").notNull().default(false),
45
+ // System role flag
46
+ // true: Defined in code (builtin or preset) - deletion restricted
47
+ // false: Runtime created custom role - fully manageable
48
+ isSystem: boolean("is_system").notNull().default(false),
49
+ // Active status
50
+ // false: Deactivated role (users cannot be assigned)
51
+ isActive: boolean("is_active").notNull().default(true),
52
+ // Priority level (higher = more privileged)
53
+ // superadmin: 100, admin: 80, user: 10
54
+ // Used for role hierarchy and conflict resolution
55
+ priority: integer("priority").notNull().default(10),
56
+ ...timestamps()
57
+ },
58
+ (table) => [
59
+ index("roles_name_idx").on(table.name),
60
+ index("roles_is_system_idx").on(table.isSystem),
61
+ index("roles_is_active_idx").on(table.isActive),
62
+ index("roles_is_builtin_idx").on(table.isBuiltin),
63
+ index("roles_priority_idx").on(table.priority)
64
+ ]
65
+ );
66
+ }
67
+ });
68
+
69
+ // src/server/entities/users.ts
70
+ import { text as text2, timestamp, check, boolean as boolean2, bigint, index as index2 } from "drizzle-orm/pg-core";
71
+ import { id as id2, timestamps as timestamps2 } from "@spfn/core/db";
72
+ import { sql } from "drizzle-orm";
73
+ var users;
74
+ var init_users = __esm({
75
+ "src/server/entities/users.ts"() {
76
+ "use strict";
77
+ init_roles();
78
+ init_schema();
79
+ users = authSchema.table(
80
+ "users",
81
+ {
82
+ // Identity
83
+ id: id2(),
84
+ // Email address (unique identifier)
85
+ // Used for: login, password reset, notifications
86
+ email: text2("email").unique(),
87
+ // Phone number in E.164 international format
88
+ // Format: +[country code][number] (e.g., +821012345678)
89
+ // Used for: SMS login, 2FA, notifications
90
+ phone: text2("phone").unique(),
91
+ // Authentication
92
+ // Bcrypt password hash ($2b$10$[salt][hash], 60 chars)
93
+ // Nullable to support OAuth-only accounts
94
+ passwordHash: text2("password_hash"),
95
+ // Force password change on next login
96
+ // Use cases: initial setup, security breach, policy violation
97
+ passwordChangeRequired: boolean2("password_change_required").notNull().default(false),
98
+ // Authorization (Role-Based Access Control)
99
+ // Foreign key to roles table
100
+ // References built-in roles: user (default), admin, superadmin
101
+ // Can also reference custom roles created at runtime
102
+ roleId: bigint("role_id", { mode: "number" }).references(() => roles.id).notNull(),
103
+ // Account status
104
+ // - active: Normal operation (default)
105
+ // - inactive: Deactivated (user request, dormant)
106
+ // - suspended: Locked (security incident, ToS violation)
107
+ status: text2(
108
+ "status",
109
+ {
110
+ enum: ["active", "inactive", "suspended"]
111
+ }
112
+ ).notNull().default("active"),
113
+ // Verification timestamps
114
+ // null = unverified, timestamp = verified at this time
115
+ // Email verification (via verification code or magic link)
116
+ emailVerifiedAt: timestamp("email_verified_at", { withTimezone: true }),
117
+ // Phone verification (via SMS OTP)
118
+ phoneVerifiedAt: timestamp("phone_verified_at", { withTimezone: true }),
119
+ // Metadata
120
+ // Last successful login timestamp
121
+ // Used for: security auditing, dormant account detection
122
+ lastLoginAt: timestamp("last_login_at", { withTimezone: true }),
123
+ ...timestamps2()
124
+ },
125
+ (table) => [
126
+ // Database constraints
127
+ // Ensure at least one identifier exists (email OR phone)
128
+ check(
129
+ "email_or_phone_check",
130
+ sql`${table.email} IS NOT NULL OR ${table.phone} IS NOT NULL`
131
+ ),
132
+ // Indexes for query optimization
133
+ index2("users_email_idx").on(table.email),
134
+ index2("users_phone_idx").on(table.phone),
135
+ index2("users_status_idx").on(table.status),
136
+ index2("users_role_id_idx").on(table.roleId)
137
+ ]
138
+ );
139
+ }
140
+ });
141
+
142
+ // src/server/entities/user-social-accounts.ts
143
+ import { text as text3, timestamp as timestamp2, uniqueIndex } from "drizzle-orm/pg-core";
144
+ import { id as id3, timestamps as timestamps3, foreignKey } from "@spfn/core/db";
145
+ var userSocialAccounts;
146
+ var init_user_social_accounts = __esm({
147
+ "src/server/entities/user-social-accounts.ts"() {
148
+ "use strict";
149
+ init_users();
150
+ init_schema();
151
+ userSocialAccounts = authSchema.table(
152
+ "user_social_accounts",
153
+ {
154
+ id: id3(),
155
+ // Foreign key to users
156
+ userId: foreignKey("user", () => users.id),
157
+ // Provider info
158
+ provider: text3(
159
+ "provider",
160
+ {
161
+ enum: ["google", "github", "kakao", "naver"]
162
+ }
163
+ ).notNull(),
164
+ providerUserId: text3("provider_user_id").notNull(),
165
+ providerEmail: text3("provider_email"),
166
+ // OAuth tokens (encrypted in production)
167
+ accessToken: text3("access_token"),
168
+ refreshToken: text3("refresh_token"),
169
+ tokenExpiresAt: timestamp2("token_expires_at", { withTimezone: true }),
170
+ ...timestamps3()
171
+ },
172
+ (table) => [
173
+ // Unique constraint: one provider account per provider
174
+ uniqueIndex("provider_user_unique_idx").on(table.provider, table.providerUserId)
175
+ ]
176
+ );
177
+ }
178
+ });
179
+
180
+ // src/server/entities/user-public-keys.ts
181
+ import { text as text4, timestamp as timestamp3, boolean as boolean3, index as index3 } from "drizzle-orm/pg-core";
182
+ import { id as id4, foreignKey as foreignKey2 } from "@spfn/core/db";
183
+ var userPublicKeys;
184
+ var init_user_public_keys = __esm({
185
+ "src/server/entities/user-public-keys.ts"() {
186
+ "use strict";
187
+ init_users();
188
+ init_schema();
189
+ userPublicKeys = authSchema.table(
190
+ "user_public_keys",
191
+ {
192
+ id: id4(),
193
+ // User reference
194
+ userId: foreignKey2("user", () => users.id),
195
+ // Key identification (client-generated UUID)
196
+ keyId: text4("key_id").notNull().unique(),
197
+ // Public key in Base64-encoded DER format (SPKI)
198
+ publicKey: text4("public_key").notNull(),
199
+ // Algorithm used (ES256 recommended, RS256 fallback)
200
+ algorithm: text4("algorithm", {
201
+ enum: ["ES256", "RS256"]
202
+ }).notNull().default("ES256"),
203
+ // Key fingerprint (SHA-256 hash for quick identification)
204
+ fingerprint: text4("fingerprint").notNull(),
205
+ // Key status
206
+ isActive: boolean3("is_active").notNull().default(true),
207
+ // Timestamps
208
+ createdAt: timestamp3("created_at", { mode: "date", withTimezone: true }).notNull().defaultNow(),
209
+ lastUsedAt: timestamp3("last_used_at", { mode: "date", withTimezone: true }),
210
+ expiresAt: timestamp3("expires_at", { mode: "date", withTimezone: true }),
211
+ // Revocation
212
+ revokedAt: timestamp3("revoked_at", { mode: "date", withTimezone: true }),
213
+ revokedReason: text4("revoked_reason")
214
+ },
215
+ (table) => [
216
+ index3("user_public_keys_user_id_idx").on(table.userId),
217
+ index3("user_public_keys_key_id_idx").on(table.keyId),
218
+ index3("user_public_keys_active_idx").on(table.isActive),
219
+ index3("user_public_keys_fingerprint_idx").on(table.fingerprint)
220
+ ]
221
+ );
222
+ }
223
+ });
224
+
225
+ // src/server/entities/verification-codes.ts
226
+ import { text as text5, timestamp as timestamp4, index as index4 } from "drizzle-orm/pg-core";
227
+ import { id as id5, timestamps as timestamps4 } from "@spfn/core/db";
228
+ var verificationCodes;
229
+ var init_verification_codes = __esm({
230
+ "src/server/entities/verification-codes.ts"() {
231
+ "use strict";
232
+ init_schema();
233
+ verificationCodes = authSchema.table(
234
+ "verification_codes",
235
+ {
236
+ id: id5(),
237
+ // Target (email or phone)
238
+ target: text5("target").notNull(),
239
+ // Email address or E.164 phone number
240
+ targetType: text5(
241
+ "target_type",
242
+ {
243
+ enum: ["email", "phone"]
244
+ }
245
+ ).notNull(),
246
+ // Code
247
+ code: text5("code").notNull(),
248
+ // 6-digit code by default (configurable)
249
+ // Purpose
250
+ purpose: text5(
251
+ "purpose",
252
+ {
253
+ enum: ["registration", "login", "password_reset", "email_change", "phone_change"]
254
+ }
255
+ ).notNull(),
256
+ // Expiry
257
+ expiresAt: timestamp4("expires_at", { withTimezone: true }).notNull(),
258
+ // Usage tracking
259
+ usedAt: timestamp4("used_at", { withTimezone: true }),
260
+ attempts: text5("attempts").notNull().default("0"),
261
+ // Track failed verification attempts
262
+ ...timestamps4()
263
+ },
264
+ (table) => [
265
+ // Index for quick lookup by target and purpose
266
+ index4("target_purpose_idx").on(table.target, table.purpose, table.expiresAt)
267
+ ]
268
+ );
269
+ }
270
+ });
271
+
272
+ // src/server/entities/invitations.ts
273
+ import { text as text6, timestamp as timestamp5, bigint as bigint2, index as index5, jsonb } from "drizzle-orm/pg-core";
274
+ import { id as id6, timestamps as timestamps5 } from "@spfn/core/db";
275
+ var invitations;
276
+ var init_invitations = __esm({
277
+ "src/server/entities/invitations.ts"() {
278
+ "use strict";
279
+ init_roles();
280
+ init_users();
281
+ init_schema();
282
+ invitations = authSchema.table(
283
+ "user_invitations",
284
+ {
285
+ // Primary key
286
+ id: id6(),
287
+ // Target email address for the invitation
288
+ // Will become the user's email upon acceptance
289
+ email: text6("email").notNull(),
290
+ // Unique invitation token (UUID v4)
291
+ // Used in invitation URL: /auth/invite/{token}
292
+ // Single-use token that expires after acceptance
293
+ token: text6("token").notNull().unique(),
294
+ // Role to be assigned when invitation is accepted
295
+ // Foreign key to roles table
296
+ roleId: bigint2("role_id", { mode: "number" }).references(() => roles.id).notNull(),
297
+ // User who created this invitation
298
+ // Foreign key to users table
299
+ // Used for: audit trail, permission checks
300
+ invitedBy: bigint2("invited_by", { mode: "number" }).references(() => users.id).notNull(),
301
+ // Invitation status
302
+ // - pending: Invitation sent, awaiting acceptance
303
+ // - accepted: User accepted and account created
304
+ // - expired: Invitation expired (automatic)
305
+ // - cancelled: Invitation cancelled by admin
306
+ status: text6(
307
+ "status",
308
+ {
309
+ enum: ["pending", "accepted", "expired", "cancelled"]
310
+ }
311
+ ).notNull().default("pending"),
312
+ // Expiration timestamp (default: 7 days from creation)
313
+ // Invitation cannot be accepted after this time
314
+ // Background job should update status to 'expired'
315
+ expiresAt: timestamp5("expires_at", { withTimezone: true }).notNull(),
316
+ // Timestamp when invitation was accepted
317
+ // null = not yet accepted
318
+ // Used for: audit trail, analytics
319
+ acceptedAt: timestamp5("accepted_at", { withTimezone: true }),
320
+ // Timestamp when invitation was cancelled
321
+ // null = not cancelled
322
+ // Used for: audit trail
323
+ cancelledAt: timestamp5("cancelled_at", { withTimezone: true }),
324
+ // Additional metadata (JSONB)
325
+ // Use cases:
326
+ // - Custom welcome message
327
+ // - Onboarding instructions
328
+ // - Team/department assignment
329
+ // - Custom fields for app-specific data
330
+ // Example: { message: "Welcome!", department: "Engineering" }
331
+ metadata: jsonb("metadata"),
332
+ ...timestamps5()
333
+ },
334
+ (table) => [
335
+ // Indexes for query optimization
336
+ index5("invitations_token_idx").on(table.token),
337
+ index5("invitations_email_idx").on(table.email),
338
+ index5("invitations_status_idx").on(table.status),
339
+ index5("invitations_invited_by_idx").on(table.invitedBy),
340
+ index5("invitations_expires_at_idx").on(table.expiresAt),
341
+ // For cleanup jobs
342
+ index5("invitations_role_id_idx").on(table.roleId)
343
+ ]
344
+ );
345
+ }
346
+ });
347
+
348
+ // src/server/entities/permissions.ts
349
+ import { text as text7, boolean as boolean4, index as index6 } from "drizzle-orm/pg-core";
350
+ import { id as id7, timestamps as timestamps6 } from "@spfn/core/db";
351
+ var permissions;
352
+ var init_permissions = __esm({
353
+ "src/server/entities/permissions.ts"() {
354
+ "use strict";
355
+ init_schema();
356
+ permissions = authSchema.table(
357
+ "permissions",
358
+ {
359
+ // Primary key
360
+ id: id7(),
361
+ // Permission identifier (e.g., 'user:delete', 'post:publish')
362
+ // Format: resource:action or namespace:resource:action
363
+ // Must be unique
364
+ name: text7("name").notNull().unique(),
365
+ // Display name for UI
366
+ displayName: text7("display_name").notNull(),
367
+ // Permission description
368
+ description: text7("description"),
369
+ // Category for grouping (e.g., 'user', 'post', 'admin', 'system')
370
+ category: text7("category"),
371
+ // Built-in permission flag
372
+ // true: Core package permissions - cannot be deleted
373
+ // false: Custom or preset permissions
374
+ isBuiltin: boolean4("is_builtin").notNull().default(false),
375
+ // System permission flag
376
+ // true: Defined in code (builtin or preset)
377
+ // false: Runtime created custom permission
378
+ isSystem: boolean4("is_system").notNull().default(false),
379
+ // Active status
380
+ // false: Deactivated permission (not enforced)
381
+ isActive: boolean4("is_active").notNull().default(true),
382
+ ...timestamps6()
383
+ },
384
+ (table) => [
385
+ index6("permissions_name_idx").on(table.name),
386
+ index6("permissions_category_idx").on(table.category),
387
+ index6("permissions_is_system_idx").on(table.isSystem),
388
+ index6("permissions_is_active_idx").on(table.isActive),
389
+ index6("permissions_is_builtin_idx").on(table.isBuiltin)
390
+ ]
391
+ );
392
+ }
393
+ });
394
+
395
+ // src/server/entities/role-permissions.ts
396
+ import { bigint as bigint3, index as index7, unique } from "drizzle-orm/pg-core";
397
+ import { id as id8, timestamps as timestamps7 } from "@spfn/core/db";
398
+ var rolePermissions;
399
+ var init_role_permissions = __esm({
400
+ "src/server/entities/role-permissions.ts"() {
401
+ "use strict";
402
+ init_roles();
403
+ init_permissions();
404
+ init_schema();
405
+ rolePermissions = authSchema.table(
406
+ "role_permissions",
407
+ {
408
+ // Primary key
409
+ id: id8(),
410
+ // Foreign key to roles table
411
+ roleId: bigint3("role_id", { mode: "number" }).notNull().references(() => roles.id, { onDelete: "cascade" }),
412
+ // Foreign key to permissions table
413
+ permissionId: bigint3("permission_id", { mode: "number" }).notNull().references(() => permissions.id, { onDelete: "cascade" }),
414
+ ...timestamps7()
415
+ },
416
+ (table) => [
417
+ // Indexes for query performance
418
+ index7("role_permissions_role_id_idx").on(table.roleId),
419
+ index7("role_permissions_permission_id_idx").on(table.permissionId),
420
+ // Unique constraint: one role-permission pair only
421
+ unique("role_permissions_unique").on(table.roleId, table.permissionId)
422
+ ]
423
+ );
424
+ }
425
+ });
426
+
427
+ // src/server/entities/user-permissions.ts
428
+ import { bigint as bigint4, boolean as boolean5, text as text8, timestamp as timestamp6, index as index8, unique as unique2 } from "drizzle-orm/pg-core";
429
+ import { id as id9, timestamps as timestamps8 } from "@spfn/core/db";
430
+ var userPermissions;
431
+ var init_user_permissions = __esm({
432
+ "src/server/entities/user-permissions.ts"() {
433
+ "use strict";
434
+ init_users();
435
+ init_permissions();
436
+ init_schema();
437
+ userPermissions = authSchema.table(
438
+ "user_permissions",
439
+ {
440
+ // Primary key
441
+ id: id9(),
442
+ // Foreign key to users table
443
+ userId: bigint4("user_id", { mode: "number" }).notNull().references(() => users.id, { onDelete: "cascade" }),
444
+ // Foreign key to permissions table
445
+ permissionId: bigint4("permission_id", { mode: "number" }).notNull().references(() => permissions.id, { onDelete: "cascade" }),
446
+ // Grant or revoke
447
+ // true: Grant this permission (even if role doesn't have it)
448
+ // false: Revoke this permission (even if role has it)
449
+ granted: boolean5("granted").notNull().default(true),
450
+ // Reason for grant/revocation (audit trail)
451
+ reason: text8("reason"),
452
+ // Expiration timestamp (optional)
453
+ // null: Permanent override
454
+ // timestamp: Permission expires at this time
455
+ expiresAt: timestamp6("expires_at", { withTimezone: true }),
456
+ ...timestamps8()
457
+ },
458
+ (table) => [
459
+ // Indexes for query performance
460
+ index8("user_permissions_user_id_idx").on(table.userId),
461
+ index8("user_permissions_permission_id_idx").on(table.permissionId),
462
+ index8("user_permissions_expires_at_idx").on(table.expiresAt),
463
+ // Unique constraint: one user-permission pair only
464
+ unique2("user_permissions_unique").on(table.userId, table.permissionId)
465
+ ]
466
+ );
467
+ }
468
+ });
469
+
470
+ // src/server/entities/index.ts
471
+ var entities_exports = {};
472
+ __export(entities_exports, {
473
+ authSchema: () => authSchema,
474
+ invitations: () => invitations,
475
+ permissions: () => permissions,
476
+ rolePermissions: () => rolePermissions,
477
+ roles: () => roles,
478
+ userPermissions: () => userPermissions,
479
+ userPublicKeys: () => userPublicKeys,
480
+ userSocialAccounts: () => userSocialAccounts,
481
+ users: () => users,
482
+ verificationCodes: () => verificationCodes
483
+ });
484
+ var init_entities = __esm({
485
+ "src/server/entities/index.ts"() {
486
+ "use strict";
487
+ init_schema();
488
+ init_users();
489
+ init_user_social_accounts();
490
+ init_user_public_keys();
491
+ init_verification_codes();
492
+ init_invitations();
493
+ init_roles();
494
+ init_permissions();
495
+ init_role_permissions();
496
+ init_user_permissions();
497
+ }
498
+ });
499
+
500
+ // src/server/helpers/jwt.ts
501
+ var jwt_exports = {};
502
+ __export(jwt_exports, {
503
+ decodeToken: () => decodeToken,
504
+ generateToken: () => generateToken,
505
+ verifyClientToken: () => verifyClientToken,
506
+ verifyKeyFingerprint: () => verifyKeyFingerprint,
507
+ verifyToken: () => verifyToken
508
+ });
509
+ import jwt from "jsonwebtoken";
510
+ import crypto from "crypto";
511
+ function generateToken(payload) {
512
+ return jwt.sign(payload, JWT_SECRET, {
513
+ expiresIn: JWT_EXPIRES_IN
514
+ });
515
+ }
516
+ function verifyToken(token) {
517
+ return jwt.verify(token, JWT_SECRET);
518
+ }
519
+ function verifyClientToken(token, publicKeyB64, algorithm) {
520
+ try {
521
+ const publicKeyDER = Buffer.from(publicKeyB64, "base64");
522
+ const publicKeyObject = crypto.createPublicKey({
523
+ key: publicKeyDER,
524
+ format: "der",
525
+ type: "spki"
526
+ });
527
+ const decoded = jwt.verify(token, publicKeyObject, {
528
+ algorithms: [algorithm],
529
+ // Prevent algorithm confusion attacks
530
+ issuer: "spfn-client"
531
+ // Validate token issuer
532
+ });
533
+ if (typeof decoded === "string") {
534
+ throw new Error("Invalid token format: expected object payload");
535
+ }
536
+ return decoded;
537
+ } catch (error) {
538
+ if (error instanceof jwt.TokenExpiredError) {
539
+ throw new Error("Token has expired");
540
+ }
541
+ if (error instanceof jwt.JsonWebTokenError) {
542
+ throw new Error("Invalid token signature");
543
+ }
544
+ throw new Error(`Token verification failed: ${error instanceof Error ? error.message : "Unknown error"}`);
545
+ }
546
+ }
547
+ function decodeToken(token) {
548
+ try {
549
+ return jwt.decode(token);
550
+ } catch {
551
+ return null;
552
+ }
553
+ }
554
+ function verifyKeyFingerprint(publicKeyB64, expectedFingerprint) {
555
+ try {
556
+ const publicKeyDER = Buffer.from(publicKeyB64, "base64");
557
+ const fingerprint = crypto.createHash("sha256").update(publicKeyDER).digest("hex");
558
+ return fingerprint === expectedFingerprint;
559
+ } catch (error) {
560
+ console.error("Failed to verify key fingerprint:", error);
561
+ return false;
562
+ }
563
+ }
564
+ var JWT_SECRET, JWT_EXPIRES_IN;
565
+ var init_jwt = __esm({
566
+ "src/server/helpers/jwt.ts"() {
567
+ "use strict";
568
+ JWT_SECRET = process.env.SPFN_AUTH_JWT_SECRET || // New prefixed version (recommended)
569
+ process.env.JWT_SECRET || // Legacy fallback
570
+ "dev-secret-key-change-in-production";
571
+ JWT_EXPIRES_IN = process.env.SPFN_AUTH_JWT_EXPIRES_IN || // New prefixed version (recommended)
572
+ process.env.JWT_EXPIRES_IN || // Legacy fallback
573
+ "7d";
574
+ }
575
+ });
576
+
577
+ // src/server/services/role.service.ts
578
+ var role_service_exports = {};
579
+ __export(role_service_exports, {
580
+ addPermissionToRole: () => addPermissionToRole,
581
+ createRole: () => createRole,
582
+ deleteRole: () => deleteRole,
583
+ getAllRoles: () => getAllRoles,
584
+ getRoleByName: () => getRoleByName,
585
+ getRolePermissions: () => getRolePermissions,
586
+ removePermissionFromRole: () => removePermissionFromRole,
587
+ setRolePermissions: () => setRolePermissions,
588
+ updateRole: () => updateRole
589
+ });
590
+ import { getDatabase as getDatabase3 } from "@spfn/core/db";
591
+ import { eq as eq3, and as and3 } from "drizzle-orm";
592
+ async function createRole(data) {
593
+ const db = getDatabase3();
594
+ if (!db) {
595
+ throw new Error("[Auth] Database not initialized");
596
+ }
597
+ const existing = await db.select().from(roles).where(eq3(roles.name, data.name)).limit(1);
598
+ if (existing.length > 0) {
599
+ throw new Error(`Role with name '${data.name}' already exists`);
600
+ }
601
+ const [newRole] = await db.insert(roles).values({
602
+ name: data.name,
603
+ displayName: data.displayName,
604
+ description: data.description,
605
+ priority: data.priority ?? 10,
606
+ isSystem: false,
607
+ // Custom roles are never system roles
608
+ isBuiltin: false
609
+ }).returning();
610
+ if (data.permissionIds && data.permissionIds.length > 0) {
611
+ const mappings = data.permissionIds.map((permId) => ({
612
+ roleId: newRole.id,
613
+ permissionId: Number(permId)
614
+ }));
615
+ await db.insert(rolePermissions).values(mappings);
616
+ }
617
+ console.log(`[Auth] \u2705 Created custom role: ${data.name}`);
618
+ return newRole;
619
+ }
620
+ async function updateRole(roleId, data) {
621
+ const db = getDatabase3();
622
+ if (!db) {
623
+ throw new Error("[Auth] Database not initialized");
624
+ }
625
+ const roleIdNum = Number(roleId);
626
+ const [role] = await db.select().from(roles).where(eq3(roles.id, roleIdNum)).limit(1);
627
+ if (!role) {
628
+ throw new Error("Role not found");
629
+ }
630
+ if (role.isBuiltin && data.priority !== void 0) {
631
+ throw new Error("Cannot modify priority of built-in roles");
632
+ }
633
+ const [updated] = await db.update(roles).set(data).where(eq3(roles.id, roleIdNum)).returning();
634
+ return updated;
635
+ }
636
+ async function deleteRole(roleId) {
637
+ const db = getDatabase3();
638
+ if (!db) {
639
+ throw new Error("[Auth] Database not initialized");
640
+ }
641
+ const roleIdNum = Number(roleId);
642
+ const [role] = await db.select().from(roles).where(eq3(roles.id, roleIdNum)).limit(1);
643
+ if (!role) {
644
+ throw new Error("Role not found");
645
+ }
646
+ if (role.isBuiltin) {
647
+ throw new Error(`Cannot delete built-in role: ${role.name}`);
648
+ }
649
+ if (role.isSystem) {
650
+ throw new Error(`Cannot delete system role: ${role.name}. Deactivate it instead.`);
651
+ }
652
+ await db.delete(roles).where(eq3(roles.id, roleIdNum));
653
+ console.log(`[Auth] \u{1F5D1}\uFE0F Deleted role: ${role.name}`);
654
+ }
655
+ async function addPermissionToRole(roleId, permissionId) {
656
+ const db = getDatabase3();
657
+ if (!db) {
658
+ throw new Error("[Auth] Database not initialized");
659
+ }
660
+ const roleIdNum = Number(roleId);
661
+ const permissionIdNum = Number(permissionId);
662
+ const existing = await db.select().from(rolePermissions).where(
663
+ and3(
664
+ eq3(rolePermissions.roleId, roleIdNum),
665
+ eq3(rolePermissions.permissionId, permissionIdNum)
666
+ )
667
+ ).limit(1);
668
+ if (existing.length > 0) {
669
+ return;
670
+ }
671
+ await db.insert(rolePermissions).values({
672
+ roleId: roleIdNum,
673
+ permissionId: permissionIdNum
674
+ });
675
+ }
676
+ async function removePermissionFromRole(roleId, permissionId) {
677
+ const db = getDatabase3();
678
+ if (!db) {
679
+ throw new Error("[Auth] Database not initialized");
680
+ }
681
+ const roleIdNum = Number(roleId);
682
+ const permissionIdNum = Number(permissionId);
683
+ await db.delete(rolePermissions).where(
684
+ and3(
685
+ eq3(rolePermissions.roleId, roleIdNum),
686
+ eq3(rolePermissions.permissionId, permissionIdNum)
687
+ )
688
+ );
689
+ }
690
+ async function setRolePermissions(roleId, permissionIds) {
691
+ const db = getDatabase3();
692
+ if (!db) {
693
+ throw new Error("[Auth] Database not initialized");
694
+ }
695
+ const roleIdNum = Number(roleId);
696
+ await db.delete(rolePermissions).where(eq3(rolePermissions.roleId, roleIdNum));
697
+ if (permissionIds.length > 0) {
698
+ const mappings = permissionIds.map((permId) => ({
699
+ roleId: roleIdNum,
700
+ permissionId: Number(permId)
701
+ }));
702
+ await db.insert(rolePermissions).values(mappings);
703
+ }
704
+ }
705
+ async function getAllRoles(includeInactive = false) {
706
+ const db = getDatabase3();
707
+ if (!db) {
708
+ throw new Error("[Auth] Database not initialized");
709
+ }
710
+ const query = db.select().from(roles);
711
+ if (!includeInactive) {
712
+ return query.where(eq3(roles.isActive, true));
713
+ }
714
+ return query;
715
+ }
716
+ async function getRoleByName(name) {
717
+ const db = getDatabase3();
718
+ if (!db) {
719
+ throw new Error("[Auth] Database not initialized");
720
+ }
721
+ const [role] = await db.select().from(roles).where(eq3(roles.name, name)).limit(1);
722
+ return role || null;
723
+ }
724
+ async function getRolePermissions(roleId) {
725
+ const db = getDatabase3();
726
+ if (!db) {
727
+ throw new Error("[Auth] Database not initialized");
728
+ }
729
+ const roleIdNum = Number(roleId);
730
+ const perms = await db.select({ name: permissions.name }).from(rolePermissions).innerJoin(permissions, eq3(rolePermissions.permissionId, permissions.id)).where(eq3(rolePermissions.roleId, roleIdNum));
731
+ return perms.map((p) => p.name);
732
+ }
733
+ var init_role_service = __esm({
734
+ "src/server/services/role.service.ts"() {
735
+ "use strict";
736
+ init_entities();
737
+ }
738
+ });
739
+
740
+ // src/server/rbac/builtin.ts
741
+ var BUILTIN_ROLES = {
742
+ SUPERADMIN: {
743
+ name: "superadmin",
744
+ displayName: "Super Administrator",
745
+ description: "Full system access and RBAC management",
746
+ priority: 100,
747
+ isSystem: true,
748
+ isBuiltin: true
749
+ },
750
+ ADMIN: {
751
+ name: "admin",
752
+ displayName: "Administrator",
753
+ description: "User management and organization administration",
754
+ priority: 80,
755
+ isSystem: true,
756
+ isBuiltin: true
757
+ },
758
+ USER: {
759
+ name: "user",
760
+ displayName: "User",
761
+ description: "Default user role with basic permissions",
762
+ priority: 10,
763
+ isSystem: true,
764
+ isBuiltin: true
765
+ }
766
+ };
767
+ var BUILTIN_PERMISSIONS = {
768
+ // Self-service auth management
769
+ AUTH_SELF_MANAGE: {
770
+ name: "auth:self:manage",
771
+ displayName: "Manage Own Auth",
772
+ description: "Change own password, rotate keys, manage own sessions",
773
+ category: "auth",
774
+ isSystem: true,
775
+ isBuiltin: true
776
+ },
777
+ // User management (admin functions)
778
+ USER_READ: {
779
+ name: "user:read",
780
+ displayName: "Read Users",
781
+ description: "View user information and list users",
782
+ category: "user",
783
+ isSystem: true,
784
+ isBuiltin: true
785
+ },
786
+ USER_WRITE: {
787
+ name: "user:write",
788
+ displayName: "Write Users",
789
+ description: "Create and update user accounts",
790
+ category: "user",
791
+ isSystem: true,
792
+ isBuiltin: true
793
+ },
794
+ USER_DELETE: {
795
+ name: "user:delete",
796
+ displayName: "Delete Users",
797
+ description: "Delete user accounts",
798
+ category: "user",
799
+ isSystem: true,
800
+ isBuiltin: true
801
+ },
802
+ USER_INVITE: {
803
+ name: "user:invite",
804
+ displayName: "Invite Users",
805
+ description: "Create and send user invitations",
806
+ category: "user",
807
+ isSystem: true,
808
+ isBuiltin: true
809
+ },
810
+ // RBAC management (superadmin functions)
811
+ RBAC_ROLE_MANAGE: {
812
+ name: "rbac:role:manage",
813
+ displayName: "Manage Roles",
814
+ description: "Create, update, and delete roles",
815
+ category: "rbac",
816
+ isSystem: true,
817
+ isBuiltin: true
818
+ },
819
+ RBAC_PERMISSION_MANAGE: {
820
+ name: "rbac:permission:manage",
821
+ displayName: "Manage Permissions",
822
+ description: "Assign permissions to roles and users",
823
+ category: "rbac",
824
+ isSystem: true,
825
+ isBuiltin: true
826
+ }
827
+ };
828
+ var BUILTIN_ROLE_PERMISSIONS = {
829
+ superadmin: [
830
+ "auth:self:manage",
831
+ "user:read",
832
+ "user:write",
833
+ "user:delete",
834
+ "user:invite",
835
+ "rbac:role:manage",
836
+ "rbac:permission:manage"
837
+ ],
838
+ admin: [
839
+ "auth:self:manage",
840
+ "user:read",
841
+ "user:write",
842
+ "user:delete",
843
+ "user:invite"
844
+ ],
845
+ user: [
846
+ "auth:self:manage"
847
+ ]
848
+ };
849
+
850
+ // src/server/services/auth.service.ts
851
+ init_entities();
852
+ import { findOne as findOne2, create as create3 } from "@spfn/core/db";
853
+ import { ValidationError as ValidationError2 } from "@spfn/core/errors";
854
+
855
+ // src/server/helpers/password.ts
856
+ import bcrypt from "bcrypt";
857
+ var SALT_ROUNDS = parseInt(
858
+ process.env.SPFN_AUTH_BCRYPT_SALT_ROUNDS || // New prefixed version (recommended)
859
+ process.env.BCRYPT_SALT_ROUNDS || // Legacy fallback
860
+ "10",
861
+ 10
862
+ );
863
+ async function hashPassword(password) {
864
+ if (!password || password.length === 0) {
865
+ throw new Error("Password cannot be empty");
866
+ }
867
+ return bcrypt.hash(password, SALT_ROUNDS);
868
+ }
869
+ async function verifyPassword(password, hash) {
870
+ if (!password || password.length === 0) {
871
+ throw new Error("Password cannot be empty");
872
+ }
873
+ if (!hash || hash.length === 0) {
874
+ throw new Error("Hash cannot be empty");
875
+ }
876
+ return bcrypt.compare(password, hash);
877
+ }
878
+ function validatePasswordStrength(password) {
879
+ const errors = [];
880
+ if (password.length < 8) {
881
+ errors.push("Password must be at least 8 characters");
882
+ }
883
+ if (!/[A-Z]/.test(password)) {
884
+ errors.push("Password must contain at least one uppercase letter");
885
+ }
886
+ if (!/[a-z]/.test(password)) {
887
+ errors.push("Password must contain at least one lowercase letter");
888
+ }
889
+ if (!/[0-9]/.test(password)) {
890
+ errors.push("Password must contain at least one number");
891
+ }
892
+ if (!/[^A-Za-z0-9]/.test(password)) {
893
+ errors.push("Password must contain at least one special character");
894
+ }
895
+ return {
896
+ valid: errors.length === 0,
897
+ errors
898
+ };
899
+ }
900
+
901
+ // src/server/helpers/index.ts
902
+ init_jwt();
903
+
904
+ // src/server/helpers/verification.ts
905
+ init_verification_codes();
906
+ import jwt2 from "jsonwebtoken";
907
+ import { getDatabase, create } from "@spfn/core/db";
908
+ import { eq, and } from "drizzle-orm";
909
+ function getVerificationTokenSecret() {
910
+ const secret = process.env.SPFN_AUTH_VERIFICATION_TOKEN_SECRET || // New prefixed version (recommended)
911
+ process.env.VERIFICATION_TOKEN_SECRET || // Legacy fallback
912
+ process.env.SPFN_AUTH_JWT_SECRET || // New JWT secret fallback
913
+ process.env.JWT_SECRET;
914
+ if (!secret || secret.length < 32) {
915
+ throw new Error("SPFN_AUTH_VERIFICATION_TOKEN_SECRET must be at least 32 characters long");
916
+ }
917
+ return secret;
918
+ }
919
+ var VERIFICATION_TOKEN_EXPIRY = "15m";
920
+ var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
921
+ var MAX_VERIFICATION_ATTEMPTS = 5;
922
+ function generateVerificationCode() {
923
+ const code = Math.floor(Math.random() * 1e6).toString().padStart(6, "0");
924
+ return code;
925
+ }
926
+ async function storeVerificationCode(target, targetType, code, purpose) {
927
+ const db = getDatabase();
928
+ if (!db) {
929
+ throw new Error("Database not initialized");
930
+ }
931
+ const expiresAt = /* @__PURE__ */ new Date();
932
+ expiresAt.setMinutes(expiresAt.getMinutes() + VERIFICATION_CODE_EXPIRY_MINUTES);
933
+ const record = await create(verificationCodes, {
934
+ target,
935
+ targetType,
936
+ code,
937
+ purpose,
938
+ expiresAt,
939
+ attempts: "0"
940
+ });
941
+ return record;
942
+ }
943
+ async function validateVerificationCode(target, targetType, code, purpose) {
944
+ const db = getDatabase();
945
+ if (!db) {
946
+ throw new Error("Database not initialized");
947
+ }
948
+ const records = await db.select().from(verificationCodes).where(
949
+ and(
950
+ eq(verificationCodes.target, target),
951
+ eq(verificationCodes.targetType, targetType),
952
+ eq(verificationCodes.code, code),
953
+ eq(verificationCodes.purpose, purpose)
954
+ )
955
+ ).limit(1);
956
+ if (records.length === 0) {
957
+ return { valid: false, error: "Invalid verification code" };
958
+ }
959
+ const record = records[0];
960
+ if (record.usedAt) {
961
+ return { valid: false, error: "Verification code already used" };
962
+ }
963
+ if (/* @__PURE__ */ new Date() > new Date(record.expiresAt)) {
964
+ return { valid: false, error: "Verification code expired" };
965
+ }
966
+ const attempts = parseInt(record.attempts, 10);
967
+ if (attempts >= MAX_VERIFICATION_ATTEMPTS) {
968
+ return { valid: false, error: "Too many attempts, please request a new code" };
969
+ }
970
+ await db.update(verificationCodes).set({ attempts: (attempts + 1).toString() }).where(eq(verificationCodes.id, record.id));
971
+ return { valid: true, codeId: record.id };
972
+ }
973
+ async function markCodeAsUsed(codeId) {
974
+ const db = getDatabase();
975
+ if (!db) {
976
+ throw new Error("Database not initialized");
977
+ }
978
+ await db.update(verificationCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(eq(verificationCodes.id, codeId));
979
+ }
980
+ function createVerificationToken(payload) {
981
+ const secret = getVerificationTokenSecret();
982
+ return jwt2.sign(payload, secret, {
983
+ expiresIn: VERIFICATION_TOKEN_EXPIRY,
984
+ issuer: "spfn-auth",
985
+ audience: "spfn-client"
986
+ });
987
+ }
988
+ function validateVerificationToken(token) {
989
+ try {
990
+ const secret = getVerificationTokenSecret();
991
+ const decoded = jwt2.verify(token, secret, {
992
+ issuer: "spfn-auth",
993
+ audience: "spfn-client"
994
+ });
995
+ if (typeof decoded === "object" && decoded !== null && "target" in decoded && "targetType" in decoded && "purpose" in decoded && "codeId" in decoded) {
996
+ return decoded;
997
+ }
998
+ return null;
999
+ } catch (error) {
1000
+ console.error("[validateVerificationToken] Error:", error);
1001
+ return null;
1002
+ }
1003
+ }
1004
+ async function sendVerificationEmail(email, code, purpose) {
1005
+ console.log(`[VERIFICATION EMAIL] To: ${email}, Code: ${code}, Purpose: ${purpose}`);
1006
+ }
1007
+ async function sendVerificationSMS(phone, code, purpose) {
1008
+ console.log(`[VERIFICATION SMS] To: ${phone}, Code: ${code}, Purpose: ${purpose}`);
1009
+ }
1010
+
1011
+ // src/server/helpers/context.ts
1012
+ function getAuth(c) {
1013
+ if ("raw" in c && c.raw) {
1014
+ return c.raw.get("auth");
1015
+ }
1016
+ return c.get("auth");
1017
+ }
1018
+ function getUser(c) {
1019
+ return getAuth(c).user;
1020
+ }
1021
+ function getUserId(c) {
1022
+ return getAuth(c).userId;
1023
+ }
1024
+ function getKeyId(c) {
1025
+ return getAuth(c).keyId;
1026
+ }
1027
+
1028
+ // src/server/errors/auth-errors.ts
1029
+ import {
1030
+ ValidationError,
1031
+ UnauthorizedError,
1032
+ ForbiddenError,
1033
+ ConflictError
1034
+ } from "@spfn/core/errors";
1035
+ var InvalidCredentialsError = class extends UnauthorizedError {
1036
+ constructor(message = "Invalid credentials") {
1037
+ super(message);
1038
+ this.name = "InvalidCredentialsError";
1039
+ }
1040
+ };
1041
+ var InvalidTokenError = class extends UnauthorizedError {
1042
+ constructor(message = "Invalid authentication token") {
1043
+ super(message);
1044
+ this.name = "InvalidTokenError";
1045
+ }
1046
+ };
1047
+ var TokenExpiredError = class extends UnauthorizedError {
1048
+ constructor(message = "Authentication token has expired") {
1049
+ super(message);
1050
+ this.name = "TokenExpiredError";
1051
+ }
1052
+ };
1053
+ var KeyExpiredError = class extends UnauthorizedError {
1054
+ constructor(message = "Public key has expired") {
1055
+ super(message);
1056
+ this.name = "KeyExpiredError";
1057
+ }
1058
+ };
1059
+ var AccountDisabledError = class extends ForbiddenError {
1060
+ constructor(status = "disabled") {
1061
+ super(`Account is ${status}`, { details: { status } });
1062
+ this.name = "AccountDisabledError";
1063
+ }
1064
+ };
1065
+ var AccountAlreadyExistsError = class extends ConflictError {
1066
+ constructor(identifier, identifierType) {
1067
+ super("Account already exists", { details: { identifier, identifierType } });
1068
+ this.name = "AccountAlreadyExistsError";
1069
+ }
1070
+ };
1071
+ var InvalidVerificationCodeError = class extends ValidationError {
1072
+ constructor(reason = "Invalid verification code") {
1073
+ super(reason);
1074
+ this.name = "InvalidVerificationCodeError";
1075
+ }
1076
+ };
1077
+ var InvalidVerificationTokenError = class extends ValidationError {
1078
+ constructor(message = "Invalid or expired verification token") {
1079
+ super(message);
1080
+ this.name = "InvalidVerificationTokenError";
1081
+ }
1082
+ };
1083
+ var InvalidKeyFingerprintError = class extends ValidationError {
1084
+ constructor(message = "Invalid key fingerprint") {
1085
+ super(message);
1086
+ this.name = "InvalidKeyFingerprintError";
1087
+ }
1088
+ };
1089
+ var VerificationTokenPurposeMismatchError = class extends ValidationError {
1090
+ constructor(expected, actual) {
1091
+ super(`Verification token is for ${actual}, but ${expected} was expected`, { details: { expected, actual } });
1092
+ this.name = "VerificationTokenPurposeMismatchError";
1093
+ }
1094
+ };
1095
+ var VerificationTokenTargetMismatchError = class extends ValidationError {
1096
+ constructor() {
1097
+ super("Verification token does not match provided email/phone");
1098
+ this.name = "VerificationTokenTargetMismatchError";
1099
+ }
1100
+ };
1101
+
1102
+ // src/server/services/key.service.ts
1103
+ init_entities();
1104
+ init_jwt();
1105
+ import { create as create2, getDatabase as getDatabase2 } from "@spfn/core/db";
1106
+ import { eq as eq2, and as and2 } from "drizzle-orm";
1107
+ function getKeyExpiryDate() {
1108
+ const expiresAt = /* @__PURE__ */ new Date();
1109
+ expiresAt.setDate(expiresAt.getDate() + 90);
1110
+ return expiresAt;
1111
+ }
1112
+ async function registerPublicKeyService(params) {
1113
+ const { userId, keyId, publicKey, fingerprint, algorithm = "ES256" } = params;
1114
+ const isValidFingerprint = verifyKeyFingerprint(publicKey, fingerprint);
1115
+ if (!isValidFingerprint) {
1116
+ throw new InvalidKeyFingerprintError();
1117
+ }
1118
+ await create2(userPublicKeys, {
1119
+ userId,
1120
+ keyId,
1121
+ publicKey,
1122
+ algorithm,
1123
+ fingerprint,
1124
+ isActive: true,
1125
+ createdAt: /* @__PURE__ */ new Date(),
1126
+ expiresAt: getKeyExpiryDate()
1127
+ });
1128
+ }
1129
+ async function rotateKeyService(params) {
1130
+ const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm = "ES256" } = params;
1131
+ const isValidFingerprint = verifyKeyFingerprint(newPublicKey, fingerprint);
1132
+ if (!isValidFingerprint) {
1133
+ throw new InvalidKeyFingerprintError();
1134
+ }
1135
+ const db = getDatabase2();
1136
+ await db.update(userPublicKeys).set({
1137
+ isActive: false,
1138
+ revokedAt: /* @__PURE__ */ new Date(),
1139
+ revokedReason: "Replaced by key rotation"
1140
+ }).where(
1141
+ and2(
1142
+ eq2(userPublicKeys.keyId, oldKeyId),
1143
+ eq2(userPublicKeys.userId, userId)
1144
+ )
1145
+ );
1146
+ await create2(userPublicKeys, {
1147
+ userId,
1148
+ keyId: newKeyId,
1149
+ publicKey: newPublicKey,
1150
+ algorithm,
1151
+ fingerprint,
1152
+ isActive: true,
1153
+ createdAt: /* @__PURE__ */ new Date(),
1154
+ expiresAt: getKeyExpiryDate()
1155
+ });
1156
+ return {
1157
+ success: true,
1158
+ keyId: newKeyId
1159
+ };
1160
+ }
1161
+ async function revokeKeyService(params) {
1162
+ const { userId, keyId, reason } = params;
1163
+ const db = getDatabase2();
1164
+ await db.update(userPublicKeys).set({
1165
+ isActive: false,
1166
+ revokedAt: /* @__PURE__ */ new Date(),
1167
+ revokedReason: reason
1168
+ }).where(
1169
+ and2(
1170
+ eq2(userPublicKeys.keyId, keyId),
1171
+ eq2(userPublicKeys.userId, userId)
1172
+ )
1173
+ );
1174
+ }
1175
+
1176
+ // src/server/services/user.service.ts
1177
+ init_entities();
1178
+ import { findOne, updateOne } from "@spfn/core/db";
1179
+ async function getUserByIdService(userId) {
1180
+ return await findOne(users, { id: userId });
1181
+ }
1182
+ async function getUserByEmailService(email) {
1183
+ return await findOne(users, { email });
1184
+ }
1185
+ async function getUserByPhoneService(phone) {
1186
+ return await findOne(users, { phone });
1187
+ }
1188
+ async function updateLastLoginService(userId) {
1189
+ await updateOne(users, { id: userId }, {
1190
+ lastLoginAt: /* @__PURE__ */ new Date()
1191
+ });
1192
+ }
1193
+ async function updateUserService(userId, updates) {
1194
+ await updateOne(users, { id: userId }, {
1195
+ ...updates,
1196
+ updatedAt: /* @__PURE__ */ new Date()
1197
+ });
1198
+ }
1199
+
1200
+ // src/server/services/auth.service.ts
1201
+ async function checkAccountExistsService(params) {
1202
+ const { email, phone } = params;
1203
+ let identifier;
1204
+ let identifierType;
1205
+ let user;
1206
+ if (email) {
1207
+ identifier = email;
1208
+ identifierType = "email";
1209
+ user = await findOne2(users, { email });
1210
+ } else if (phone) {
1211
+ identifier = phone;
1212
+ identifierType = "phone";
1213
+ user = await findOne2(users, { phone });
1214
+ } else {
1215
+ throw new ValidationError2("Either email or phone must be provided");
1216
+ }
1217
+ return {
1218
+ exists: !!user,
1219
+ identifier,
1220
+ identifierType
1221
+ };
1222
+ }
1223
+ async function registerService(params) {
1224
+ const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm } = params;
1225
+ const tokenPayload = validateVerificationToken(verificationToken);
1226
+ if (!tokenPayload) {
1227
+ throw new InvalidVerificationTokenError();
1228
+ }
1229
+ if (tokenPayload.purpose !== "registration") {
1230
+ throw new VerificationTokenPurposeMismatchError("registration", tokenPayload.purpose);
1231
+ }
1232
+ const providedTarget = email || phone;
1233
+ if (tokenPayload.target !== providedTarget) {
1234
+ throw new VerificationTokenTargetMismatchError();
1235
+ }
1236
+ const providedTargetType = email ? "email" : "phone";
1237
+ if (tokenPayload.targetType !== providedTargetType) {
1238
+ throw new VerificationTokenTargetMismatchError();
1239
+ }
1240
+ let existingUser;
1241
+ if (email) {
1242
+ existingUser = await findOne2(users, { email });
1243
+ } else if (phone) {
1244
+ existingUser = await findOne2(users, { phone });
1245
+ } else {
1246
+ throw new ValidationError2("Either email or phone must be provided");
1247
+ }
1248
+ if (existingUser) {
1249
+ const identifierType = email ? "email" : "phone";
1250
+ throw new AccountAlreadyExistsError(email || phone, identifierType);
1251
+ }
1252
+ const passwordHash = await hashPassword(password);
1253
+ const { getRoleByName: getRoleByName2 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
1254
+ const userRole = await getRoleByName2("user");
1255
+ if (!userRole) {
1256
+ throw new Error("Default user role not found. Run initializeAuth() first.");
1257
+ }
1258
+ const newUser = await create3(users, {
1259
+ email: email || null,
1260
+ phone: phone || null,
1261
+ passwordHash,
1262
+ passwordChangeRequired: false,
1263
+ roleId: userRole.id,
1264
+ status: "active",
1265
+ createdAt: /* @__PURE__ */ new Date(),
1266
+ updatedAt: /* @__PURE__ */ new Date()
1267
+ });
1268
+ await registerPublicKeyService({
1269
+ userId: newUser.id,
1270
+ keyId,
1271
+ publicKey,
1272
+ fingerprint,
1273
+ algorithm
1274
+ });
1275
+ return {
1276
+ userId: String(newUser.id),
1277
+ email: newUser.email || void 0,
1278
+ phone: newUser.phone || void 0
1279
+ };
1280
+ }
1281
+ async function loginService(params) {
1282
+ const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
1283
+ let user;
1284
+ if (email) {
1285
+ user = await findOne2(users, { email });
1286
+ } else if (phone) {
1287
+ user = await findOne2(users, { phone });
1288
+ } else {
1289
+ throw new ValidationError2("Either email or phone must be provided");
1290
+ }
1291
+ if (!user || !user.passwordHash) {
1292
+ throw new InvalidCredentialsError();
1293
+ }
1294
+ const isValid = await verifyPassword(password, user.passwordHash);
1295
+ if (!isValid) {
1296
+ throw new InvalidCredentialsError();
1297
+ }
1298
+ if (user.status !== "active") {
1299
+ throw new AccountDisabledError(user.status);
1300
+ }
1301
+ if (oldKeyId) {
1302
+ await revokeKeyService({
1303
+ userId: user.id,
1304
+ keyId: oldKeyId,
1305
+ reason: "Replaced by new key on login"
1306
+ });
1307
+ }
1308
+ await registerPublicKeyService({
1309
+ userId: user.id,
1310
+ keyId,
1311
+ publicKey,
1312
+ fingerprint,
1313
+ algorithm
1314
+ });
1315
+ await updateLastLoginService(user.id);
1316
+ return {
1317
+ userId: String(user.id),
1318
+ email: user.email || void 0,
1319
+ phone: user.phone || void 0,
1320
+ passwordChangeRequired: user.passwordChangeRequired
1321
+ };
1322
+ }
1323
+ async function logoutService(params) {
1324
+ const { userId, keyId } = params;
1325
+ await revokeKeyService({
1326
+ userId,
1327
+ keyId,
1328
+ reason: "Revoked by logout"
1329
+ });
1330
+ }
1331
+ async function changePasswordService(params) {
1332
+ const { userId, currentPassword, newPassword, passwordHash: providedHash } = params;
1333
+ let passwordHash;
1334
+ if (providedHash) {
1335
+ passwordHash = providedHash;
1336
+ } else {
1337
+ const user = await findOne2(users, { id: userId });
1338
+ if (!user) {
1339
+ throw new ValidationError2("User not found");
1340
+ }
1341
+ passwordHash = user.passwordHash;
1342
+ }
1343
+ if (!passwordHash) {
1344
+ throw new ValidationError2("No password set for this account");
1345
+ }
1346
+ const isValid = await verifyPassword(currentPassword, passwordHash);
1347
+ if (!isValid) {
1348
+ throw new InvalidCredentialsError("Current password is incorrect");
1349
+ }
1350
+ const newPasswordHash = await hashPassword(newPassword);
1351
+ const { updateOne: updateOne2 } = await import("@spfn/core/db");
1352
+ await updateOne2(users, { id: userId }, {
1353
+ passwordHash: newPasswordHash,
1354
+ passwordChangeRequired: false,
1355
+ updatedAt: /* @__PURE__ */ new Date()
1356
+ });
1357
+ }
1358
+
1359
+ // src/server/services/verification.service.ts
1360
+ async function sendVerificationCodeService(params) {
1361
+ const { target, targetType, purpose } = params;
1362
+ const code = generateVerificationCode();
1363
+ const codeRecord = await storeVerificationCode(target, targetType, code, purpose);
1364
+ if (targetType === "email") {
1365
+ await sendVerificationEmail(target, code, purpose);
1366
+ } else {
1367
+ await sendVerificationSMS(target, code, purpose);
1368
+ }
1369
+ return {
1370
+ success: true,
1371
+ expiresAt: codeRecord.expiresAt.toISOString()
1372
+ };
1373
+ }
1374
+ async function verifyCodeService(params) {
1375
+ const { target, targetType, code, purpose } = params;
1376
+ const validation = await validateVerificationCode(target, targetType, code, purpose);
1377
+ if (!validation.valid) {
1378
+ throw new InvalidVerificationCodeError(validation.error || "Invalid verification code");
1379
+ }
1380
+ await markCodeAsUsed(validation.codeId);
1381
+ const verificationToken = createVerificationToken({
1382
+ target,
1383
+ targetType,
1384
+ purpose,
1385
+ codeId: validation.codeId
1386
+ });
1387
+ return {
1388
+ valid: true,
1389
+ verificationToken
1390
+ };
1391
+ }
1392
+
1393
+ // src/server/services/me.service.ts
1394
+ init_entities();
1395
+ import { getDatabase as getDatabase4 } from "@spfn/core/db";
1396
+ import { eq as eq4, and as and4 } from "drizzle-orm";
1397
+ async function getMeService(userId) {
1398
+ const db = getDatabase4();
1399
+ if (!db) {
1400
+ throw new Error("[Auth] Database not initialized");
1401
+ }
1402
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
1403
+ const [userWithRole] = await db.select({
1404
+ userId: users.id,
1405
+ email: users.email,
1406
+ phone: users.phone,
1407
+ roleId: roles.id,
1408
+ roleName: roles.name,
1409
+ roleDisplayName: roles.displayName,
1410
+ rolePriority: roles.priority
1411
+ }).from(users).innerJoin(roles, eq4(users.roleId, roles.id)).where(eq4(users.id, userIdNum)).limit(1);
1412
+ if (!userWithRole) {
1413
+ throw new Error("[Auth] User not found");
1414
+ }
1415
+ const rolePerms = await db.select({
1416
+ id: permissions.id,
1417
+ name: permissions.name,
1418
+ displayName: permissions.displayName,
1419
+ category: permissions.category
1420
+ }).from(rolePermissions).innerJoin(permissions, eq4(rolePermissions.permissionId, permissions.id)).where(
1421
+ and4(
1422
+ eq4(rolePermissions.roleId, userWithRole.roleId),
1423
+ eq4(permissions.isActive, true)
1424
+ )
1425
+ );
1426
+ return {
1427
+ userId: userWithRole.userId.toString(),
1428
+ email: userWithRole.email ?? void 0,
1429
+ phone: userWithRole.phone ?? void 0,
1430
+ role: {
1431
+ id: userWithRole.roleId,
1432
+ name: userWithRole.roleName,
1433
+ displayName: userWithRole.roleDisplayName,
1434
+ priority: userWithRole.rolePriority
1435
+ },
1436
+ permissions: rolePerms.map((perm) => ({
1437
+ id: perm.id,
1438
+ name: perm.name,
1439
+ displayName: perm.displayName,
1440
+ category: perm.category ?? void 0
1441
+ }))
1442
+ };
1443
+ }
1444
+
1445
+ // src/server/services/rbac.service.ts
1446
+ init_entities();
1447
+ import { getDatabase as getDatabase5 } from "@spfn/core/db";
1448
+ import { logger } from "@spfn/core/logger";
1449
+ import { eq as eq5, and as and5, inArray } from "drizzle-orm";
1450
+
1451
+ // src/lib/config.ts
1452
+ var globalConfig = {
1453
+ sessionTtl: "7d"
1454
+ // Default: 7 days
1455
+ };
1456
+ function configureAuth(config) {
1457
+ globalConfig = {
1458
+ ...globalConfig,
1459
+ ...config
1460
+ };
1461
+ }
1462
+
1463
+ // src/server/services/rbac.service.ts
1464
+ var authLogger = logger.child("@spfn/auth");
1465
+ async function initializeAuth(options = {}) {
1466
+ const db = getDatabase5();
1467
+ if (!db) {
1468
+ throw new Error("[Auth] Database not initialized. Call initDatabase() first.");
1469
+ }
1470
+ authLogger.info("\u{1F510} Initializing RBAC system...");
1471
+ if (options.sessionTtl !== void 0) {
1472
+ configureAuth({
1473
+ sessionTtl: options.sessionTtl
1474
+ });
1475
+ authLogger.info(`\u23F1\uFE0F Session TTL: ${options.sessionTtl}`);
1476
+ }
1477
+ const allRoles = [
1478
+ ...Object.values(BUILTIN_ROLES),
1479
+ ...options.roles || []
1480
+ ];
1481
+ for (const roleConfig of allRoles) {
1482
+ await upsertRole(roleConfig);
1483
+ }
1484
+ const allPermissions = [
1485
+ ...Object.values(BUILTIN_PERMISSIONS),
1486
+ ...options.permissions || []
1487
+ ];
1488
+ for (const permConfig of allPermissions) {
1489
+ await upsertPermission(permConfig);
1490
+ }
1491
+ const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
1492
+ if (options.rolePermissions) {
1493
+ for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
1494
+ if (allMappings[roleName]) {
1495
+ allMappings[roleName] = [
1496
+ .../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
1497
+ ];
1498
+ } else {
1499
+ allMappings[roleName] = permNames;
1500
+ }
1501
+ }
1502
+ }
1503
+ for (const [roleName, permNames] of Object.entries(allMappings)) {
1504
+ await assignPermissionsToRole(roleName, permNames);
1505
+ }
1506
+ authLogger.info("\u2705 RBAC initialization complete");
1507
+ authLogger.info(`\u{1F4CA} Roles: ${allRoles.length}, Permissions: ${allPermissions.length}`);
1508
+ authLogger.info("\u{1F512} Built-in roles: user, admin, superadmin");
1509
+ }
1510
+ async function upsertRole(config) {
1511
+ const db = getDatabase5();
1512
+ const existing = await db.select().from(roles).where(eq5(roles.name, config.name)).limit(1);
1513
+ if (existing.length === 0) {
1514
+ await db.insert(roles).values({
1515
+ name: config.name,
1516
+ displayName: config.displayName,
1517
+ description: config.description,
1518
+ priority: config.priority ?? 10,
1519
+ isSystem: config.isSystem ?? false,
1520
+ isBuiltin: config.isBuiltin ?? false
1521
+ });
1522
+ authLogger.info(` \u2705 Created role: ${config.name}`);
1523
+ } else {
1524
+ const updateData = {
1525
+ displayName: config.displayName,
1526
+ description: config.description
1527
+ };
1528
+ if (!existing[0].isBuiltin) {
1529
+ updateData.priority = config.priority ?? existing[0].priority;
1530
+ }
1531
+ await db.update(roles).set(updateData).where(eq5(roles.id, existing[0].id));
1532
+ }
1533
+ }
1534
+ async function upsertPermission(config) {
1535
+ const db = getDatabase5();
1536
+ const existing = await db.select().from(permissions).where(eq5(permissions.name, config.name)).limit(1);
1537
+ if (existing.length === 0) {
1538
+ await db.insert(permissions).values({
1539
+ name: config.name,
1540
+ displayName: config.displayName,
1541
+ description: config.description,
1542
+ category: config.category,
1543
+ isSystem: config.isSystem ?? false,
1544
+ isBuiltin: config.isBuiltin ?? false
1545
+ });
1546
+ authLogger.info(` \u2705 Created permission: ${config.name}`);
1547
+ } else {
1548
+ await db.update(permissions).set({
1549
+ displayName: config.displayName,
1550
+ description: config.description,
1551
+ category: config.category
1552
+ }).where(eq5(permissions.id, existing[0].id));
1553
+ }
1554
+ }
1555
+ async function assignPermissionsToRole(roleName, permissionNames) {
1556
+ const db = getDatabase5();
1557
+ const [role] = await db.select().from(roles).where(eq5(roles.name, roleName)).limit(1);
1558
+ if (!role) {
1559
+ authLogger.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
1560
+ return;
1561
+ }
1562
+ const perms = await db.select().from(permissions).where(inArray(permissions.name, permissionNames));
1563
+ if (perms.length === 0) {
1564
+ authLogger.warn(` \u26A0\uFE0F No permissions found for role: ${roleName}`);
1565
+ return;
1566
+ }
1567
+ for (const perm of perms) {
1568
+ const existing = await db.select().from(rolePermissions).where(
1569
+ and5(
1570
+ eq5(rolePermissions.roleId, role.id),
1571
+ eq5(rolePermissions.permissionId, perm.id)
1572
+ )
1573
+ ).limit(1);
1574
+ if (existing.length === 0) {
1575
+ await db.insert(rolePermissions).values({
1576
+ roleId: role.id,
1577
+ permissionId: perm.id
1578
+ });
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ // src/server/services/permission.service.ts
1584
+ init_entities();
1585
+ import { getDatabase as getDatabase6 } from "@spfn/core/db";
1586
+ import { eq as eq6, and as and6 } from "drizzle-orm";
1587
+ async function getUserPermissions(userId) {
1588
+ const db = getDatabase6();
1589
+ if (!db) {
1590
+ throw new Error("[Auth] Database not initialized");
1591
+ }
1592
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
1593
+ const [user] = await db.select({ roleId: users.roleId }).from(users).where(eq6(users.id, userIdNum)).limit(1);
1594
+ if (!user || !user.roleId) {
1595
+ return [];
1596
+ }
1597
+ const permSet = /* @__PURE__ */ new Set();
1598
+ const rolePerms = await db.select({ name: permissions.name }).from(rolePermissions).innerJoin(permissions, eq6(rolePermissions.permissionId, permissions.id)).where(
1599
+ and6(
1600
+ eq6(rolePermissions.roleId, user.roleId),
1601
+ eq6(permissions.isActive, true)
1602
+ )
1603
+ );
1604
+ for (const perm of rolePerms) {
1605
+ permSet.add(perm.name);
1606
+ }
1607
+ const userPerms = await db.select({
1608
+ name: permissions.name,
1609
+ granted: userPermissions.granted,
1610
+ expiresAt: userPermissions.expiresAt
1611
+ }).from(userPermissions).innerJoin(permissions, eq6(userPermissions.permissionId, permissions.id)).where(eq6(userPermissions.userId, userIdNum));
1612
+ const now = /* @__PURE__ */ new Date();
1613
+ for (const userPerm of userPerms) {
1614
+ if (userPerm.expiresAt && userPerm.expiresAt < now) {
1615
+ continue;
1616
+ }
1617
+ if (userPerm.granted) {
1618
+ permSet.add(userPerm.name);
1619
+ } else {
1620
+ permSet.delete(userPerm.name);
1621
+ }
1622
+ }
1623
+ return Array.from(permSet);
1624
+ }
1625
+ async function hasPermission(userId, permissionName) {
1626
+ const perms = await getUserPermissions(userId);
1627
+ return perms.includes(permissionName);
1628
+ }
1629
+ async function hasAnyPermission(userId, permissionNames) {
1630
+ const perms = await getUserPermissions(userId);
1631
+ return permissionNames.some((p) => perms.includes(p));
1632
+ }
1633
+ async function hasAllPermissions(userId, permissionNames) {
1634
+ const perms = await getUserPermissions(userId);
1635
+ return permissionNames.every((p) => perms.includes(p));
1636
+ }
1637
+ async function hasRole(userId, roleName) {
1638
+ const db = getDatabase6();
1639
+ if (!db) {
1640
+ throw new Error("[Auth] Database not initialized");
1641
+ }
1642
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
1643
+ const [user] = await db.select({ roleId: users.roleId }).from(users).where(eq6(users.id, userIdNum)).limit(1);
1644
+ if (!user || !user.roleId) {
1645
+ return false;
1646
+ }
1647
+ const [role] = await db.select({ name: roles.name }).from(roles).where(eq6(roles.id, user.roleId)).limit(1);
1648
+ return role?.name === roleName;
1649
+ }
1650
+ async function hasAnyRole(userId, roleNames) {
1651
+ for (const roleName of roleNames) {
1652
+ if (await hasRole(userId, roleName)) {
1653
+ return true;
1654
+ }
1655
+ }
1656
+ return false;
1657
+ }
1658
+
1659
+ // src/server/services/index.ts
1660
+ init_role_service();
1661
+
1662
+ // src/server/services/invitation.service.ts
1663
+ init_entities();
1664
+ import { getDatabase as getDatabase7 } from "@spfn/core/db";
1665
+ import { eq as eq7, and as and7, lt, desc, sql as sql2 } from "drizzle-orm";
1666
+ import crypto2 from "crypto";
1667
+ function generateInvitationToken() {
1668
+ return crypto2.randomUUID();
1669
+ }
1670
+ function calculateExpiresAt(days = 7) {
1671
+ const expiresAt = /* @__PURE__ */ new Date();
1672
+ expiresAt.setDate(expiresAt.getDate() + days);
1673
+ return expiresAt;
1674
+ }
1675
+ async function createInvitation(params) {
1676
+ const db = getDatabase7();
1677
+ if (!db) {
1678
+ throw new Error("[Auth] Database not initialized");
1679
+ }
1680
+ const { email, roleId, invitedBy, expiresInDays = 7, metadata } = params;
1681
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1682
+ if (!emailRegex.test(email)) {
1683
+ throw new Error("Invalid email format");
1684
+ }
1685
+ const existingUser = await db.select().from(users).where(eq7(users.email, email)).limit(1);
1686
+ if (existingUser.length > 0) {
1687
+ throw new Error("User with this email already exists");
1688
+ }
1689
+ const existingInvitation = await db.select().from(invitations).where(
1690
+ and7(
1691
+ eq7(invitations.email, email),
1692
+ eq7(invitations.status, "pending")
1693
+ )
1694
+ ).limit(1);
1695
+ if (existingInvitation.length > 0) {
1696
+ throw new Error("Pending invitation already exists for this email");
1697
+ }
1698
+ const role = await db.select().from(roles).where(eq7(roles.id, roleId)).limit(1);
1699
+ if (role.length === 0) {
1700
+ throw new Error(`Role with id ${roleId} not found`);
1701
+ }
1702
+ const inviter = await db.select().from(users).where(eq7(users.id, invitedBy)).limit(1);
1703
+ if (inviter.length === 0) {
1704
+ throw new Error(`User with id ${invitedBy} not found`);
1705
+ }
1706
+ const token = generateInvitationToken();
1707
+ const expiresAt = calculateExpiresAt(expiresInDays);
1708
+ const [invitation] = await db.insert(invitations).values({
1709
+ email,
1710
+ token,
1711
+ roleId,
1712
+ invitedBy,
1713
+ status: "pending",
1714
+ expiresAt,
1715
+ metadata: metadata || null
1716
+ }).returning();
1717
+ console.log(`[Auth] \u2705 Created invitation: ${email} as ${role[0].name} (expires: ${expiresAt.toISOString()})`);
1718
+ return invitation;
1719
+ }
1720
+ async function getInvitationByToken(token) {
1721
+ const db = getDatabase7();
1722
+ if (!db) {
1723
+ throw new Error("[Auth] Database not initialized");
1724
+ }
1725
+ const result = await db.select().from(invitations).where(eq7(invitations.token, token)).limit(1);
1726
+ return result[0] || null;
1727
+ }
1728
+ async function getInvitationWithDetails(token) {
1729
+ const db = getDatabase7();
1730
+ if (!db) {
1731
+ throw new Error("[Auth] Database not initialized");
1732
+ }
1733
+ const result = await db.select({
1734
+ id: invitations.id,
1735
+ email: invitations.email,
1736
+ token: invitations.token,
1737
+ roleId: invitations.roleId,
1738
+ invitedBy: invitations.invitedBy,
1739
+ status: invitations.status,
1740
+ expiresAt: invitations.expiresAt,
1741
+ acceptedAt: invitations.acceptedAt,
1742
+ cancelledAt: invitations.cancelledAt,
1743
+ metadata: invitations.metadata,
1744
+ createdAt: invitations.createdAt,
1745
+ updatedAt: invitations.updatedAt,
1746
+ role: {
1747
+ id: roles.id,
1748
+ name: roles.name,
1749
+ displayName: roles.displayName
1750
+ },
1751
+ inviter: {
1752
+ id: users.id,
1753
+ email: users.email
1754
+ }
1755
+ }).from(invitations).innerJoin(roles, eq7(invitations.roleId, roles.id)).innerJoin(users, eq7(invitations.invitedBy, users.id)).where(eq7(invitations.token, token)).limit(1);
1756
+ return result[0] || null;
1757
+ }
1758
+ async function validateInvitation(token) {
1759
+ const invitation = await getInvitationByToken(token);
1760
+ if (!invitation) {
1761
+ return { valid: false, error: "Invitation not found" };
1762
+ }
1763
+ if (invitation.status === "accepted") {
1764
+ return { valid: false, error: "Invitation already accepted", invitation };
1765
+ }
1766
+ if (invitation.status === "cancelled") {
1767
+ return { valid: false, error: "Invitation was cancelled", invitation };
1768
+ }
1769
+ if (invitation.status === "expired") {
1770
+ return { valid: false, error: "Invitation has expired", invitation };
1771
+ }
1772
+ if (/* @__PURE__ */ new Date() > new Date(invitation.expiresAt)) {
1773
+ return { valid: false, error: "Invitation has expired", invitation };
1774
+ }
1775
+ return { valid: true, invitation };
1776
+ }
1777
+ async function acceptInvitation(params) {
1778
+ const db = getDatabase7();
1779
+ if (!db) {
1780
+ throw new Error("[Auth] Database not initialized");
1781
+ }
1782
+ const { token, password, publicKey, keyId, fingerprint, algorithm } = params;
1783
+ const validation = await validateInvitation(token);
1784
+ if (!validation.valid || !validation.invitation) {
1785
+ throw new Error(validation.error || "Invalid invitation");
1786
+ }
1787
+ const invitation = validation.invitation;
1788
+ const role = await db.select().from(roles).where(eq7(roles.id, invitation.roleId)).limit(1);
1789
+ if (role.length === 0) {
1790
+ throw new Error("Role not found");
1791
+ }
1792
+ const passwordHash = await hashPassword(password);
1793
+ const result = await db.transaction(async (tx) => {
1794
+ const [newUser] = await tx.insert(users).values({
1795
+ email: invitation.email,
1796
+ passwordHash,
1797
+ roleId: invitation.roleId,
1798
+ emailVerifiedAt: /* @__PURE__ */ new Date(),
1799
+ // Auto-verify invited users
1800
+ passwordChangeRequired: false,
1801
+ status: "active"
1802
+ }).returning();
1803
+ const { userPublicKeys: userPublicKeys2 } = await Promise.resolve().then(() => (init_entities(), entities_exports));
1804
+ await tx.insert(userPublicKeys2).values({
1805
+ userId: newUser.id,
1806
+ keyId,
1807
+ publicKey,
1808
+ algorithm,
1809
+ fingerprint,
1810
+ isActive: true,
1811
+ expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1e3)
1812
+ // 90 days
1813
+ });
1814
+ await tx.update(invitations).set({
1815
+ status: "accepted",
1816
+ acceptedAt: /* @__PURE__ */ new Date(),
1817
+ updatedAt: /* @__PURE__ */ new Date()
1818
+ }).where(eq7(invitations.id, invitation.id));
1819
+ return { newUser, role: role[0] };
1820
+ });
1821
+ console.log(`[Auth] \u2705 Invitation accepted: ${invitation.email} as ${result.role.name}`);
1822
+ return {
1823
+ userId: result.newUser.id,
1824
+ email: result.newUser.email,
1825
+ role: result.role.name
1826
+ };
1827
+ }
1828
+ async function listInvitations(params) {
1829
+ const db = getDatabase7();
1830
+ if (!db) {
1831
+ throw new Error("[Auth] Database not initialized");
1832
+ }
1833
+ const { status, invitedBy, page = 1, limit = 20 } = params;
1834
+ const offset = (page - 1) * limit;
1835
+ const conditions = [];
1836
+ if (status) {
1837
+ conditions.push(eq7(invitations.status, status));
1838
+ }
1839
+ if (invitedBy) {
1840
+ conditions.push(eq7(invitations.invitedBy, invitedBy));
1841
+ }
1842
+ const whereClause = conditions.length > 0 ? and7(...conditions) : void 0;
1843
+ const countResult = await db.select({ count: sql2`count(*)` }).from(invitations).where(whereClause);
1844
+ const total = Number(countResult[0]?.count || 0);
1845
+ const results = await db.select({
1846
+ id: invitations.id,
1847
+ email: invitations.email,
1848
+ token: invitations.token,
1849
+ roleId: invitations.roleId,
1850
+ invitedBy: invitations.invitedBy,
1851
+ status: invitations.status,
1852
+ expiresAt: invitations.expiresAt,
1853
+ acceptedAt: invitations.acceptedAt,
1854
+ cancelledAt: invitations.cancelledAt,
1855
+ metadata: invitations.metadata,
1856
+ createdAt: invitations.createdAt,
1857
+ updatedAt: invitations.updatedAt,
1858
+ role: {
1859
+ id: roles.id,
1860
+ name: roles.name,
1861
+ displayName: roles.displayName
1862
+ },
1863
+ inviter: {
1864
+ id: users.id,
1865
+ email: users.email
1866
+ }
1867
+ }).from(invitations).innerJoin(roles, eq7(invitations.roleId, roles.id)).innerJoin(users, eq7(invitations.invitedBy, users.id)).where(whereClause).orderBy(desc(invitations.createdAt)).limit(limit).offset(offset);
1868
+ return {
1869
+ invitations: results,
1870
+ total,
1871
+ page,
1872
+ limit,
1873
+ totalPages: Math.ceil(total / limit)
1874
+ };
1875
+ }
1876
+ async function cancelInvitation(id10, cancelledBy, reason) {
1877
+ const db = getDatabase7();
1878
+ if (!db) {
1879
+ throw new Error("[Auth] Database not initialized");
1880
+ }
1881
+ const invitation = await db.select().from(invitations).where(eq7(invitations.id, id10)).limit(1);
1882
+ if (invitation.length === 0) {
1883
+ throw new Error("Invitation not found");
1884
+ }
1885
+ if (invitation[0].status !== "pending") {
1886
+ throw new Error(`Cannot cancel ${invitation[0].status} invitation`);
1887
+ }
1888
+ await db.update(invitations).set({
1889
+ status: "cancelled",
1890
+ cancelledAt: /* @__PURE__ */ new Date(),
1891
+ updatedAt: /* @__PURE__ */ new Date(),
1892
+ metadata: invitation[0].metadata ? { ...invitation[0].metadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy }
1893
+ }).where(eq7(invitations.id, id10));
1894
+ console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation[0].email} (reason: ${reason || "none"})`);
1895
+ }
1896
+ async function deleteInvitation(id10) {
1897
+ const db = getDatabase7();
1898
+ if (!db) {
1899
+ throw new Error("[Auth] Database not initialized");
1900
+ }
1901
+ await db.delete(invitations).where(eq7(invitations.id, id10));
1902
+ console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id10}`);
1903
+ }
1904
+ async function expireOldInvitations() {
1905
+ const db = getDatabase7();
1906
+ if (!db) {
1907
+ throw new Error("[Auth] Database not initialized");
1908
+ }
1909
+ const now = /* @__PURE__ */ new Date();
1910
+ const expiredInvitations = await db.select().from(invitations).where(
1911
+ and7(
1912
+ eq7(invitations.status, "pending"),
1913
+ lt(invitations.expiresAt, now)
1914
+ )
1915
+ );
1916
+ if (expiredInvitations.length === 0) {
1917
+ return 0;
1918
+ }
1919
+ await db.update(invitations).set({
1920
+ status: "expired",
1921
+ updatedAt: now
1922
+ }).where(
1923
+ and7(
1924
+ eq7(invitations.status, "pending"),
1925
+ lt(invitations.expiresAt, now)
1926
+ )
1927
+ );
1928
+ console.log(`[Auth] \u23F0 Expired ${expiredInvitations.length} old invitations`);
1929
+ return expiredInvitations.length;
1930
+ }
1931
+ async function resendInvitation(id10, expiresInDays = 7) {
1932
+ const db = getDatabase7();
1933
+ if (!db) {
1934
+ throw new Error("[Auth] Database not initialized");
1935
+ }
1936
+ const invitation = await db.select().from(invitations).where(eq7(invitations.id, id10)).limit(1);
1937
+ if (invitation.length === 0) {
1938
+ throw new Error("Invitation not found");
1939
+ }
1940
+ if (!["pending", "expired"].includes(invitation[0].status)) {
1941
+ throw new Error(`Cannot resend ${invitation[0].status} invitation`);
1942
+ }
1943
+ const newExpiresAt = calculateExpiresAt(expiresInDays);
1944
+ const [updated] = await db.update(invitations).set({
1945
+ status: "pending",
1946
+ expiresAt: newExpiresAt,
1947
+ updatedAt: /* @__PURE__ */ new Date()
1948
+ }).where(eq7(invitations.id, id10)).returning();
1949
+ console.log(`[Auth] \u{1F4E7} Invitation resent: ${invitation[0].email} (new expiry: ${newExpiresAt.toISOString()})`);
1950
+ return updated;
1951
+ }
1952
+
1953
+ // src/server/middleware/authenticate.ts
1954
+ init_jwt();
1955
+ init_entities();
1956
+ import { findOne as findOne3, getDatabase as getDatabase8 } from "@spfn/core/db";
1957
+ import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
1958
+ import { eq as eq8, and as and8 } from "drizzle-orm";
1959
+ async function authenticate(c, next) {
1960
+ const authHeader = c.req.header("Authorization");
1961
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
1962
+ throw new UnauthorizedError2("Missing or invalid authorization header");
1963
+ }
1964
+ const token = authHeader.substring(7);
1965
+ const { decodeToken: decodeToken2 } = await Promise.resolve().then(() => (init_jwt(), jwt_exports));
1966
+ const decoded = decodeToken2(token);
1967
+ if (!decoded || !decoded.keyId) {
1968
+ throw new UnauthorizedError2("Invalid token: missing keyId");
1969
+ }
1970
+ const keyId = decoded.keyId;
1971
+ const db = getDatabase8();
1972
+ const [keyRecord] = await db.select().from(userPublicKeys).where(
1973
+ and8(
1974
+ eq8(userPublicKeys.keyId, keyId),
1975
+ eq8(userPublicKeys.isActive, true)
1976
+ )
1977
+ );
1978
+ if (!keyRecord) {
1979
+ throw new UnauthorizedError2("Invalid or revoked key");
1980
+ }
1981
+ if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
1982
+ throw new KeyExpiredError();
1983
+ }
1984
+ try {
1985
+ verifyClientToken(
1986
+ token,
1987
+ keyRecord.publicKey,
1988
+ keyRecord.algorithm
1989
+ );
1990
+ } catch (err) {
1991
+ if (err instanceof Error) {
1992
+ if (err.name === "TokenExpiredError") {
1993
+ throw new TokenExpiredError();
1994
+ }
1995
+ if (err.name === "JsonWebTokenError") {
1996
+ throw new InvalidTokenError("Invalid token signature");
1997
+ }
1998
+ }
1999
+ throw new UnauthorizedError2("Authentication failed");
2000
+ }
2001
+ const user = await findOne3(users, { id: keyRecord.userId });
2002
+ if (!user) {
2003
+ throw new UnauthorizedError2("User not found");
2004
+ }
2005
+ if (user.status !== "active") {
2006
+ throw new AccountDisabledError(user.status);
2007
+ }
2008
+ db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq8(userPublicKeys.id, keyRecord.id)).execute().catch((err) => console.error("Failed to update lastUsedAt:", err));
2009
+ c.set("auth", {
2010
+ user,
2011
+ userId: String(user.id),
2012
+ keyId
2013
+ });
2014
+ await next();
2015
+ }
2016
+
2017
+ // src/server/middleware/require-permission.ts
2018
+ import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
2019
+ function requirePermissions(...permissionNames) {
2020
+ return async (c, next) => {
2021
+ const auth = getAuth(c);
2022
+ if (!auth) {
2023
+ throw new ForbiddenError2("Authentication required");
2024
+ }
2025
+ const { userId } = auth;
2026
+ const allowed = await hasAllPermissions(userId, permissionNames);
2027
+ if (!allowed) {
2028
+ throw new ForbiddenError2(
2029
+ `Missing required permissions: ${permissionNames.join(", ")}`
2030
+ );
2031
+ }
2032
+ await next();
2033
+ };
2034
+ }
2035
+ function requireAnyPermission(...permissionNames) {
2036
+ return async (c, next) => {
2037
+ const auth = getAuth(c);
2038
+ if (!auth) {
2039
+ throw new ForbiddenError2("Authentication required");
2040
+ }
2041
+ const { userId } = auth;
2042
+ const allowed = await hasAnyPermission(userId, permissionNames);
2043
+ if (!allowed) {
2044
+ throw new ForbiddenError2(
2045
+ `Requires one of: ${permissionNames.join(", ")}`
2046
+ );
2047
+ }
2048
+ await next();
2049
+ };
2050
+ }
2051
+
2052
+ // src/server/middleware/require-role.ts
2053
+ import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
2054
+ function requireRole(...roleNames) {
2055
+ return async (c, next) => {
2056
+ const auth = getAuth(c);
2057
+ if (!auth) {
2058
+ throw new ForbiddenError3("Authentication required");
2059
+ }
2060
+ const { userId } = auth;
2061
+ const allowed = await hasAnyRole(userId, roleNames);
2062
+ if (!allowed) {
2063
+ throw new ForbiddenError3(
2064
+ `Required roles: ${roleNames.join(", ")}`
2065
+ );
2066
+ }
2067
+ await next();
2068
+ };
2069
+ }
2070
+
2071
+ // src/server/setup.ts
2072
+ init_entities();
2073
+ import { findOne as findOne4, create as create4 } from "@spfn/core/db";
2074
+ import { logger as logger2 } from "@spfn/core/logger";
2075
+ init_role_service();
2076
+ var authLogger2 = logger2.child("@spfn/auth");
2077
+ function parseAdminAccounts() {
2078
+ const accounts = [];
2079
+ if (process.env.SPFN_AUTH_ADMIN_ACCOUNTS || process.env.ADMIN_ACCOUNTS) {
2080
+ try {
2081
+ const accountsJson = process.env.SPFN_AUTH_ADMIN_ACCOUNTS || // New prefixed version (recommended)
2082
+ process.env.ADMIN_ACCOUNTS;
2083
+ const parsed = JSON.parse(accountsJson);
2084
+ if (!Array.isArray(parsed)) {
2085
+ authLogger2.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
2086
+ return accounts;
2087
+ }
2088
+ for (const item of parsed) {
2089
+ if (!item.email || !item.password) {
2090
+ authLogger2.warn("\u26A0\uFE0F Skipping account: missing email or password");
2091
+ continue;
2092
+ }
2093
+ accounts.push({
2094
+ email: item.email,
2095
+ password: item.password,
2096
+ role: item.role || "user",
2097
+ phone: item.phone,
2098
+ passwordChangeRequired: item.passwordChangeRequired !== false
2099
+ // Default: true
2100
+ });
2101
+ }
2102
+ return accounts;
2103
+ } catch (error) {
2104
+ const err = error;
2105
+ authLogger2.error("\u274C Failed to parse SPFN_AUTH_ADMIN_ACCOUNTS:", err);
2106
+ return accounts;
2107
+ }
2108
+ }
2109
+ const adminEmails = process.env.SPFN_AUTH_ADMIN_EMAILS || // New prefixed version (recommended)
2110
+ process.env.ADMIN_EMAILS;
2111
+ if (adminEmails) {
2112
+ const emails = adminEmails.split(",").map((s) => s.trim());
2113
+ const passwords = (process.env.SPFN_AUTH_ADMIN_PASSWORDS || // New prefixed version (recommended)
2114
+ process.env.ADMIN_PASSWORDS || // Legacy fallback
2115
+ "").split(",").map((s) => s.trim());
2116
+ const roles2 = (process.env.SPFN_AUTH_ADMIN_ROLES || // New prefixed version (recommended)
2117
+ process.env.ADMIN_ROLES || // Legacy fallback
2118
+ "").split(",").map((s) => s.trim());
2119
+ if (passwords.length !== emails.length) {
2120
+ authLogger2.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
2121
+ return accounts;
2122
+ }
2123
+ for (let i = 0; i < emails.length; i++) {
2124
+ const email = emails[i];
2125
+ const password = passwords[i];
2126
+ const role = roles2[i] || "user";
2127
+ if (!email || !password) {
2128
+ authLogger2.warn(`\u26A0\uFE0F Skipping account ${i + 1}: missing email or password`);
2129
+ continue;
2130
+ }
2131
+ accounts.push({
2132
+ email,
2133
+ password,
2134
+ role,
2135
+ passwordChangeRequired: true
2136
+ });
2137
+ }
2138
+ return accounts;
2139
+ }
2140
+ const adminEmail = process.env.SPFN_AUTH_ADMIN_EMAIL || // New prefixed version (recommended)
2141
+ process.env.ADMIN_EMAIL;
2142
+ const adminPassword = process.env.SPFN_AUTH_ADMIN_PASSWORD || // New prefixed version (recommended)
2143
+ process.env.ADMIN_PASSWORD;
2144
+ if (adminEmail && adminPassword) {
2145
+ accounts.push({
2146
+ email: adminEmail,
2147
+ password: adminPassword,
2148
+ role: "superadmin",
2149
+ passwordChangeRequired: true
2150
+ });
2151
+ }
2152
+ return accounts;
2153
+ }
2154
+ async function ensureAdminExists() {
2155
+ const accounts = parseAdminAccounts();
2156
+ if (accounts.length === 0) {
2157
+ return;
2158
+ }
2159
+ authLogger2.info(`Creating ${accounts.length} admin account(s)...`);
2160
+ let created = 0;
2161
+ let skipped = 0;
2162
+ let failed = 0;
2163
+ for (const account of accounts) {
2164
+ try {
2165
+ const existing = await findOne4(users, { email: account.email });
2166
+ if (existing) {
2167
+ authLogger2.info(`\u26A0\uFE0F Account already exists: ${account.email} (skipped)`);
2168
+ skipped++;
2169
+ continue;
2170
+ }
2171
+ const roleName = account.role || "user";
2172
+ const role = await getRoleByName(roleName);
2173
+ if (!role) {
2174
+ authLogger2.error(`\u274C Role '${roleName}' not found for ${account.email}. Run initializeAuth() first.`);
2175
+ failed++;
2176
+ continue;
2177
+ }
2178
+ const passwordHash = await hashPassword(account.password);
2179
+ await create4(users, {
2180
+ email: account.email,
2181
+ phone: account.phone || null,
2182
+ passwordHash,
2183
+ roleId: role.id,
2184
+ emailVerifiedAt: /* @__PURE__ */ new Date(),
2185
+ // Auto-verify admin
2186
+ passwordChangeRequired: account.passwordChangeRequired !== false,
2187
+ status: "active"
2188
+ });
2189
+ authLogger2.info(`\u2705 Admin account created: ${account.email} (${roleName})`);
2190
+ created++;
2191
+ } catch (error) {
2192
+ const err = error;
2193
+ authLogger2.error(`\u274C Failed to create account ${account.email}:`, err);
2194
+ failed++;
2195
+ }
2196
+ }
2197
+ authLogger2.info(`\u{1F4CA} Summary: ${created} created, ${skipped} skipped, ${failed} failed`);
2198
+ if (created > 0) {
2199
+ authLogger2.info("\u26A0\uFE0F Please change passwords on first login!");
2200
+ }
2201
+ }
2202
+ export {
2203
+ BUILTIN_PERMISSIONS,
2204
+ BUILTIN_ROLES,
2205
+ BUILTIN_ROLE_PERMISSIONS,
2206
+ acceptInvitation,
2207
+ addPermissionToRole,
2208
+ authenticate,
2209
+ cancelInvitation,
2210
+ changePasswordService,
2211
+ checkAccountExistsService,
2212
+ createInvitation,
2213
+ createRole,
2214
+ createVerificationToken,
2215
+ decodeToken,
2216
+ deleteInvitation,
2217
+ deleteRole,
2218
+ ensureAdminExists,
2219
+ expireOldInvitations,
2220
+ generateToken,
2221
+ generateVerificationCode,
2222
+ getAllRoles,
2223
+ getAuth,
2224
+ getInvitationByToken,
2225
+ getInvitationWithDetails,
2226
+ getKeyId,
2227
+ getMeService,
2228
+ getRoleByName,
2229
+ getRolePermissions,
2230
+ getUser,
2231
+ getUserByEmailService,
2232
+ getUserByIdService,
2233
+ getUserByPhoneService,
2234
+ getUserId,
2235
+ getUserPermissions,
2236
+ hasAllPermissions,
2237
+ hasAnyPermission,
2238
+ hasAnyRole,
2239
+ hasPermission,
2240
+ hasRole,
2241
+ hashPassword,
2242
+ initializeAuth,
2243
+ listInvitations,
2244
+ loginService,
2245
+ logoutService,
2246
+ markCodeAsUsed,
2247
+ registerPublicKeyService,
2248
+ registerService,
2249
+ removePermissionFromRole,
2250
+ requireAnyPermission,
2251
+ requirePermissions,
2252
+ requireRole,
2253
+ resendInvitation,
2254
+ revokeKeyService,
2255
+ rotateKeyService,
2256
+ sendVerificationCodeService,
2257
+ sendVerificationEmail,
2258
+ sendVerificationSMS,
2259
+ setRolePermissions,
2260
+ storeVerificationCode,
2261
+ updateLastLoginService,
2262
+ updateRole,
2263
+ updateUserService,
2264
+ validateInvitation,
2265
+ validatePasswordStrength,
2266
+ validateVerificationCode,
2267
+ validateVerificationToken,
2268
+ verifyClientToken,
2269
+ verifyCodeService,
2270
+ verifyKeyFingerprint,
2271
+ verifyPassword,
2272
+ verifyToken
2273
+ };
2274
+ //# sourceMappingURL=server.js.map