@parsrun/auth 0.1.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 (54) hide show
  1. package/README.md +133 -0
  2. package/dist/adapters/hono.d.ts +9 -0
  3. package/dist/adapters/hono.js +6 -0
  4. package/dist/adapters/hono.js.map +1 -0
  5. package/dist/adapters/index.d.ts +9 -0
  6. package/dist/adapters/index.js +7 -0
  7. package/dist/adapters/index.js.map +1 -0
  8. package/dist/authorization-By1Xp8Za.d.ts +213 -0
  9. package/dist/base-BKyR8rcE.d.ts +646 -0
  10. package/dist/chunk-42MGHABB.js +263 -0
  11. package/dist/chunk-42MGHABB.js.map +1 -0
  12. package/dist/chunk-7GOBAL4G.js +3 -0
  13. package/dist/chunk-7GOBAL4G.js.map +1 -0
  14. package/dist/chunk-G5I3T73A.js +152 -0
  15. package/dist/chunk-G5I3T73A.js.map +1 -0
  16. package/dist/chunk-IB4WUQDZ.js +410 -0
  17. package/dist/chunk-IB4WUQDZ.js.map +1 -0
  18. package/dist/chunk-MOG4Y6I7.js +415 -0
  19. package/dist/chunk-MOG4Y6I7.js.map +1 -0
  20. package/dist/chunk-NK4TJV2W.js +295 -0
  21. package/dist/chunk-NK4TJV2W.js.map +1 -0
  22. package/dist/chunk-RHNVRCF3.js +838 -0
  23. package/dist/chunk-RHNVRCF3.js.map +1 -0
  24. package/dist/chunk-YTCPXJR5.js +570 -0
  25. package/dist/chunk-YTCPXJR5.js.map +1 -0
  26. package/dist/cloudflare-kv-L64CZKDK.js +105 -0
  27. package/dist/cloudflare-kv-L64CZKDK.js.map +1 -0
  28. package/dist/deno-kv-F55HKKP6.js +111 -0
  29. package/dist/deno-kv-F55HKKP6.js.map +1 -0
  30. package/dist/index-C3kz9XqE.d.ts +226 -0
  31. package/dist/index-DOGcetyD.d.ts +1041 -0
  32. package/dist/index.d.ts +1579 -0
  33. package/dist/index.js +4294 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/jwt-manager-CH8H0kmm.d.ts +182 -0
  36. package/dist/providers/index.d.ts +90 -0
  37. package/dist/providers/index.js +3 -0
  38. package/dist/providers/index.js.map +1 -0
  39. package/dist/providers/otp/index.d.ts +3 -0
  40. package/dist/providers/otp/index.js +4 -0
  41. package/dist/providers/otp/index.js.map +1 -0
  42. package/dist/redis-5TIS6XCA.js +121 -0
  43. package/dist/redis-5TIS6XCA.js.map +1 -0
  44. package/dist/security/index.d.ts +301 -0
  45. package/dist/security/index.js +5 -0
  46. package/dist/security/index.js.map +1 -0
  47. package/dist/session/index.d.ts +117 -0
  48. package/dist/session/index.js +4 -0
  49. package/dist/session/index.js.map +1 -0
  50. package/dist/storage/index.d.ts +97 -0
  51. package/dist/storage/index.js +3 -0
  52. package/dist/storage/index.js.map +1 -0
  53. package/dist/types-DSjafxJ4.d.ts +193 -0
  54. package/package.json +102 -0
package/dist/index.js ADDED
@@ -0,0 +1,4294 @@
1
+ import './chunk-7GOBAL4G.js';
2
+ export { createAuthCookies, createAuthMiddleware, createAuthRoutes, createHonoAuth, createLogoutCookies, createOptionalAuthMiddleware, requireAdmin, requireAll, requireAny, requireAnyPermission, requireOwnerOrPermission, requirePermission, requireRole, requireTenant, requireTenantAccess } from './chunk-RHNVRCF3.js';
3
+ import { ProviderRegistry } from './chunk-G5I3T73A.js';
4
+ export { ProviderRegistry } from './chunk-G5I3T73A.js';
5
+ import { generateRandomHex, sha256Hex } from './chunk-YTCPXJR5.js';
6
+ export { CsrfManager, CsrfUtils, DefaultLockoutConfig, LockoutManager, RateLimitPresets, RateLimiter, base64UrlDecode, base64UrlEncode, bytesToHex, createCsrfManager, createLockoutManager, createRateLimiter, generateRandomBase64Url, generateRandomHex, hexToBytes, randomInt, sha256, sha256Hex, timingSafeEqual, timingSafeEqualBytes } from './chunk-YTCPXJR5.js';
7
+ export { AuthorizationGuard, Permissions, Roles, authorize, createAuthorizationGuard } from './chunk-NK4TJV2W.js';
8
+ import { JwtManager, SessionBlocklist } from './chunk-MOG4Y6I7.js';
9
+ export { JwtError, JwtManager, SessionBlocklist, TokenBlocklist, createJwtManager, createSessionBlocklist, createTokenBlocklist, extractBearerToken, parseDuration } from './chunk-MOG4Y6I7.js';
10
+ import { OTPProvider } from './chunk-IB4WUQDZ.js';
11
+ export { OTPManager, OTPProvider, createOTPManager, createOTPProvider } from './chunk-IB4WUQDZ.js';
12
+ import { createStorage } from './chunk-42MGHABB.js';
13
+ export { MemoryStorage, StorageKeys, createMemoryStorage, createStorage, createStorageSync, detectRuntime, getEnv, isBun, isCloudflare, isDeno, isEdge, isNode } from './chunk-42MGHABB.js';
14
+ import { eq, and, isNull, desc } from 'drizzle-orm';
15
+
16
+ // src/config.ts
17
+ var defaultConfig = {
18
+ providers: {
19
+ otp: {
20
+ enabled: true,
21
+ email: {
22
+ enabled: true,
23
+ expiresIn: 600,
24
+ length: 6,
25
+ maxAttempts: 3,
26
+ rateLimit: 5,
27
+ rateLimitWindow: 900,
28
+ // send function must be provided by user
29
+ send: async () => {
30
+ throw new Error("[Pars Auth] Email OTP send function not configured");
31
+ }
32
+ },
33
+ sms: {
34
+ enabled: false,
35
+ expiresIn: 300,
36
+ length: 6,
37
+ maxAttempts: 3,
38
+ rateLimit: 3,
39
+ rateLimitWindow: 900,
40
+ send: async () => {
41
+ throw new Error("[Pars Auth] SMS OTP send function not configured");
42
+ }
43
+ }
44
+ },
45
+ password: {
46
+ enabled: false
47
+ // EXPLICITLY DISABLED BY DEFAULT
48
+ }
49
+ },
50
+ session: {
51
+ accessTokenExpiry: 900,
52
+ // 15 minutes
53
+ refreshTokenExpiry: 604800,
54
+ // 7 days
55
+ slidingWindow: true,
56
+ maxSessions: 5,
57
+ invalidateOnPasswordChange: true
58
+ },
59
+ jwt: {
60
+ algorithm: "HS256"
61
+ },
62
+ cookies: {
63
+ prefix: "pars",
64
+ path: "/",
65
+ sameSite: "lax",
66
+ httpOnly: true
67
+ },
68
+ security: {
69
+ rateLimit: {
70
+ enabled: true,
71
+ loginAttempts: 5,
72
+ windowSize: 900
73
+ },
74
+ lockout: {
75
+ enabled: true,
76
+ maxAttempts: 5,
77
+ duration: 900
78
+ },
79
+ csrf: {
80
+ enabled: true,
81
+ headerName: "x-csrf-token",
82
+ cookieName: "csrf"
83
+ }
84
+ },
85
+ tenant: {
86
+ enabled: true,
87
+ strategy: "header",
88
+ headerName: "x-tenant-id"
89
+ }
90
+ };
91
+ function mergeConfig(config) {
92
+ return {
93
+ secret: config.secret,
94
+ baseUrl: config.baseUrl ?? "",
95
+ storage: config.storage ?? {},
96
+ providers: {
97
+ ...defaultConfig.providers,
98
+ ...config.providers,
99
+ otp: {
100
+ ...defaultConfig.providers?.otp,
101
+ ...config.providers?.otp,
102
+ email: {
103
+ ...defaultConfig.providers?.otp?.email,
104
+ ...config.providers?.otp?.email
105
+ },
106
+ sms: {
107
+ ...defaultConfig.providers?.otp?.sms,
108
+ ...config.providers?.otp?.sms
109
+ }
110
+ },
111
+ password: {
112
+ ...defaultConfig.providers?.password,
113
+ ...config.providers?.password
114
+ }
115
+ },
116
+ session: { ...defaultConfig.session, ...config.session },
117
+ jwt: { ...defaultConfig.jwt, ...config.jwt },
118
+ cookies: { ...defaultConfig.cookies, ...config.cookies },
119
+ security: {
120
+ ...defaultConfig.security,
121
+ ...config.security,
122
+ rateLimit: { ...defaultConfig.security?.rateLimit, ...config.security?.rateLimit },
123
+ lockout: { ...defaultConfig.security?.lockout, ...config.security?.lockout },
124
+ csrf: { ...defaultConfig.security?.csrf, ...config.security?.csrf }
125
+ },
126
+ tenant: { ...defaultConfig.tenant, ...config.tenant },
127
+ adapter: config.adapter,
128
+ callbacks: config.callbacks ?? {}
129
+ };
130
+ }
131
+ function validateConfig(config) {
132
+ if (!config.secret) {
133
+ throw new Error("[Pars Auth] Secret is required");
134
+ }
135
+ if (config.secret.length < 32) {
136
+ console.warn(
137
+ "[Pars Auth] Secret should be at least 32 characters for security"
138
+ );
139
+ }
140
+ if (!config.adapter) {
141
+ throw new Error("[Pars Auth] Database adapter is required");
142
+ }
143
+ if (config.providers?.password?.enabled) {
144
+ console.warn(
145
+ "[Pars Auth] Password authentication is enabled. Consider using passwordless authentication (OTP, Magic Link, WebAuthn) for better security."
146
+ );
147
+ }
148
+ if (config.providers?.otp?.email?.enabled !== false && !config.providers?.otp?.email?.send) {
149
+ console.warn(
150
+ "[Pars Auth] Email OTP is enabled but send function is not configured"
151
+ );
152
+ }
153
+ }
154
+
155
+ // src/core/tenant-manager.ts
156
+ var TenantManager = class {
157
+ adapter;
158
+ constructor(adapter) {
159
+ this.adapter = adapter;
160
+ }
161
+ // ============================================
162
+ // Tenant Operations
163
+ // ============================================
164
+ /**
165
+ * Create a new tenant with owner
166
+ */
167
+ async createTenant(input) {
168
+ if (!this.adapter.findTenantById) {
169
+ throw new Error("Tenant operations not supported by adapter");
170
+ }
171
+ const slug = input.slug ?? this.generateSlug(input.name);
172
+ const existingTenant = await this.adapter.findTenantBySlug?.(slug);
173
+ if (existingTenant) {
174
+ throw new Error(`Tenant with slug '${slug}' already exists`);
175
+ }
176
+ throw new Error(
177
+ "createTenant not implemented in adapter. Please implement createTenant in your adapter or use direct database access."
178
+ );
179
+ }
180
+ /**
181
+ * Get tenant by ID
182
+ */
183
+ async getTenantById(id) {
184
+ if (!this.adapter.findTenantById) {
185
+ return null;
186
+ }
187
+ return this.adapter.findTenantById(id);
188
+ }
189
+ /**
190
+ * Get tenant by slug
191
+ */
192
+ async getTenantBySlug(slug) {
193
+ if (!this.adapter.findTenantBySlug) {
194
+ return null;
195
+ }
196
+ return this.adapter.findTenantBySlug(slug);
197
+ }
198
+ /**
199
+ * Check if user is member of tenant
200
+ */
201
+ async isMember(userId, tenantId) {
202
+ if (!this.adapter.findMembership) {
203
+ return false;
204
+ }
205
+ const membership = await this.adapter.findMembership(userId, tenantId);
206
+ return membership !== null && membership.status === "active";
207
+ }
208
+ /**
209
+ * Check if user has specific role in tenant
210
+ */
211
+ async hasRole(userId, tenantId, role) {
212
+ if (!this.adapter.findMembership) {
213
+ return false;
214
+ }
215
+ const membership = await this.adapter.findMembership(userId, tenantId);
216
+ return membership !== null && membership.role === role && membership.status === "active";
217
+ }
218
+ /**
219
+ * Check if user is owner of tenant
220
+ */
221
+ async isOwner(userId, tenantId) {
222
+ return this.hasRole(userId, tenantId, "owner");
223
+ }
224
+ /**
225
+ * Check if user is admin of tenant (owner or admin)
226
+ */
227
+ async isAdmin(userId, tenantId) {
228
+ if (!this.adapter.findMembership) {
229
+ return false;
230
+ }
231
+ const membership = await this.adapter.findMembership(userId, tenantId);
232
+ if (!membership || membership.status !== "active") {
233
+ return false;
234
+ }
235
+ return membership.role === "owner" || membership.role === "admin";
236
+ }
237
+ // ============================================
238
+ // Membership Operations
239
+ // ============================================
240
+ /**
241
+ * Add a member to tenant
242
+ */
243
+ async addMember(input) {
244
+ if (!this.adapter.createMembership) {
245
+ throw new Error("Membership operations not supported by adapter");
246
+ }
247
+ const existing = await this.adapter.findMembership?.(input.userId, input.tenantId);
248
+ if (existing) {
249
+ throw new Error("User is already a member of this tenant");
250
+ }
251
+ return this.adapter.createMembership({
252
+ userId: input.userId,
253
+ tenantId: input.tenantId,
254
+ role: input.role,
255
+ permissions: input.permissions
256
+ });
257
+ }
258
+ /**
259
+ * Update member role/permissions
260
+ */
261
+ async updateMember(userId, tenantId, updates) {
262
+ if (!this.adapter.findMembership || !this.adapter.updateMembership) {
263
+ throw new Error("Membership operations not supported by adapter");
264
+ }
265
+ const membership = await this.adapter.findMembership(userId, tenantId);
266
+ if (!membership) {
267
+ throw new Error("Membership not found");
268
+ }
269
+ return this.adapter.updateMembership(membership.id, updates);
270
+ }
271
+ /**
272
+ * Remove member from tenant
273
+ */
274
+ async removeMember(userId, tenantId) {
275
+ if (!this.adapter.findMembership || !this.adapter.deleteMembership) {
276
+ throw new Error("Membership operations not supported by adapter");
277
+ }
278
+ const membership = await this.adapter.findMembership(userId, tenantId);
279
+ if (!membership) {
280
+ throw new Error("Membership not found");
281
+ }
282
+ if (membership.role === "owner") {
283
+ const allMemberships = await this.getMembersByTenant(tenantId);
284
+ const owners = allMemberships.filter((m) => m.role === "owner");
285
+ if (owners.length <= 1) {
286
+ throw new Error("Cannot remove the last owner of a tenant");
287
+ }
288
+ }
289
+ await this.adapter.deleteMembership(membership.id);
290
+ }
291
+ /**
292
+ * Get membership for user in tenant
293
+ */
294
+ async getMembership(userId, tenantId) {
295
+ if (!this.adapter.findMembership) {
296
+ return null;
297
+ }
298
+ return this.adapter.findMembership(userId, tenantId);
299
+ }
300
+ /**
301
+ * Get all tenants for a user
302
+ */
303
+ async getUserTenants(userId) {
304
+ if (!this.adapter.findMembershipsByUserId) {
305
+ return [];
306
+ }
307
+ const memberships = await this.adapter.findMembershipsByUserId(userId);
308
+ const enriched = await Promise.all(
309
+ memberships.map(async (membership) => {
310
+ const tenant = await this.adapter.findTenantById?.(membership.tenantId);
311
+ return {
312
+ ...membership,
313
+ tenant: tenant ?? void 0
314
+ };
315
+ })
316
+ );
317
+ return enriched;
318
+ }
319
+ /**
320
+ * Get all members of a tenant
321
+ * Note: This requires iterating through users, which is not efficient
322
+ * Consider adding findMembershipsByTenantId to the adapter
323
+ */
324
+ async getMembersByTenant(_tenantId) {
325
+ console.warn(
326
+ "getMembersByTenant: Consider implementing findMembershipsByTenantId in adapter"
327
+ );
328
+ return [];
329
+ }
330
+ /**
331
+ * Transfer ownership to another member
332
+ */
333
+ async transferOwnership(tenantId, currentOwnerId, newOwnerId) {
334
+ if (!this.adapter.findMembership || !this.adapter.updateMembership) {
335
+ throw new Error("Membership operations not supported by adapter");
336
+ }
337
+ const currentOwnership = await this.adapter.findMembership(currentOwnerId, tenantId);
338
+ if (!currentOwnership || currentOwnership.role !== "owner") {
339
+ throw new Error("Current user is not the owner");
340
+ }
341
+ const newOwnership = await this.adapter.findMembership(newOwnerId, tenantId);
342
+ if (!newOwnership) {
343
+ throw new Error("New owner must be an existing member");
344
+ }
345
+ await this.adapter.updateMembership(newOwnership.id, { role: "owner" });
346
+ await this.adapter.updateMembership(currentOwnership.id, { role: "admin" });
347
+ }
348
+ // ============================================
349
+ // Tenant Switching
350
+ // ============================================
351
+ /**
352
+ * Validate tenant switch
353
+ * Returns the tenant if switch is allowed
354
+ */
355
+ async validateTenantSwitch(userId, targetTenantId) {
356
+ const tenant = await this.getTenantById(targetTenantId);
357
+ if (!tenant) {
358
+ throw new Error("Tenant not found");
359
+ }
360
+ if (tenant.status !== "active") {
361
+ throw new Error("Tenant is not active");
362
+ }
363
+ const membership = await this.getMembership(userId, targetTenantId);
364
+ if (!membership) {
365
+ throw new Error("User is not a member of this tenant");
366
+ }
367
+ if (membership.status !== "active") {
368
+ throw new Error("Membership is not active");
369
+ }
370
+ return { tenant, membership };
371
+ }
372
+ /**
373
+ * Get default tenant for user
374
+ * Returns the first active tenant membership
375
+ */
376
+ async getDefaultTenant(userId) {
377
+ const tenants = await this.getUserTenants(userId);
378
+ const active = tenants.find(
379
+ (t) => t.status === "active" && t.tenant?.status === "active"
380
+ );
381
+ return active ?? null;
382
+ }
383
+ // ============================================
384
+ // Utility Methods
385
+ // ============================================
386
+ /**
387
+ * Generate URL-friendly slug from name
388
+ */
389
+ generateSlug(name) {
390
+ return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
391
+ }
392
+ /**
393
+ * Generate unique tenant slug
394
+ */
395
+ async generateUniqueSlug(name) {
396
+ const baseSlug = this.generateSlug(name);
397
+ let slug = baseSlug;
398
+ let counter = 1;
399
+ while (await this.adapter.findTenantBySlug?.(slug)) {
400
+ slug = `${baseSlug}-${counter}`;
401
+ counter++;
402
+ }
403
+ return slug;
404
+ }
405
+ };
406
+ function createTenantManager(adapter) {
407
+ return new TenantManager(adapter);
408
+ }
409
+
410
+ // src/core/tenant-resolver.ts
411
+ var TenantResolver = class {
412
+ config;
413
+ constructor(config) {
414
+ this.config = {
415
+ headerName: "x-tenant-id",
416
+ pathPrefix: "/t/",
417
+ queryParam: "tenant",
418
+ required: false,
419
+ ...config
420
+ };
421
+ }
422
+ /**
423
+ * Resolve tenant from request
424
+ */
425
+ async resolve(request) {
426
+ const { strategy } = this.config;
427
+ let result = {
428
+ tenantId: null,
429
+ resolvedFrom: null
430
+ };
431
+ switch (strategy) {
432
+ case "subdomain":
433
+ result = this.resolveFromSubdomain(request);
434
+ break;
435
+ case "header":
436
+ result = this.resolveFromHeader(request);
437
+ break;
438
+ case "path":
439
+ result = this.resolveFromPath(request);
440
+ break;
441
+ case "query":
442
+ result = this.resolveFromQuery(request);
443
+ break;
444
+ case "custom":
445
+ result = await this.resolveFromCustom(request);
446
+ break;
447
+ }
448
+ if (!result.tenantId && this.config.fallbackTenantId) {
449
+ result = {
450
+ tenantId: this.config.fallbackTenantId,
451
+ resolvedFrom: "fallback"
452
+ };
453
+ }
454
+ return result;
455
+ }
456
+ /**
457
+ * Resolve tenant from subdomain
458
+ * e.g., acme.example.com -> 'acme'
459
+ */
460
+ resolveFromSubdomain(request) {
461
+ const url = new URL(request.url);
462
+ const host = url.hostname;
463
+ const parts = host.split(".");
464
+ if (parts.length >= 3 || parts.length === 2 && parts[1] === "localhost") {
465
+ const subdomain = parts[0];
466
+ const skipSubdomains = ["www", "api", "app", "admin", "mail", "ftp"];
467
+ if (!skipSubdomains.includes(subdomain.toLowerCase())) {
468
+ return {
469
+ tenantId: subdomain,
470
+ resolvedFrom: "subdomain",
471
+ originalValue: subdomain
472
+ };
473
+ }
474
+ }
475
+ return { tenantId: null, resolvedFrom: null };
476
+ }
477
+ /**
478
+ * Resolve tenant from header
479
+ * e.g., X-Tenant-ID: acme
480
+ */
481
+ resolveFromHeader(request) {
482
+ const headerValue = request.headers.get(this.config.headerName);
483
+ if (headerValue) {
484
+ return {
485
+ tenantId: headerValue,
486
+ resolvedFrom: "header",
487
+ originalValue: headerValue
488
+ };
489
+ }
490
+ return { tenantId: null, resolvedFrom: null };
491
+ }
492
+ /**
493
+ * Resolve tenant from URL path
494
+ * e.g., /t/acme/api/users -> 'acme'
495
+ */
496
+ resolveFromPath(request) {
497
+ const url = new URL(request.url);
498
+ const pathname = url.pathname;
499
+ const prefix = this.config.pathPrefix;
500
+ if (pathname.startsWith(prefix)) {
501
+ const rest = pathname.slice(prefix.length);
502
+ const slashIndex = rest.indexOf("/");
503
+ const tenantSlug = slashIndex > 0 ? rest.slice(0, slashIndex) : rest;
504
+ if (tenantSlug) {
505
+ return {
506
+ tenantId: tenantSlug,
507
+ resolvedFrom: "path",
508
+ originalValue: tenantSlug
509
+ };
510
+ }
511
+ }
512
+ return { tenantId: null, resolvedFrom: null };
513
+ }
514
+ /**
515
+ * Resolve tenant from query parameter
516
+ * e.g., /api/users?tenant=acme -> 'acme'
517
+ */
518
+ resolveFromQuery(request) {
519
+ const url = new URL(request.url);
520
+ const tenantId = url.searchParams.get(this.config.queryParam);
521
+ if (tenantId) {
522
+ return {
523
+ tenantId,
524
+ resolvedFrom: "query",
525
+ originalValue: tenantId
526
+ };
527
+ }
528
+ return { tenantId: null, resolvedFrom: null };
529
+ }
530
+ /**
531
+ * Resolve tenant using custom resolver
532
+ */
533
+ async resolveFromCustom(request) {
534
+ if (!this.config.resolver) {
535
+ return { tenantId: null, resolvedFrom: null };
536
+ }
537
+ const tenantId = await this.config.resolver(request);
538
+ if (tenantId) {
539
+ return {
540
+ tenantId,
541
+ resolvedFrom: "custom",
542
+ originalValue: tenantId
543
+ };
544
+ }
545
+ return { tenantId: null, resolvedFrom: null };
546
+ }
547
+ /**
548
+ * Check if tenant is required but not found
549
+ */
550
+ isRequired() {
551
+ return this.config.required;
552
+ }
553
+ /**
554
+ * Get the path without tenant prefix (for path strategy)
555
+ */
556
+ stripTenantFromPath(pathname) {
557
+ if (this.config.strategy !== "path") {
558
+ return pathname;
559
+ }
560
+ const prefix = this.config.pathPrefix;
561
+ if (pathname.startsWith(prefix)) {
562
+ const rest = pathname.slice(prefix.length);
563
+ const slashIndex = rest.indexOf("/");
564
+ if (slashIndex > 0) {
565
+ return rest.slice(slashIndex);
566
+ }
567
+ return "/";
568
+ }
569
+ return pathname;
570
+ }
571
+ };
572
+ function createTenantResolver(config) {
573
+ return new TenantResolver(config);
574
+ }
575
+ var MultiStrategyTenantResolver = class {
576
+ resolvers;
577
+ fallbackTenantId;
578
+ constructor(strategies, options) {
579
+ this.resolvers = strategies.map((s) => new TenantResolver(s));
580
+ this.fallbackTenantId = options?.fallbackTenantId;
581
+ }
582
+ /**
583
+ * Resolve tenant trying each strategy in order
584
+ */
585
+ async resolve(request) {
586
+ for (const resolver of this.resolvers) {
587
+ const result = await resolver.resolve(request);
588
+ if (result.tenantId) {
589
+ return result;
590
+ }
591
+ }
592
+ if (this.fallbackTenantId) {
593
+ return {
594
+ tenantId: this.fallbackTenantId,
595
+ resolvedFrom: "fallback"
596
+ };
597
+ }
598
+ return { tenantId: null, resolvedFrom: null };
599
+ }
600
+ };
601
+ function createMultiStrategyResolver(strategies, options) {
602
+ return new MultiStrategyTenantResolver(strategies, options);
603
+ }
604
+
605
+ // src/core/invitation.ts
606
+ var InvitationService = class {
607
+ storage;
608
+ adapter;
609
+ config;
610
+ constructor(storage, adapter, config) {
611
+ this.storage = storage;
612
+ this.adapter = adapter;
613
+ this.config = {
614
+ callbackPath: "/auth/invitation",
615
+ expiresIn: 604800,
616
+ // 7 days
617
+ tokenLength: 32,
618
+ ...config
619
+ };
620
+ }
621
+ /**
622
+ * Send an invitation to join a tenant
623
+ */
624
+ async sendInvitation(input) {
625
+ const normalizedEmail = input.email.toLowerCase().trim();
626
+ const existingUser = await this.adapter.findUserByEmail(normalizedEmail);
627
+ if (existingUser) {
628
+ const membership = await this.adapter.findMembership?.(existingUser.id, input.tenantId);
629
+ if (membership && membership.status === "active") {
630
+ return { success: false, error: "User is already a member of this tenant" };
631
+ }
632
+ }
633
+ const existingInvitation = await this.getInvitationByEmail(normalizedEmail, input.tenantId);
634
+ if (existingInvitation && existingInvitation.status === "pending") {
635
+ await this.cancelInvitation(existingInvitation.id);
636
+ }
637
+ const token = await generateRandomHex(this.config.tokenLength);
638
+ const tokenHash = await sha256Hex(token);
639
+ const id = await generateRandomHex(16);
640
+ const expiresAt = new Date(Date.now() + this.config.expiresIn * 1e3);
641
+ const invitation = {
642
+ id,
643
+ email: normalizedEmail,
644
+ tenantId: input.tenantId,
645
+ role: input.role,
646
+ permissions: input.permissions,
647
+ invitedBy: input.invitedBy,
648
+ tokenHash,
649
+ expiresAt: expiresAt.toISOString(),
650
+ status: "pending",
651
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
652
+ message: input.message
653
+ };
654
+ await this.storage.set(
655
+ `invitation:hash:${tokenHash}`,
656
+ invitation,
657
+ this.config.expiresIn
658
+ );
659
+ await this.storage.set(
660
+ `invitation:id:${id}`,
661
+ invitation,
662
+ this.config.expiresIn
663
+ );
664
+ await this.storage.set(
665
+ `invitation:email:${normalizedEmail}:${input.tenantId}`,
666
+ id,
667
+ this.config.expiresIn
668
+ );
669
+ const params = new URLSearchParams({ token });
670
+ const invitationUrl = `${this.config.baseUrl}${this.config.callbackPath}?${params}`;
671
+ return {
672
+ success: true,
673
+ invitation,
674
+ invitationUrl,
675
+ token
676
+ };
677
+ }
678
+ /**
679
+ * Accept an invitation
680
+ */
681
+ async acceptInvitation(input) {
682
+ const tokenHash = await sha256Hex(input.token);
683
+ const invitation = await this.storage.get(`invitation:hash:${tokenHash}`);
684
+ if (!invitation) {
685
+ return { success: false, error: "Invalid or expired invitation" };
686
+ }
687
+ if (invitation.status !== "pending") {
688
+ return { success: false, error: `Invitation is ${invitation.status}` };
689
+ }
690
+ if (new Date(invitation.expiresAt) < /* @__PURE__ */ new Date()) {
691
+ invitation.status = "expired";
692
+ await this.updateInvitation(invitation);
693
+ return { success: false, error: "Invitation has expired" };
694
+ }
695
+ const existingMembership = await this.adapter.findMembership?.(
696
+ input.userId,
697
+ invitation.tenantId
698
+ );
699
+ if (existingMembership && existingMembership.status === "active") {
700
+ return { success: false, error: "You are already a member of this tenant" };
701
+ }
702
+ let membership;
703
+ if (existingMembership) {
704
+ membership = await this.adapter.updateMembership(existingMembership.id, {
705
+ role: invitation.role,
706
+ permissions: invitation.permissions,
707
+ status: "active"
708
+ });
709
+ } else {
710
+ membership = await this.adapter.createMembership({
711
+ userId: input.userId,
712
+ tenantId: invitation.tenantId,
713
+ role: invitation.role,
714
+ permissions: invitation.permissions
715
+ });
716
+ }
717
+ invitation.status = "accepted";
718
+ invitation.acceptedAt = (/* @__PURE__ */ new Date()).toISOString();
719
+ invitation.acceptedBy = input.userId;
720
+ await this.updateInvitation(invitation);
721
+ const tenant = await this.adapter.findTenantById?.(invitation.tenantId);
722
+ return {
723
+ success: true,
724
+ membership,
725
+ tenant: tenant ?? void 0
726
+ };
727
+ }
728
+ /**
729
+ * Check invitation status
730
+ */
731
+ async checkInvitation(token) {
732
+ const tokenHash = await sha256Hex(token);
733
+ const invitation = await this.storage.get(`invitation:hash:${tokenHash}`);
734
+ if (!invitation) {
735
+ return { valid: false, error: "Invalid or expired invitation" };
736
+ }
737
+ if (invitation.status !== "pending") {
738
+ return { valid: false, error: `Invitation is ${invitation.status}`, invitation };
739
+ }
740
+ if (new Date(invitation.expiresAt) < /* @__PURE__ */ new Date()) {
741
+ invitation.status = "expired";
742
+ await this.updateInvitation(invitation);
743
+ return { valid: false, error: "Invitation has expired", invitation };
744
+ }
745
+ const tenant = await this.adapter.findTenantById?.(invitation.tenantId);
746
+ return {
747
+ valid: true,
748
+ invitation,
749
+ tenant: tenant ?? void 0
750
+ };
751
+ }
752
+ /**
753
+ * Cancel an invitation
754
+ */
755
+ async cancelInvitation(invitationId) {
756
+ const invitation = await this.storage.get(`invitation:id:${invitationId}`);
757
+ if (!invitation) {
758
+ return false;
759
+ }
760
+ if (invitation.status !== "pending") {
761
+ return false;
762
+ }
763
+ invitation.status = "cancelled";
764
+ await this.updateInvitation(invitation);
765
+ return true;
766
+ }
767
+ /**
768
+ * Get invitation by ID
769
+ */
770
+ async getInvitationById(id) {
771
+ return this.storage.get(`invitation:id:${id}`);
772
+ }
773
+ /**
774
+ * Get invitation by email and tenant
775
+ */
776
+ async getInvitationByEmail(email, tenantId) {
777
+ const normalizedEmail = email.toLowerCase().trim();
778
+ const invitationId = await this.storage.get(
779
+ `invitation:email:${normalizedEmail}:${tenantId}`
780
+ );
781
+ if (!invitationId) {
782
+ return null;
783
+ }
784
+ return this.getInvitationById(invitationId);
785
+ }
786
+ /**
787
+ * Resend invitation (generates new token)
788
+ */
789
+ async resendInvitation(invitationId, invitedBy) {
790
+ const invitation = await this.getInvitationById(invitationId);
791
+ if (!invitation) {
792
+ return { success: false, error: "Invitation not found" };
793
+ }
794
+ if (invitation.status !== "pending") {
795
+ return { success: false, error: `Cannot resend ${invitation.status} invitation` };
796
+ }
797
+ await this.cancelInvitation(invitationId);
798
+ return this.sendInvitation({
799
+ email: invitation.email,
800
+ tenantId: invitation.tenantId,
801
+ role: invitation.role,
802
+ permissions: invitation.permissions,
803
+ invitedBy,
804
+ message: invitation.message
805
+ });
806
+ }
807
+ /**
808
+ * Get pending invitations for a tenant
809
+ * Note: This requires listing keys which may not be efficient for all storage backends
810
+ */
811
+ async getPendingInvitations(_tenantId) {
812
+ console.warn(
813
+ "getPendingInvitations: Consider implementing list operation in storage"
814
+ );
815
+ return [];
816
+ }
817
+ /**
818
+ * Update invitation record
819
+ */
820
+ async updateInvitation(invitation) {
821
+ const ttl = Math.max(
822
+ 0,
823
+ Math.floor((new Date(invitation.expiresAt).getTime() - Date.now()) / 1e3)
824
+ );
825
+ await this.storage.set(`invitation:id:${invitation.id}`, invitation, ttl || 300);
826
+ await this.storage.set(
827
+ `invitation:hash:${invitation.tokenHash}`,
828
+ invitation,
829
+ ttl || 300
830
+ );
831
+ }
832
+ };
833
+ function createInvitationService(storage, adapter, config) {
834
+ return new InvitationService(storage, adapter, config);
835
+ }
836
+
837
+ // src/core/auth-engine.ts
838
+ var ParsAuthEngine = class {
839
+ config;
840
+ storage;
841
+ providers;
842
+ jwtManager;
843
+ sessionBlocklist;
844
+ adapter;
845
+ callbacks;
846
+ initialized = false;
847
+ // Multi-tenant components
848
+ tenantManager;
849
+ tenantResolver;
850
+ invitationService;
851
+ constructor(config) {
852
+ validateConfig(config);
853
+ this.config = mergeConfig(config);
854
+ this.adapter = config.adapter;
855
+ this.callbacks = config.callbacks ?? {};
856
+ this.providers = new ProviderRegistry();
857
+ const sessionConfig = this.config.session;
858
+ const jwtIssuer = this.config.jwt.issuer;
859
+ const issuer = Array.isArray(jwtIssuer) ? jwtIssuer[0] ?? "pars-auth" : jwtIssuer ?? "pars-auth";
860
+ const jwtAudience = this.config.jwt.audience;
861
+ const audience = Array.isArray(jwtAudience) ? jwtAudience[0] ?? "pars-client" : jwtAudience ?? "pars-client";
862
+ this.jwtManager = new JwtManager({
863
+ secret: this.config.secret,
864
+ issuer,
865
+ audience,
866
+ accessTokenTTL: `${sessionConfig.accessTokenExpiry}s`,
867
+ refreshTokenTTL: `${sessionConfig.refreshTokenExpiry}s`
868
+ });
869
+ }
870
+ /**
871
+ * Initialize the auth engine (async operations)
872
+ * Must be called before using the engine
873
+ */
874
+ async initialize() {
875
+ if (this.initialized) return;
876
+ this.storage = await createStorage(this.config.storage);
877
+ this.sessionBlocklist = new SessionBlocklist(this.storage);
878
+ if (this.config.providers.otp?.enabled !== false) {
879
+ const otpProvider = new OTPProvider(this.storage, this.config.providers.otp);
880
+ this.providers.register(otpProvider);
881
+ }
882
+ this.tenantManager = createTenantManager(this.adapter);
883
+ if (this.config.tenant?.enabled !== false) {
884
+ this.tenantResolver = createTenantResolver({
885
+ strategy: this.config.tenant.strategy ?? "header",
886
+ headerName: this.config.tenant.headerName ?? "x-tenant-id"
887
+ });
888
+ }
889
+ if (this.config.baseUrl) {
890
+ this.invitationService = createInvitationService(
891
+ this.storage,
892
+ this.adapter,
893
+ { baseUrl: this.config.baseUrl }
894
+ );
895
+ }
896
+ this.initialized = true;
897
+ }
898
+ /**
899
+ * Ensure engine is initialized
900
+ */
901
+ ensureInitialized() {
902
+ if (!this.initialized) {
903
+ throw new Error("[Pars Auth] Engine not initialized. Call initialize() first.");
904
+ }
905
+ }
906
+ /**
907
+ * Get all registered providers
908
+ */
909
+ getProviders() {
910
+ return this.providers.getEnabled().map((p) => p.getInfo());
911
+ }
912
+ /**
913
+ * Check if a provider is enabled
914
+ */
915
+ isProviderEnabled(name) {
916
+ const provider = this.providers.get(name);
917
+ return provider?.enabled ?? false;
918
+ }
919
+ /**
920
+ * Request OTP (for OTP provider)
921
+ */
922
+ async requestOTP(input) {
923
+ this.ensureInitialized();
924
+ const otpProvider = this.providers.get("otp");
925
+ if (!otpProvider) {
926
+ return {
927
+ success: false,
928
+ error: "OTP provider not enabled"
929
+ };
930
+ }
931
+ return otpProvider.requestOTP(input);
932
+ }
933
+ /**
934
+ * Sign in with any provider
935
+ */
936
+ async signIn(input) {
937
+ this.ensureInitialized();
938
+ const { provider: providerName, identifier, credential, data, metadata } = input;
939
+ const provider = this.providers.get(providerName);
940
+ if (!provider) {
941
+ return {
942
+ success: false,
943
+ error: `Provider '${providerName}' not found`,
944
+ errorCode: "PROVIDER_NOT_FOUND"
945
+ };
946
+ }
947
+ if (!provider.enabled) {
948
+ return {
949
+ success: false,
950
+ error: `Provider '${providerName}' is not enabled`,
951
+ errorCode: "PROVIDER_DISABLED"
952
+ };
953
+ }
954
+ const authResult = await provider.authenticate({
955
+ identifier,
956
+ credential: credential ?? "",
957
+ data
958
+ });
959
+ if (!authResult.success) {
960
+ return {
961
+ success: false,
962
+ error: authResult.error,
963
+ errorCode: authResult.errorCode
964
+ };
965
+ }
966
+ let user = await this.findUserByIdentifier(identifier, providerName);
967
+ if (!user) {
968
+ if (providerName === "otp") {
969
+ const otpType = data?.["type"];
970
+ const createResult = await this.signUp({
971
+ email: otpType === "email" || !otpType ? identifier : void 0,
972
+ phone: otpType === "sms" ? identifier : void 0,
973
+ metadata
974
+ });
975
+ if (!createResult.success) {
976
+ return {
977
+ success: false,
978
+ error: createResult.error,
979
+ errorCode: createResult.errorCode
980
+ };
981
+ }
982
+ user = createResult.user;
983
+ } else {
984
+ return {
985
+ success: false,
986
+ error: "User not found",
987
+ errorCode: "USER_NOT_FOUND"
988
+ };
989
+ }
990
+ }
991
+ if (this.callbacks.validateSignIn) {
992
+ const allowed = await this.callbacks.validateSignIn(user);
993
+ if (!allowed) {
994
+ return {
995
+ success: false,
996
+ error: "Sign in not allowed",
997
+ errorCode: "SIGN_IN_REJECTED"
998
+ };
999
+ }
1000
+ }
1001
+ if (user.twoFactorEnabled && providerName !== "totp") {
1002
+ return {
1003
+ success: true,
1004
+ user,
1005
+ requiresTwoFactor: true,
1006
+ twoFactorChallengeId: ""
1007
+ // Generate challenge ID
1008
+ };
1009
+ }
1010
+ const session = await this.createSession(user.id, metadata);
1011
+ if (!session) {
1012
+ return {
1013
+ success: false,
1014
+ error: "Failed to create session",
1015
+ errorCode: "SESSION_CREATE_FAILED"
1016
+ };
1017
+ }
1018
+ const tokens = await this.jwtManager.generateTokenPair({
1019
+ userId: user.id,
1020
+ tenantId: metadata?.tenantId,
1021
+ sessionId: session.id
1022
+ });
1023
+ if (this.callbacks.onSignIn) {
1024
+ await this.callbacks.onSignIn(user, session);
1025
+ }
1026
+ return {
1027
+ success: true,
1028
+ user,
1029
+ session,
1030
+ tokens
1031
+ };
1032
+ }
1033
+ /**
1034
+ * Sign up a new user
1035
+ */
1036
+ async signUp(input) {
1037
+ this.ensureInitialized();
1038
+ const { email, phone, name, avatar, metadata } = input;
1039
+ if (!email && !phone) {
1040
+ return {
1041
+ success: false,
1042
+ error: "Email or phone is required",
1043
+ errorCode: "INVALID_INPUT"
1044
+ };
1045
+ }
1046
+ if (email) {
1047
+ const existing = await this.adapter.findUserByEmail(email);
1048
+ if (existing) {
1049
+ return {
1050
+ success: false,
1051
+ error: "User with this email already exists",
1052
+ errorCode: "USER_EXISTS"
1053
+ };
1054
+ }
1055
+ }
1056
+ if (phone) {
1057
+ const existing = await this.adapter.findUserByPhone(phone);
1058
+ if (existing) {
1059
+ return {
1060
+ success: false,
1061
+ error: "User with this phone already exists",
1062
+ errorCode: "USER_EXISTS"
1063
+ };
1064
+ }
1065
+ }
1066
+ const user = await this.adapter.createUser({
1067
+ email,
1068
+ phone,
1069
+ name,
1070
+ avatar,
1071
+ emailVerified: !!email,
1072
+ // OTP verification counts as email verification
1073
+ phoneVerified: !!phone
1074
+ });
1075
+ const session = await this.createSession(user.id, metadata);
1076
+ const tokens = session ? await this.jwtManager.generateTokenPair({
1077
+ userId: user.id,
1078
+ tenantId: metadata?.tenantId,
1079
+ sessionId: session.id
1080
+ }) : void 0;
1081
+ if (this.callbacks.onSignUp) {
1082
+ await this.callbacks.onSignUp(user);
1083
+ }
1084
+ return {
1085
+ success: true,
1086
+ user,
1087
+ session: session ?? void 0,
1088
+ tokens
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Sign out (revoke session)
1093
+ */
1094
+ async signOut(sessionId, options) {
1095
+ this.ensureInitialized();
1096
+ if (options?.revokeAll && options?.userId) {
1097
+ const sessions = await this.adapter.findSessionsByUserId(options.userId);
1098
+ await this.adapter.deleteSessionsByUserId(options.userId);
1099
+ const tokenExpiry = new Date(
1100
+ Date.now() + this.config.session.refreshTokenExpiry * 1e3
1101
+ );
1102
+ await this.sessionBlocklist.blockAllUserSessions(
1103
+ options.userId,
1104
+ sessions.map((s) => s.id),
1105
+ tokenExpiry,
1106
+ "Sign out all"
1107
+ );
1108
+ if (this.callbacks.onSignOut) {
1109
+ for (const session of sessions) {
1110
+ await this.callbacks.onSignOut(options.userId, session.id);
1111
+ }
1112
+ }
1113
+ } else {
1114
+ const session = await this.adapter.findSessionById(sessionId);
1115
+ if (session) {
1116
+ await this.adapter.deleteSession(sessionId);
1117
+ const tokenExpiry = new Date(
1118
+ Date.now() + this.config.session.refreshTokenExpiry * 1e3
1119
+ );
1120
+ await this.sessionBlocklist.blockSession(sessionId, tokenExpiry, {
1121
+ reason: "Sign out",
1122
+ userId: session.userId
1123
+ });
1124
+ if (this.callbacks.onSignOut) {
1125
+ await this.callbacks.onSignOut(session.userId, sessionId);
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Verify access token
1132
+ */
1133
+ async verifyAccessToken(token) {
1134
+ this.ensureInitialized();
1135
+ try {
1136
+ const payload = await this.jwtManager.verifyAccessToken(token);
1137
+ if (payload.sid) {
1138
+ const isBlocked = await this.sessionBlocklist.isBlocked(payload.sid);
1139
+ if (isBlocked) {
1140
+ return {
1141
+ valid: false,
1142
+ error: "Session has been revoked"
1143
+ };
1144
+ }
1145
+ }
1146
+ return {
1147
+ valid: true,
1148
+ payload
1149
+ };
1150
+ } catch (error) {
1151
+ return {
1152
+ valid: false,
1153
+ error: error instanceof Error ? error.message : "Invalid token"
1154
+ };
1155
+ }
1156
+ }
1157
+ /**
1158
+ * Refresh tokens
1159
+ */
1160
+ async refreshTokens(refreshToken) {
1161
+ this.ensureInitialized();
1162
+ try {
1163
+ const { userId, sessionId, tenantId } = await this.jwtManager.verifyRefreshToken(refreshToken);
1164
+ if (sessionId) {
1165
+ const isBlocked = await this.sessionBlocklist.isBlocked(sessionId);
1166
+ if (isBlocked) {
1167
+ return {
1168
+ success: false,
1169
+ error: "Session has been revoked"
1170
+ };
1171
+ }
1172
+ }
1173
+ if (sessionId) {
1174
+ const session = await this.adapter.findSessionById(sessionId);
1175
+ if (!session || session.status !== "active") {
1176
+ return {
1177
+ success: false,
1178
+ error: "Session not found or inactive"
1179
+ };
1180
+ }
1181
+ if (this.config.session.slidingWindow) {
1182
+ const newExpiresAt = new Date(
1183
+ Date.now() + this.config.session.refreshTokenExpiry * 1e3
1184
+ );
1185
+ await this.adapter.updateSession(sessionId, {
1186
+ expiresAt: newExpiresAt
1187
+ });
1188
+ }
1189
+ }
1190
+ const tokens = await this.jwtManager.generateTokenPair({
1191
+ userId,
1192
+ tenantId,
1193
+ sessionId
1194
+ });
1195
+ return {
1196
+ success: true,
1197
+ tokens
1198
+ };
1199
+ } catch (error) {
1200
+ return {
1201
+ success: false,
1202
+ error: error instanceof Error ? error.message : "Invalid refresh token"
1203
+ };
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Get user sessions
1208
+ */
1209
+ async getSessions(userId, currentSessionId) {
1210
+ this.ensureInitialized();
1211
+ const sessions = await this.adapter.findSessionsByUserId(userId);
1212
+ return sessions.filter((s) => s.status === "active").map((s) => ({
1213
+ id: s.id,
1214
+ userId: s.userId,
1215
+ tenantId: s.tenantId ?? void 0,
1216
+ deviceType: s.deviceType ?? void 0,
1217
+ deviceName: s.deviceName ?? void 0,
1218
+ ipAddress: s.ipAddress ?? void 0,
1219
+ createdAt: s.createdAt,
1220
+ expiresAt: s.expiresAt,
1221
+ isCurrent: s.id === currentSessionId
1222
+ }));
1223
+ }
1224
+ /**
1225
+ * Revoke a specific session
1226
+ */
1227
+ async revokeSession(sessionId) {
1228
+ await this.signOut(sessionId);
1229
+ }
1230
+ /**
1231
+ * Revoke all sessions for a user
1232
+ */
1233
+ async revokeAllSessions(userId) {
1234
+ await this.signOut("", { revokeAll: true, userId });
1235
+ }
1236
+ /**
1237
+ * Get the underlying storage instance
1238
+ */
1239
+ getStorage() {
1240
+ this.ensureInitialized();
1241
+ return this.storage;
1242
+ }
1243
+ /**
1244
+ * Get the JWT manager
1245
+ */
1246
+ getJwtManager() {
1247
+ return this.jwtManager;
1248
+ }
1249
+ /**
1250
+ * Get the database adapter
1251
+ */
1252
+ getAdapter() {
1253
+ return this.adapter;
1254
+ }
1255
+ /**
1256
+ * Get configuration
1257
+ */
1258
+ getConfig() {
1259
+ return this.config;
1260
+ }
1261
+ /**
1262
+ * Find user by identifier based on provider
1263
+ */
1264
+ async findUserByIdentifier(identifier, provider) {
1265
+ switch (provider) {
1266
+ case "otp":
1267
+ const userByEmail = await this.adapter.findUserByEmail(identifier);
1268
+ if (userByEmail) return userByEmail;
1269
+ return this.adapter.findUserByPhone(identifier);
1270
+ case "password":
1271
+ return this.adapter.findUserByEmail(identifier);
1272
+ default:
1273
+ const authMethod = await this.adapter.findAuthMethod(provider, identifier);
1274
+ if (authMethod) {
1275
+ return this.adapter.findUserById(authMethod.userId);
1276
+ }
1277
+ return null;
1278
+ }
1279
+ }
1280
+ /**
1281
+ * Create a new session
1282
+ */
1283
+ async createSession(userId, metadata) {
1284
+ const existingSessions = await this.adapter.findSessionsByUserId(userId);
1285
+ const activeSessions = existingSessions.filter((s) => s.status === "active");
1286
+ if (activeSessions.length >= this.config.session.maxSessions) {
1287
+ const oldest = activeSessions.sort(
1288
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
1289
+ )[0];
1290
+ if (oldest) {
1291
+ await this.adapter.deleteSession(oldest.id);
1292
+ await this.sessionBlocklist.blockSession(
1293
+ oldest.id,
1294
+ new Date(Date.now() + this.config.session.refreshTokenExpiry * 1e3),
1295
+ { reason: "Session limit reached", userId }
1296
+ );
1297
+ }
1298
+ }
1299
+ const expiresAt = new Date(
1300
+ Date.now() + this.config.session.accessTokenExpiry * 1e3
1301
+ );
1302
+ const refreshExpiresAt = new Date(
1303
+ Date.now() + this.config.session.refreshTokenExpiry * 1e3
1304
+ );
1305
+ const session = await this.adapter.createSession({
1306
+ userId,
1307
+ tenantId: metadata?.tenantId,
1308
+ expiresAt,
1309
+ refreshExpiresAt,
1310
+ deviceType: metadata?.deviceType,
1311
+ deviceName: metadata?.deviceName,
1312
+ userAgent: metadata?.userAgent,
1313
+ ipAddress: metadata?.ipAddress
1314
+ });
1315
+ if (this.callbacks.onSessionCreated) {
1316
+ await this.callbacks.onSessionCreated({ id: session.id, userId });
1317
+ }
1318
+ return session;
1319
+ }
1320
+ // ============================================
1321
+ // MULTI-TENANT OPERATIONS
1322
+ // ============================================
1323
+ /**
1324
+ * Resolve tenant from request
1325
+ */
1326
+ async resolveTenant(request) {
1327
+ this.ensureInitialized();
1328
+ if (!this.tenantResolver) {
1329
+ return { tenantId: null, resolvedFrom: null };
1330
+ }
1331
+ return this.tenantResolver.resolve(request);
1332
+ }
1333
+ /**
1334
+ * Switch current session to a different tenant
1335
+ */
1336
+ async switchTenant(sessionId, targetTenantId) {
1337
+ this.ensureInitialized();
1338
+ const session = await this.adapter.findSessionById(sessionId);
1339
+ if (!session) {
1340
+ return { success: false, error: "Session not found" };
1341
+ }
1342
+ try {
1343
+ await this.tenantManager.validateTenantSwitch(session.userId, targetTenantId);
1344
+ } catch (error) {
1345
+ return {
1346
+ success: false,
1347
+ error: error instanceof Error ? error.message : "Tenant switch not allowed"
1348
+ };
1349
+ }
1350
+ await this.adapter.updateSession(sessionId, {
1351
+ tenantId: targetTenantId
1352
+ });
1353
+ const tokens = await this.jwtManager.generateTokenPair({
1354
+ userId: session.userId,
1355
+ tenantId: targetTenantId,
1356
+ sessionId: session.id
1357
+ });
1358
+ return { success: true, tokens };
1359
+ }
1360
+ /**
1361
+ * Get all tenants for current user
1362
+ */
1363
+ async getUserTenants(userId) {
1364
+ this.ensureInitialized();
1365
+ const memberships = await this.tenantManager.getUserTenants(userId);
1366
+ return memberships.filter((m) => m.tenant && m.status === "active").map((m) => ({
1367
+ tenant: m.tenant,
1368
+ membership: m
1369
+ }));
1370
+ }
1371
+ /**
1372
+ * Check if user is member of tenant
1373
+ */
1374
+ async isTenantMember(userId, tenantId) {
1375
+ this.ensureInitialized();
1376
+ return this.tenantManager.isMember(userId, tenantId);
1377
+ }
1378
+ /**
1379
+ * Check if user has role in tenant
1380
+ */
1381
+ async hasRoleInTenant(userId, tenantId, role) {
1382
+ this.ensureInitialized();
1383
+ return this.tenantManager.hasRole(userId, tenantId, role);
1384
+ }
1385
+ /**
1386
+ * Get user's membership in a tenant
1387
+ */
1388
+ async getTenantMembership(userId, tenantId) {
1389
+ this.ensureInitialized();
1390
+ return this.tenantManager.getMembership(userId, tenantId);
1391
+ }
1392
+ /**
1393
+ * Add member to tenant
1394
+ */
1395
+ async addTenantMember(input) {
1396
+ this.ensureInitialized();
1397
+ return this.tenantManager.addMember(input);
1398
+ }
1399
+ /**
1400
+ * Update member's role/permissions in tenant
1401
+ */
1402
+ async updateTenantMember(userId, tenantId, updates) {
1403
+ this.ensureInitialized();
1404
+ return this.tenantManager.updateMember(userId, tenantId, updates);
1405
+ }
1406
+ /**
1407
+ * Remove member from tenant
1408
+ */
1409
+ async removeTenantMember(userId, tenantId) {
1410
+ this.ensureInitialized();
1411
+ return this.tenantManager.removeMember(userId, tenantId);
1412
+ }
1413
+ /**
1414
+ * Send invitation to join tenant
1415
+ */
1416
+ async inviteToTenant(input) {
1417
+ this.ensureInitialized();
1418
+ if (!this.invitationService) {
1419
+ return { success: false, error: "Invitation service not configured. Set baseUrl in config." };
1420
+ }
1421
+ return this.invitationService.sendInvitation(input);
1422
+ }
1423
+ /**
1424
+ * Accept an invitation
1425
+ */
1426
+ async acceptInvitation(token, userId) {
1427
+ this.ensureInitialized();
1428
+ if (!this.invitationService) {
1429
+ return { success: false, error: "Invitation service not configured" };
1430
+ }
1431
+ return this.invitationService.acceptInvitation({ token, userId });
1432
+ }
1433
+ /**
1434
+ * Check invitation status
1435
+ */
1436
+ async checkInvitation(token) {
1437
+ this.ensureInitialized();
1438
+ if (!this.invitationService) {
1439
+ return { valid: false, error: "Invitation service not configured" };
1440
+ }
1441
+ const result = await this.invitationService.checkInvitation(token);
1442
+ if (!result.valid) {
1443
+ return { valid: false, error: result.error };
1444
+ }
1445
+ return {
1446
+ valid: true,
1447
+ tenantName: result.tenant?.name,
1448
+ role: result.invitation?.role,
1449
+ email: result.invitation?.email
1450
+ };
1451
+ }
1452
+ /**
1453
+ * Get tenant manager instance
1454
+ */
1455
+ getTenantManager() {
1456
+ this.ensureInitialized();
1457
+ return this.tenantManager;
1458
+ }
1459
+ /**
1460
+ * Get invitation service instance
1461
+ */
1462
+ getInvitationService() {
1463
+ this.ensureInitialized();
1464
+ return this.invitationService;
1465
+ }
1466
+ /**
1467
+ * Get tenant resolver instance
1468
+ */
1469
+ getTenantResolver() {
1470
+ return this.tenantResolver;
1471
+ }
1472
+ };
1473
+ function createAuthEngine(config) {
1474
+ return new ParsAuthEngine(config);
1475
+ }
1476
+
1477
+ // src/providers/oauth/google.ts
1478
+ var GoogleProvider = class {
1479
+ name = "google";
1480
+ config;
1481
+ constructor(config) {
1482
+ this.config = config;
1483
+ }
1484
+ async getAuthorizationUrl(state, codeChallenge) {
1485
+ const params = new URLSearchParams({
1486
+ client_id: this.config.clientId,
1487
+ redirect_uri: this.config.redirectUri,
1488
+ response_type: "code",
1489
+ scope: this.config.scopes?.join(" ") ?? "openid email profile",
1490
+ state,
1491
+ access_type: "offline",
1492
+ prompt: "consent"
1493
+ });
1494
+ if (codeChallenge) {
1495
+ params.set("code_challenge", codeChallenge);
1496
+ params.set("code_challenge_method", "S256");
1497
+ }
1498
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
1499
+ }
1500
+ async exchangeCode(code, codeVerifier) {
1501
+ const params = {
1502
+ client_id: this.config.clientId,
1503
+ client_secret: this.config.clientSecret,
1504
+ code,
1505
+ grant_type: "authorization_code",
1506
+ redirect_uri: this.config.redirectUri
1507
+ };
1508
+ if (codeVerifier) {
1509
+ params["code_verifier"] = codeVerifier;
1510
+ }
1511
+ const response = await fetch("https://oauth2.googleapis.com/token", {
1512
+ method: "POST",
1513
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1514
+ body: new URLSearchParams(params)
1515
+ });
1516
+ if (!response.ok) {
1517
+ const error = await response.text();
1518
+ throw new Error(`Google token exchange failed: ${error}`);
1519
+ }
1520
+ const data = await response.json();
1521
+ return {
1522
+ accessToken: data["access_token"],
1523
+ refreshToken: data["refresh_token"],
1524
+ expiresIn: data["expires_in"],
1525
+ idToken: data["id_token"],
1526
+ tokenType: data["token_type"],
1527
+ scope: data["scope"]
1528
+ };
1529
+ }
1530
+ async getUserInfo(accessToken) {
1531
+ const response = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
1532
+ headers: { Authorization: `Bearer ${accessToken}` }
1533
+ });
1534
+ if (!response.ok) {
1535
+ throw new Error("Failed to fetch Google user info");
1536
+ }
1537
+ const data = await response.json();
1538
+ return {
1539
+ id: data["sub"],
1540
+ email: data["email"],
1541
+ name: data["name"],
1542
+ avatarUrl: data["picture"],
1543
+ emailVerified: data["email_verified"] ?? false,
1544
+ raw: data
1545
+ };
1546
+ }
1547
+ async refreshToken(refreshToken) {
1548
+ const response = await fetch("https://oauth2.googleapis.com/token", {
1549
+ method: "POST",
1550
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1551
+ body: new URLSearchParams({
1552
+ client_id: this.config.clientId,
1553
+ client_secret: this.config.clientSecret,
1554
+ refresh_token: refreshToken,
1555
+ grant_type: "refresh_token"
1556
+ })
1557
+ });
1558
+ if (!response.ok) {
1559
+ const error = await response.text();
1560
+ throw new Error(`Google token refresh failed: ${error}`);
1561
+ }
1562
+ const data = await response.json();
1563
+ return {
1564
+ accessToken: data["access_token"],
1565
+ refreshToken: data["refresh_token"] ?? refreshToken,
1566
+ expiresIn: data["expires_in"],
1567
+ idToken: data["id_token"],
1568
+ tokenType: data["token_type"],
1569
+ scope: data["scope"]
1570
+ };
1571
+ }
1572
+ };
1573
+
1574
+ // src/providers/oauth/github.ts
1575
+ var GitHubProvider = class {
1576
+ name = "github";
1577
+ config;
1578
+ constructor(config) {
1579
+ this.config = config;
1580
+ }
1581
+ async getAuthorizationUrl(state, _codeChallenge) {
1582
+ const params = new URLSearchParams({
1583
+ client_id: this.config.clientId,
1584
+ redirect_uri: this.config.redirectUri,
1585
+ scope: this.config.scopes?.join(" ") ?? "read:user user:email",
1586
+ state
1587
+ });
1588
+ return `https://github.com/login/oauth/authorize?${params}`;
1589
+ }
1590
+ async exchangeCode(code, _codeVerifier) {
1591
+ const response = await fetch("https://github.com/login/oauth/access_token", {
1592
+ method: "POST",
1593
+ headers: {
1594
+ "Content-Type": "application/json",
1595
+ Accept: "application/json"
1596
+ },
1597
+ body: JSON.stringify({
1598
+ client_id: this.config.clientId,
1599
+ client_secret: this.config.clientSecret,
1600
+ code,
1601
+ redirect_uri: this.config.redirectUri
1602
+ })
1603
+ });
1604
+ if (!response.ok) {
1605
+ const error = await response.text();
1606
+ throw new Error(`GitHub token exchange failed: ${error}`);
1607
+ }
1608
+ const data = await response.json();
1609
+ if (data["error"]) {
1610
+ throw new Error(`GitHub OAuth error: ${data["error_description"] ?? data["error"]}`);
1611
+ }
1612
+ return {
1613
+ accessToken: data["access_token"],
1614
+ tokenType: data["token_type"],
1615
+ scope: data["scope"]
1616
+ };
1617
+ }
1618
+ async getUserInfo(accessToken) {
1619
+ const userResponse = await fetch("https://api.github.com/user", {
1620
+ headers: {
1621
+ Authorization: `Bearer ${accessToken}`,
1622
+ Accept: "application/vnd.github.v3+json"
1623
+ }
1624
+ });
1625
+ if (!userResponse.ok) {
1626
+ throw new Error("Failed to fetch GitHub user info");
1627
+ }
1628
+ const userData = await userResponse.json();
1629
+ let email = userData["email"];
1630
+ let emailVerified = false;
1631
+ if (!email) {
1632
+ try {
1633
+ const emailsResponse = await fetch("https://api.github.com/user/emails", {
1634
+ headers: {
1635
+ Authorization: `Bearer ${accessToken}`,
1636
+ Accept: "application/vnd.github.v3+json"
1637
+ }
1638
+ });
1639
+ if (emailsResponse.ok) {
1640
+ const emails = await emailsResponse.json();
1641
+ const primaryEmail = emails.find((e) => e.primary && e.verified);
1642
+ if (primaryEmail) {
1643
+ email = primaryEmail.email;
1644
+ emailVerified = primaryEmail.verified;
1645
+ }
1646
+ }
1647
+ } catch {
1648
+ }
1649
+ }
1650
+ return {
1651
+ id: String(userData["id"]),
1652
+ email: email ?? "",
1653
+ name: userData["name"] ?? userData["login"],
1654
+ avatarUrl: userData["avatar_url"],
1655
+ emailVerified,
1656
+ raw: userData
1657
+ };
1658
+ }
1659
+ };
1660
+
1661
+ // src/providers/oauth/microsoft.ts
1662
+ var MicrosoftProvider = class {
1663
+ name = "microsoft";
1664
+ config;
1665
+ baseUrl;
1666
+ constructor(config) {
1667
+ this.config = config;
1668
+ const tenant = config.tenantId ?? "common";
1669
+ this.baseUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0`;
1670
+ }
1671
+ async getAuthorizationUrl(state, codeChallenge) {
1672
+ const params = new URLSearchParams({
1673
+ client_id: this.config.clientId,
1674
+ redirect_uri: this.config.redirectUri,
1675
+ response_type: "code",
1676
+ scope: this.config.scopes?.join(" ") ?? "openid email profile User.Read",
1677
+ state,
1678
+ response_mode: "query"
1679
+ });
1680
+ if (codeChallenge) {
1681
+ params.set("code_challenge", codeChallenge);
1682
+ params.set("code_challenge_method", "S256");
1683
+ }
1684
+ return `${this.baseUrl}/authorize?${params}`;
1685
+ }
1686
+ async exchangeCode(code, codeVerifier) {
1687
+ const params = {
1688
+ client_id: this.config.clientId,
1689
+ client_secret: this.config.clientSecret,
1690
+ code,
1691
+ grant_type: "authorization_code",
1692
+ redirect_uri: this.config.redirectUri
1693
+ };
1694
+ if (codeVerifier) {
1695
+ params["code_verifier"] = codeVerifier;
1696
+ }
1697
+ const response = await fetch(`${this.baseUrl}/token`, {
1698
+ method: "POST",
1699
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1700
+ body: new URLSearchParams(params)
1701
+ });
1702
+ if (!response.ok) {
1703
+ const error = await response.text();
1704
+ throw new Error(`Microsoft token exchange failed: ${error}`);
1705
+ }
1706
+ const data = await response.json();
1707
+ return {
1708
+ accessToken: data["access_token"],
1709
+ refreshToken: data["refresh_token"],
1710
+ expiresIn: data["expires_in"],
1711
+ idToken: data["id_token"],
1712
+ tokenType: data["token_type"],
1713
+ scope: data["scope"]
1714
+ };
1715
+ }
1716
+ async getUserInfo(accessToken) {
1717
+ const response = await fetch("https://graph.microsoft.com/v1.0/me", {
1718
+ headers: { Authorization: `Bearer ${accessToken}` }
1719
+ });
1720
+ if (!response.ok) {
1721
+ throw new Error("Failed to fetch Microsoft user info");
1722
+ }
1723
+ const data = await response.json();
1724
+ return {
1725
+ id: data["id"],
1726
+ email: data["mail"] ?? data["userPrincipalName"],
1727
+ name: data["displayName"],
1728
+ avatarUrl: void 0,
1729
+ // Would need separate call to get photo
1730
+ emailVerified: true,
1731
+ // Microsoft accounts are verified
1732
+ raw: data
1733
+ };
1734
+ }
1735
+ async refreshToken(refreshToken) {
1736
+ const response = await fetch(`${this.baseUrl}/token`, {
1737
+ method: "POST",
1738
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1739
+ body: new URLSearchParams({
1740
+ client_id: this.config.clientId,
1741
+ client_secret: this.config.clientSecret,
1742
+ refresh_token: refreshToken,
1743
+ grant_type: "refresh_token",
1744
+ scope: this.config.scopes?.join(" ") ?? "openid email profile User.Read"
1745
+ })
1746
+ });
1747
+ if (!response.ok) {
1748
+ const error = await response.text();
1749
+ throw new Error(`Microsoft token refresh failed: ${error}`);
1750
+ }
1751
+ const data = await response.json();
1752
+ return {
1753
+ accessToken: data["access_token"],
1754
+ refreshToken: data["refresh_token"] ?? refreshToken,
1755
+ expiresIn: data["expires_in"],
1756
+ idToken: data["id_token"],
1757
+ tokenType: data["token_type"],
1758
+ scope: data["scope"]
1759
+ };
1760
+ }
1761
+ };
1762
+
1763
+ // src/providers/oauth/apple.ts
1764
+ var AppleProvider = class {
1765
+ name = "apple";
1766
+ config;
1767
+ constructor(config) {
1768
+ this.config = config;
1769
+ }
1770
+ async getAuthorizationUrl(state, _codeChallenge) {
1771
+ const params = new URLSearchParams({
1772
+ client_id: this.config.clientId,
1773
+ redirect_uri: this.config.redirectUri,
1774
+ response_type: "code id_token",
1775
+ response_mode: "form_post",
1776
+ scope: this.config.scopes?.join(" ") ?? "name email",
1777
+ state
1778
+ });
1779
+ return `https://appleid.apple.com/auth/authorize?${params}`;
1780
+ }
1781
+ async exchangeCode(code, _codeVerifier) {
1782
+ const clientSecret = await this.generateClientSecret();
1783
+ const params = new URLSearchParams({
1784
+ client_id: this.config.clientId,
1785
+ client_secret: clientSecret,
1786
+ code,
1787
+ grant_type: "authorization_code",
1788
+ redirect_uri: this.config.redirectUri
1789
+ });
1790
+ const response = await fetch("https://appleid.apple.com/auth/token", {
1791
+ method: "POST",
1792
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1793
+ body: params
1794
+ });
1795
+ if (!response.ok) {
1796
+ const error = await response.text();
1797
+ throw new Error(`Apple token exchange failed: ${error}`);
1798
+ }
1799
+ const data = await response.json();
1800
+ return {
1801
+ accessToken: data["access_token"],
1802
+ refreshToken: data["refresh_token"],
1803
+ expiresIn: data["expires_in"],
1804
+ idToken: data["id_token"],
1805
+ tokenType: data["token_type"]
1806
+ };
1807
+ }
1808
+ async getUserInfo(accessToken) {
1809
+ return {
1810
+ id: "",
1811
+ email: "",
1812
+ emailVerified: false,
1813
+ raw: { accessToken }
1814
+ };
1815
+ }
1816
+ /**
1817
+ * Parse user info from id_token and form post data
1818
+ * Apple sends user info only on first authorization
1819
+ */
1820
+ parseUserFromCallback(idToken, userData) {
1821
+ const parts = idToken.split(".");
1822
+ if (parts.length !== 3) {
1823
+ throw new Error("Invalid id_token format");
1824
+ }
1825
+ const payload = JSON.parse(
1826
+ Buffer.from(parts[1], "base64url").toString("utf-8")
1827
+ );
1828
+ const name = userData?.name ? [userData.name.firstName, userData.name.lastName].filter(Boolean).join(" ") : void 0;
1829
+ return {
1830
+ id: payload["sub"],
1831
+ email: userData?.email ?? payload["email"],
1832
+ name,
1833
+ emailVerified: payload["email_verified"] === "true" || payload["email_verified"] === true,
1834
+ raw: payload
1835
+ };
1836
+ }
1837
+ /**
1838
+ * Generate client secret JWT for Apple
1839
+ * Apple requires a JWT signed with your private key
1840
+ */
1841
+ async generateClientSecret() {
1842
+ const now = Math.floor(Date.now() / 1e3);
1843
+ const expiry = now + 86400 * 180;
1844
+ const header = {
1845
+ alg: "ES256",
1846
+ kid: this.config.keyId,
1847
+ typ: "JWT"
1848
+ };
1849
+ const payload = {
1850
+ iss: this.config.teamId,
1851
+ iat: now,
1852
+ exp: expiry,
1853
+ aud: "https://appleid.apple.com",
1854
+ sub: this.config.clientId
1855
+ };
1856
+ const privateKey = await crypto.subtle.importKey(
1857
+ "pkcs8",
1858
+ this.pemToArrayBuffer(this.config.privateKey),
1859
+ { name: "ECDSA", namedCurve: "P-256" },
1860
+ false,
1861
+ ["sign"]
1862
+ );
1863
+ const headerB64 = this.base64UrlEncode(JSON.stringify(header));
1864
+ const payloadB64 = this.base64UrlEncode(JSON.stringify(payload));
1865
+ const signingInput = `${headerB64}.${payloadB64}`;
1866
+ const signature = await crypto.subtle.sign(
1867
+ { name: "ECDSA", hash: "SHA-256" },
1868
+ privateKey,
1869
+ new TextEncoder().encode(signingInput)
1870
+ );
1871
+ const signatureB64 = this.base64UrlEncode(
1872
+ String.fromCharCode(...new Uint8Array(signature))
1873
+ );
1874
+ return `${signingInput}.${signatureB64}`;
1875
+ }
1876
+ pemToArrayBuffer(pem) {
1877
+ const pemContents = pem.replace(/-----BEGIN PRIVATE KEY-----/, "").replace(/-----END PRIVATE KEY-----/, "").replace(/\s/g, "");
1878
+ const binaryString = atob(pemContents);
1879
+ const bytes = new Uint8Array(binaryString.length);
1880
+ for (let i = 0; i < binaryString.length; i++) {
1881
+ bytes[i] = binaryString.charCodeAt(i);
1882
+ }
1883
+ return bytes.buffer;
1884
+ }
1885
+ base64UrlEncode(str) {
1886
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1887
+ }
1888
+ };
1889
+
1890
+ // src/providers/oauth/index.ts
1891
+ async function generatePKCE() {
1892
+ const array = new Uint8Array(32);
1893
+ crypto.getRandomValues(array);
1894
+ const codeVerifier = btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1895
+ const encoder = new TextEncoder();
1896
+ const data = encoder.encode(codeVerifier);
1897
+ const digest = await crypto.subtle.digest("SHA-256", data);
1898
+ const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1899
+ return { codeVerifier, codeChallenge };
1900
+ }
1901
+ function generateState() {
1902
+ const array = new Uint8Array(32);
1903
+ crypto.getRandomValues(array);
1904
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1905
+ }
1906
+ var OAuthManager = class {
1907
+ storage;
1908
+ providers = /* @__PURE__ */ new Map();
1909
+ stateExpirySeconds;
1910
+ constructor(storage, config, options) {
1911
+ this.storage = storage;
1912
+ this.stateExpirySeconds = options?.stateExpirySeconds ?? 600;
1913
+ if (config.google) {
1914
+ this.providers.set("google", new GoogleProvider(config.google));
1915
+ }
1916
+ if (config.github) {
1917
+ this.providers.set("github", new GitHubProvider(config.github));
1918
+ }
1919
+ if (config.microsoft) {
1920
+ this.providers.set("microsoft", new MicrosoftProvider(config.microsoft));
1921
+ }
1922
+ if (config.apple) {
1923
+ this.providers.set("apple", new AppleProvider(config.apple));
1924
+ }
1925
+ }
1926
+ /**
1927
+ * Get available providers
1928
+ */
1929
+ getAvailableProviders() {
1930
+ return Array.from(this.providers.keys());
1931
+ }
1932
+ /**
1933
+ * Check if a provider is configured
1934
+ */
1935
+ hasProvider(name) {
1936
+ return this.providers.has(name);
1937
+ }
1938
+ /**
1939
+ * Get a provider by name
1940
+ */
1941
+ getProvider(name) {
1942
+ return this.providers.get(name);
1943
+ }
1944
+ /**
1945
+ * Start OAuth flow - returns authorization URL
1946
+ */
1947
+ async startFlow(providerName, options) {
1948
+ const provider = this.providers.get(providerName);
1949
+ if (!provider) {
1950
+ throw new Error(`OAuth provider '${providerName}' not configured`);
1951
+ }
1952
+ const state = generateState();
1953
+ const { codeVerifier, codeChallenge } = await generatePKCE();
1954
+ const stateData = {
1955
+ state,
1956
+ provider: providerName,
1957
+ codeVerifier,
1958
+ tenantId: options?.tenantId,
1959
+ redirectUrl: options?.redirectUrl,
1960
+ expiresAt: new Date(Date.now() + this.stateExpirySeconds * 1e3)
1961
+ };
1962
+ await this.storage.set(
1963
+ `oauth:state:${state}`,
1964
+ stateData,
1965
+ this.stateExpirySeconds
1966
+ );
1967
+ const authorizationUrl = await provider.getAuthorizationUrl(state, codeChallenge);
1968
+ return { authorizationUrl, state };
1969
+ }
1970
+ /**
1971
+ * Handle OAuth callback
1972
+ */
1973
+ async handleCallback(providerName, code, state) {
1974
+ const provider = this.providers.get(providerName);
1975
+ if (!provider) {
1976
+ throw new Error(`OAuth provider '${providerName}' not configured`);
1977
+ }
1978
+ const storedState = await this.storage.get(`oauth:state:${state}`);
1979
+ if (!storedState) {
1980
+ throw new Error("Invalid OAuth state");
1981
+ }
1982
+ if (storedState.expiresAt < /* @__PURE__ */ new Date()) {
1983
+ await this.storage.delete(`oauth:state:${state}`);
1984
+ throw new Error("OAuth state expired");
1985
+ }
1986
+ if (storedState.provider !== providerName) {
1987
+ throw new Error("Provider mismatch");
1988
+ }
1989
+ const tokens = await provider.exchangeCode(code, storedState.codeVerifier);
1990
+ const userInfo = await provider.getUserInfo(tokens.accessToken);
1991
+ await this.storage.delete(`oauth:state:${state}`);
1992
+ return {
1993
+ userInfo,
1994
+ tokens,
1995
+ tenantId: storedState.tenantId,
1996
+ redirectUrl: storedState.redirectUrl
1997
+ };
1998
+ }
1999
+ /**
2000
+ * Refresh OAuth tokens (if supported by provider)
2001
+ */
2002
+ async refreshTokens(providerName, refreshToken) {
2003
+ const provider = this.providers.get(providerName);
2004
+ if (!provider) {
2005
+ throw new Error(`OAuth provider '${providerName}' not configured`);
2006
+ }
2007
+ if (!provider.refreshToken) {
2008
+ throw new Error(`Token refresh not supported for ${providerName}`);
2009
+ }
2010
+ return provider.refreshToken(refreshToken);
2011
+ }
2012
+ /**
2013
+ * Check if provider supports token refresh
2014
+ */
2015
+ supportsRefresh(providerName) {
2016
+ const provider = this.providers.get(providerName);
2017
+ return provider ? typeof provider.refreshToken === "function" : false;
2018
+ }
2019
+ };
2020
+ function createOAuthManager(storage, config, options) {
2021
+ return new OAuthManager(storage, config, options);
2022
+ }
2023
+
2024
+ // src/providers/magic-link/index.ts
2025
+ var MagicLinkProvider = class {
2026
+ name = "magic-link";
2027
+ type = "magic-link";
2028
+ storage;
2029
+ config;
2030
+ _enabled;
2031
+ constructor(storage, config) {
2032
+ this.storage = storage;
2033
+ this.config = {
2034
+ callbackPath: "/auth/magic-link/callback",
2035
+ expiresIn: 900,
2036
+ // 15 minutes
2037
+ tokenLength: 32,
2038
+ ...config
2039
+ };
2040
+ this._enabled = true;
2041
+ }
2042
+ get enabled() {
2043
+ return this._enabled;
2044
+ }
2045
+ /**
2046
+ * Send magic link to email
2047
+ */
2048
+ async sendMagicLink(email, options) {
2049
+ const normalizedEmail = email.toLowerCase().trim();
2050
+ const token = await generateRandomHex(this.config.tokenLength);
2051
+ const tokenHash = await sha256Hex(token);
2052
+ const expiresAt = new Date(Date.now() + this.config.expiresIn * 1e3);
2053
+ await this.storage.delete(`magic:email:${normalizedEmail}`);
2054
+ const tokenData = {
2055
+ email: normalizedEmail,
2056
+ tokenHash,
2057
+ tenantId: options?.tenantId,
2058
+ redirectUrl: options?.redirectUrl,
2059
+ expiresAt: expiresAt.toISOString()
2060
+ };
2061
+ await this.storage.set(
2062
+ `magic:token:${tokenHash}`,
2063
+ tokenData,
2064
+ this.config.expiresIn
2065
+ );
2066
+ await this.storage.set(
2067
+ `magic:email:${normalizedEmail}`,
2068
+ tokenHash,
2069
+ this.config.expiresIn
2070
+ );
2071
+ const params = new URLSearchParams({ token });
2072
+ if (options?.redirectUrl) {
2073
+ params.set("redirect", options.redirectUrl);
2074
+ }
2075
+ const magicLink = `${this.config.baseUrl}${this.config.callbackPath}?${params}`;
2076
+ try {
2077
+ await this.config.send(normalizedEmail, magicLink, this.config.expiresIn);
2078
+ return { success: true, expiresAt };
2079
+ } catch (error) {
2080
+ await this.storage.delete(`magic:token:${tokenHash}`);
2081
+ await this.storage.delete(`magic:email:${normalizedEmail}`);
2082
+ return {
2083
+ success: false,
2084
+ error: error instanceof Error ? error.message : "Failed to send email"
2085
+ };
2086
+ }
2087
+ }
2088
+ /**
2089
+ * Verify magic link token
2090
+ */
2091
+ async verifyMagicLink(token) {
2092
+ const tokenHash = await sha256Hex(token);
2093
+ const tokenData = await this.storage.get(`magic:token:${tokenHash}`);
2094
+ if (!tokenData) {
2095
+ return { success: false, error: "Invalid or expired magic link" };
2096
+ }
2097
+ if (tokenData.usedAt) {
2098
+ return { success: false, error: "Magic link already used" };
2099
+ }
2100
+ if (new Date(tokenData.expiresAt) < /* @__PURE__ */ new Date()) {
2101
+ await this.storage.delete(`magic:token:${tokenHash}`);
2102
+ return { success: false, error: "Magic link expired" };
2103
+ }
2104
+ tokenData.usedAt = (/* @__PURE__ */ new Date()).toISOString();
2105
+ await this.storage.set(`magic:token:${tokenHash}`, tokenData, 60);
2106
+ await this.storage.delete(`magic:email:${tokenData.email}`);
2107
+ return {
2108
+ success: true,
2109
+ email: tokenData.email,
2110
+ tenantId: tokenData.tenantId,
2111
+ redirectUrl: tokenData.redirectUrl
2112
+ };
2113
+ }
2114
+ /**
2115
+ * Authenticate with magic link token (implements AuthProvider)
2116
+ */
2117
+ async authenticate(input) {
2118
+ const { credential } = input;
2119
+ if (!credential) {
2120
+ return {
2121
+ success: false,
2122
+ error: "Token is required",
2123
+ errorCode: "INVALID_INPUT"
2124
+ };
2125
+ }
2126
+ const result = await this.verifyMagicLink(credential);
2127
+ if (!result.success) {
2128
+ return {
2129
+ success: false,
2130
+ error: result.error,
2131
+ errorCode: "INVALID_TOKEN"
2132
+ };
2133
+ }
2134
+ return {
2135
+ success: true
2136
+ // Auth engine will handle user lookup/creation based on email
2137
+ };
2138
+ }
2139
+ /**
2140
+ * Get provider info
2141
+ */
2142
+ getInfo() {
2143
+ return {
2144
+ name: this.name,
2145
+ type: this.type,
2146
+ enabled: this.enabled,
2147
+ displayName: "Magic Link"
2148
+ };
2149
+ }
2150
+ /**
2151
+ * Request magic link (for use in auth routes)
2152
+ */
2153
+ async requestMagicLink(email, options) {
2154
+ return this.sendMagicLink(email, options);
2155
+ }
2156
+ };
2157
+ function createMagicLinkProvider(storage, config) {
2158
+ return new MagicLinkProvider(storage, config);
2159
+ }
2160
+
2161
+ // src/providers/totp/index.ts
2162
+ var DEFAULT_CONFIG = {
2163
+ algorithm: "SHA1",
2164
+ digits: 6,
2165
+ period: 30,
2166
+ window: 1,
2167
+ backupCodeCount: 10
2168
+ };
2169
+ function bytesToBase32(bytes) {
2170
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2171
+ let result = "";
2172
+ let bits = 0;
2173
+ let value = 0;
2174
+ for (const byte of bytes) {
2175
+ value = value << 8 | byte;
2176
+ bits += 8;
2177
+ while (bits >= 5) {
2178
+ result += alphabet[value >>> bits - 5 & 31];
2179
+ bits -= 5;
2180
+ }
2181
+ }
2182
+ if (bits > 0) {
2183
+ result += alphabet[value << 5 - bits & 31];
2184
+ }
2185
+ return result;
2186
+ }
2187
+ function base32ToBytes(base32) {
2188
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2189
+ const cleaned = base32.toUpperCase().replace(/[^A-Z2-7]/g, "");
2190
+ const bytes = [];
2191
+ let bits = 0;
2192
+ let value = 0;
2193
+ for (const char of cleaned) {
2194
+ const index = alphabet.indexOf(char);
2195
+ if (index === -1) continue;
2196
+ value = value << 5 | index;
2197
+ bits += 5;
2198
+ if (bits >= 8) {
2199
+ bytes.push(value >>> bits - 8 & 255);
2200
+ bits -= 8;
2201
+ }
2202
+ }
2203
+ return new Uint8Array(bytes);
2204
+ }
2205
+ async function hotp(secret, counter, algorithm, digits) {
2206
+ const counterBytes = new Uint8Array(8);
2207
+ const view = new DataView(counterBytes.buffer);
2208
+ view.setBigUint64(0, counter, false);
2209
+ const hashName = algorithm === "SHA1" ? "SHA-1" : `SHA-${algorithm.slice(3)}`;
2210
+ const key = await crypto.subtle.importKey(
2211
+ "raw",
2212
+ secret,
2213
+ { name: "HMAC", hash: hashName },
2214
+ false,
2215
+ ["sign"]
2216
+ );
2217
+ const signature = await crypto.subtle.sign("HMAC", key, counterBytes);
2218
+ const hmac = new Uint8Array(signature);
2219
+ const offset = hmac[hmac.length - 1] & 15;
2220
+ const binary = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
2221
+ const otp = binary % Math.pow(10, digits);
2222
+ return otp.toString().padStart(digits, "0");
2223
+ }
2224
+ async function generateTOTP(secret, algorithm, digits, period) {
2225
+ const counter = BigInt(Math.floor(Date.now() / 1e3 / period));
2226
+ return hotp(secret, counter, algorithm, digits);
2227
+ }
2228
+ async function verifyTOTP(token, secret, algorithm, digits, period, window) {
2229
+ const now = Math.floor(Date.now() / 1e3 / period);
2230
+ for (let i = -window; i <= window; i++) {
2231
+ const counter = BigInt(now + i);
2232
+ const expected = await hotp(secret, counter, algorithm, digits);
2233
+ if (token === expected) {
2234
+ return true;
2235
+ }
2236
+ }
2237
+ return false;
2238
+ }
2239
+ async function generateBackupCodes(count) {
2240
+ const codes = [];
2241
+ for (let i = 0; i < count; i++) {
2242
+ const hex = await generateRandomHex(4);
2243
+ codes.push(`${hex.slice(0, 4).toUpperCase()}-${hex.slice(4, 8).toUpperCase()}`);
2244
+ }
2245
+ return codes;
2246
+ }
2247
+ async function encryptSecret(secret, keyString) {
2248
+ const encoder = new TextEncoder();
2249
+ const data = encoder.encode(secret);
2250
+ const keyData = encoder.encode(keyString.padEnd(32, "0").slice(0, 32));
2251
+ const key = await crypto.subtle.importKey(
2252
+ "raw",
2253
+ keyData,
2254
+ { name: "AES-GCM" },
2255
+ false,
2256
+ ["encrypt"]
2257
+ );
2258
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2259
+ const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
2260
+ const result = new Uint8Array(iv.length + encrypted.byteLength);
2261
+ result.set(iv);
2262
+ result.set(new Uint8Array(encrypted), iv.length);
2263
+ return btoa(String.fromCharCode(...result));
2264
+ }
2265
+ async function decryptSecret(encrypted, keyString) {
2266
+ const encoder = new TextEncoder();
2267
+ const keyData = encoder.encode(keyString.padEnd(32, "0").slice(0, 32));
2268
+ const data = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
2269
+ const iv = data.slice(0, 12);
2270
+ const ciphertext = data.slice(12);
2271
+ const key = await crypto.subtle.importKey(
2272
+ "raw",
2273
+ keyData,
2274
+ { name: "AES-GCM" },
2275
+ false,
2276
+ ["decrypt"]
2277
+ );
2278
+ const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
2279
+ return new TextDecoder().decode(decrypted);
2280
+ }
2281
+ var TOTPProvider = class {
2282
+ name = "totp";
2283
+ type = "totp";
2284
+ storage;
2285
+ config;
2286
+ _enabled = true;
2287
+ constructor(storage, config) {
2288
+ this.storage = storage;
2289
+ this.config = {
2290
+ ...DEFAULT_CONFIG,
2291
+ encryptionKey: "change-me-in-production",
2292
+ ...config
2293
+ };
2294
+ }
2295
+ get enabled() {
2296
+ return this._enabled;
2297
+ }
2298
+ /**
2299
+ * Setup TOTP for a user
2300
+ * Note: Use setupWithEmail for full setup including QR code URL
2301
+ */
2302
+ async setup(userId) {
2303
+ return this.setupWithEmail(userId, userId);
2304
+ }
2305
+ /**
2306
+ * Setup TOTP for a user with email for QR code label
2307
+ */
2308
+ async setupWithEmail(userId, userEmail) {
2309
+ const secretBytes = crypto.getRandomValues(new Uint8Array(20));
2310
+ const secret = bytesToBase32(secretBytes);
2311
+ const backupCodes = await generateBackupCodes(this.config.backupCodeCount);
2312
+ const otpauthUri = this.createOtpauthUri(secret, userEmail);
2313
+ const qrCodeUrl = `https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=${encodeURIComponent(otpauthUri)}`;
2314
+ const encryptedSecret = await encryptSecret(secret, this.config.encryptionKey);
2315
+ const encryptedBackupCodes = await encryptSecret(
2316
+ JSON.stringify(backupCodes),
2317
+ this.config.encryptionKey
2318
+ );
2319
+ const totpData = {
2320
+ userId,
2321
+ encryptedSecret,
2322
+ backupCodes: encryptedBackupCodes,
2323
+ verified: false,
2324
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2325
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2326
+ };
2327
+ await this.storage.set(`totp:user:${userId}`, totpData);
2328
+ return {
2329
+ secret,
2330
+ qrCode: qrCodeUrl,
2331
+ backupCodes,
2332
+ qrCodeUrl,
2333
+ otpauthUri
2334
+ };
2335
+ }
2336
+ /**
2337
+ * Verify TOTP and activate 2FA (for TwoFactorProvider interface)
2338
+ */
2339
+ async verifySetup(userId, token) {
2340
+ const totpData = await this.storage.get(`totp:user:${userId}`);
2341
+ if (!totpData) {
2342
+ throw new Error("TOTP not set up for this user");
2343
+ }
2344
+ const secret = await decryptSecret(totpData.encryptedSecret, this.config.encryptionKey);
2345
+ const secretBytes = base32ToBytes(secret);
2346
+ const valid = await verifyTOTP(
2347
+ token,
2348
+ secretBytes,
2349
+ this.config.algorithm,
2350
+ this.config.digits,
2351
+ this.config.period,
2352
+ this.config.window
2353
+ );
2354
+ if (valid) {
2355
+ totpData.verified = true;
2356
+ totpData.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2357
+ await this.storage.set(`totp:user:${userId}`, totpData);
2358
+ return true;
2359
+ }
2360
+ return false;
2361
+ }
2362
+ /**
2363
+ * Verify TOTP during login (for TwoFactorProvider interface)
2364
+ */
2365
+ async verifyLogin(userId, code) {
2366
+ const result = await this.verifyCode(userId, code);
2367
+ return result.valid;
2368
+ }
2369
+ /**
2370
+ * Authenticate (for AuthProvider interface)
2371
+ */
2372
+ async authenticate(_input) {
2373
+ return {
2374
+ success: false,
2375
+ error: "TOTP is used for two-factor authentication, not primary auth",
2376
+ errorCode: "USE_TWO_FACTOR_METHODS"
2377
+ };
2378
+ }
2379
+ /**
2380
+ * Verify TOTP token
2381
+ */
2382
+ async verifyCode(userId, token) {
2383
+ const totpData = await this.storage.get(`totp:user:${userId}`);
2384
+ if (!totpData || !totpData.verified) {
2385
+ throw new Error("TOTP not enabled for this user");
2386
+ }
2387
+ const secret = await decryptSecret(totpData.encryptedSecret, this.config.encryptionKey);
2388
+ const secretBytes = base32ToBytes(secret);
2389
+ const validTOTP = await verifyTOTP(
2390
+ token,
2391
+ secretBytes,
2392
+ this.config.algorithm,
2393
+ this.config.digits,
2394
+ this.config.period,
2395
+ this.config.window
2396
+ );
2397
+ if (validTOTP) {
2398
+ return { valid: true, usedBackupCode: false };
2399
+ }
2400
+ const backupCodesJson = await decryptSecret(
2401
+ totpData.backupCodes,
2402
+ this.config.encryptionKey
2403
+ );
2404
+ const backupCodes = JSON.parse(backupCodesJson);
2405
+ const normalizedToken = token.toUpperCase().replace(/[^A-Z0-9]/g, "");
2406
+ const backupIndex = backupCodes.findIndex(
2407
+ (code) => code.replace("-", "") === normalizedToken
2408
+ );
2409
+ if (backupIndex !== -1) {
2410
+ backupCodes.splice(backupIndex, 1);
2411
+ const encryptedBackupCodes = await encryptSecret(
2412
+ JSON.stringify(backupCodes),
2413
+ this.config.encryptionKey
2414
+ );
2415
+ totpData.backupCodes = encryptedBackupCodes;
2416
+ totpData.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2417
+ await this.storage.set(`totp:user:${userId}`, totpData);
2418
+ return { valid: true, usedBackupCode: true };
2419
+ }
2420
+ return { valid: false };
2421
+ }
2422
+ /**
2423
+ * Disable TOTP for user
2424
+ */
2425
+ async disable(userId) {
2426
+ await this.storage.delete(`totp:user:${userId}`);
2427
+ }
2428
+ /**
2429
+ * Regenerate backup codes
2430
+ */
2431
+ async regenerateBackupCodes(userId) {
2432
+ const totpData = await this.storage.get(`totp:user:${userId}`);
2433
+ if (!totpData || !totpData.verified) {
2434
+ throw new Error("TOTP not enabled for this user");
2435
+ }
2436
+ const backupCodes = await generateBackupCodes(this.config.backupCodeCount);
2437
+ const encryptedBackupCodes = await encryptSecret(
2438
+ JSON.stringify(backupCodes),
2439
+ this.config.encryptionKey
2440
+ );
2441
+ totpData.backupCodes = encryptedBackupCodes;
2442
+ totpData.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2443
+ await this.storage.set(`totp:user:${userId}`, totpData);
2444
+ return backupCodes;
2445
+ }
2446
+ /**
2447
+ * Get remaining backup codes count
2448
+ */
2449
+ async getBackupCodesCount(userId) {
2450
+ const totpData = await this.storage.get(`totp:user:${userId}`);
2451
+ if (!totpData) {
2452
+ return 0;
2453
+ }
2454
+ const backupCodesJson = await decryptSecret(
2455
+ totpData.backupCodes,
2456
+ this.config.encryptionKey
2457
+ );
2458
+ const backupCodes = JSON.parse(backupCodesJson);
2459
+ return backupCodes.length;
2460
+ }
2461
+ /**
2462
+ * Check if TOTP is enabled for user
2463
+ */
2464
+ async isEnabled(userId) {
2465
+ const totpData = await this.storage.get(`totp:user:${userId}`);
2466
+ return !!totpData?.verified;
2467
+ }
2468
+ /**
2469
+ * Generate current TOTP (for testing)
2470
+ */
2471
+ async generateCurrent(userId) {
2472
+ const totpData = await this.storage.get(`totp:user:${userId}`);
2473
+ if (!totpData) {
2474
+ throw new Error("TOTP not set up for this user");
2475
+ }
2476
+ const secret = await decryptSecret(totpData.encryptedSecret, this.config.encryptionKey);
2477
+ const secretBytes = base32ToBytes(secret);
2478
+ return generateTOTP(
2479
+ secretBytes,
2480
+ this.config.algorithm,
2481
+ this.config.digits,
2482
+ this.config.period
2483
+ );
2484
+ }
2485
+ /**
2486
+ * Get provider info
2487
+ */
2488
+ getInfo() {
2489
+ return {
2490
+ name: this.name,
2491
+ type: this.type,
2492
+ enabled: this.enabled,
2493
+ displayName: "Authenticator App"
2494
+ };
2495
+ }
2496
+ /**
2497
+ * Create otpauth URI for QR code
2498
+ */
2499
+ createOtpauthUri(secret, userEmail) {
2500
+ const params = new URLSearchParams({
2501
+ secret,
2502
+ issuer: this.config.issuer,
2503
+ algorithm: this.config.algorithm,
2504
+ digits: String(this.config.digits),
2505
+ period: String(this.config.period)
2506
+ });
2507
+ const label = encodeURIComponent(`${this.config.issuer}:${userEmail}`);
2508
+ return `otpauth://totp/${label}?${params}`;
2509
+ }
2510
+ };
2511
+ function createTOTPProvider(storage, config) {
2512
+ return new TOTPProvider(storage, config);
2513
+ }
2514
+
2515
+ // src/providers/webauthn/index.ts
2516
+ function base64UrlEncode2(bytes) {
2517
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2518
+ }
2519
+ function base64UrlDecode2(str) {
2520
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
2521
+ const padding = "=".repeat((4 - base64.length % 4) % 4);
2522
+ const binary = atob(base64 + padding);
2523
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
2524
+ }
2525
+ function generateChallenge() {
2526
+ const bytes = new Uint8Array(32);
2527
+ crypto.getRandomValues(bytes);
2528
+ return base64UrlEncode2(bytes);
2529
+ }
2530
+ function parseAuthenticatorData(data) {
2531
+ const rpIdHash = data.slice(0, 32);
2532
+ const flagsByte = data[32];
2533
+ const flags = {
2534
+ userPresent: (flagsByte & 1) !== 0,
2535
+ userVerified: (flagsByte & 4) !== 0,
2536
+ attestedCredentialData: (flagsByte & 64) !== 0
2537
+ };
2538
+ const signCount = new DataView(data.buffer, data.byteOffset + 33, 4).getUint32(0, false);
2539
+ let attestedCredentialData;
2540
+ if (flags.attestedCredentialData && data.length > 37) {
2541
+ const aaguid = data.slice(37, 53);
2542
+ const credentialIdLength = new DataView(data.buffer, data.byteOffset + 53, 2).getUint16(0, false);
2543
+ const credentialId = data.slice(55, 55 + credentialIdLength);
2544
+ const publicKey = data.slice(55 + credentialIdLength);
2545
+ attestedCredentialData = { aaguid, credentialId, publicKey };
2546
+ }
2547
+ return { rpIdHash, flags, signCount, attestedCredentialData };
2548
+ }
2549
+ function parseCBORMap(data) {
2550
+ const result = {};
2551
+ let offset = 0;
2552
+ const initial = data[offset++];
2553
+ if ((initial & 224) !== 160) {
2554
+ throw new Error("Expected CBOR map");
2555
+ }
2556
+ const mapSize = initial & 31;
2557
+ for (let i = 0; i < mapSize; i++) {
2558
+ const keyInitial = data[offset++];
2559
+ const keyLength = keyInitial & 31;
2560
+ const key = new TextDecoder().decode(data.slice(offset, offset + keyLength));
2561
+ offset += keyLength;
2562
+ const valueInitial = data[offset++];
2563
+ let valueLength;
2564
+ if ((valueInitial & 31) < 24) {
2565
+ valueLength = valueInitial & 31;
2566
+ } else if ((valueInitial & 31) === 24) {
2567
+ valueLength = data[offset++];
2568
+ } else if ((valueInitial & 31) === 25) {
2569
+ valueLength = data[offset++] << 8 | data[offset++];
2570
+ } else {
2571
+ throw new Error("Unsupported CBOR value length");
2572
+ }
2573
+ result[key] = data.slice(offset, offset + valueLength);
2574
+ offset += valueLength;
2575
+ }
2576
+ return result;
2577
+ }
2578
+ function arrayBufferEqual(a, b) {
2579
+ if (a.byteLength !== b.byteLength) return false;
2580
+ const viewA = new Uint8Array(a);
2581
+ const viewB = new Uint8Array(b);
2582
+ for (let i = 0; i < viewA.length; i++) {
2583
+ if (viewA[i] !== viewB[i]) return false;
2584
+ }
2585
+ return true;
2586
+ }
2587
+ var WebAuthnProvider = class {
2588
+ name = "webauthn";
2589
+ type = "webauthn";
2590
+ storage;
2591
+ config;
2592
+ _enabled = true;
2593
+ constructor(storage, config) {
2594
+ this.storage = storage;
2595
+ this.config = {
2596
+ timeout: 6e4,
2597
+ attestation: "none",
2598
+ userVerification: "preferred",
2599
+ ...config
2600
+ };
2601
+ }
2602
+ get enabled() {
2603
+ return this._enabled;
2604
+ }
2605
+ /**
2606
+ * Generate registration options
2607
+ */
2608
+ async generateRegistrationOptions(userId, userName, userDisplayName, authenticatorType) {
2609
+ const challenge = generateChallenge();
2610
+ const existingCredentials = await this.getUserCredentials(userId);
2611
+ const challengeData = {
2612
+ challenge,
2613
+ userId,
2614
+ type: "registration",
2615
+ expiresAt: new Date(Date.now() + this.config.timeout).toISOString()
2616
+ };
2617
+ await this.storage.set(`webauthn:challenge:${challenge}`, challengeData, this.config.timeout / 1e3);
2618
+ return {
2619
+ challenge,
2620
+ rp: {
2621
+ name: this.config.rpName,
2622
+ id: this.config.rpId
2623
+ },
2624
+ user: {
2625
+ id: base64UrlEncode2(new TextEncoder().encode(userId)),
2626
+ name: userName,
2627
+ displayName: userDisplayName
2628
+ },
2629
+ pubKeyCredParams: [
2630
+ { type: "public-key", alg: -7 },
2631
+ // ES256
2632
+ { type: "public-key", alg: -257 }
2633
+ // RS256
2634
+ ],
2635
+ timeout: this.config.timeout,
2636
+ attestation: this.config.attestation,
2637
+ authenticatorSelection: {
2638
+ authenticatorAttachment: authenticatorType,
2639
+ residentKey: "preferred",
2640
+ userVerification: this.config.userVerification
2641
+ },
2642
+ excludeCredentials: existingCredentials.map((cred) => ({
2643
+ id: cred.credentialId,
2644
+ type: "public-key",
2645
+ transports: cred.transports
2646
+ }))
2647
+ };
2648
+ }
2649
+ /**
2650
+ * Verify registration response
2651
+ */
2652
+ async verifyRegistration(response, challenge, credentialName) {
2653
+ const pending = await this.storage.get(`webauthn:challenge:${challenge}`);
2654
+ if (!pending || new Date(pending.expiresAt) < /* @__PURE__ */ new Date()) {
2655
+ return { success: false, error: "Invalid or expired challenge" };
2656
+ }
2657
+ const userId = pending.userId;
2658
+ if (!userId) {
2659
+ return { success: false, error: "No user ID in challenge" };
2660
+ }
2661
+ const clientDataJSON = base64UrlDecode2(response.response.clientDataJSON);
2662
+ const clientData = JSON.parse(new TextDecoder().decode(clientDataJSON));
2663
+ if (clientData["type"] !== "webauthn.create") {
2664
+ return { success: false, error: "Invalid client data type" };
2665
+ }
2666
+ if (clientData["challenge"] !== challenge) {
2667
+ return { success: false, error: "Challenge mismatch" };
2668
+ }
2669
+ if (clientData["origin"] !== this.config.origin) {
2670
+ return { success: false, error: "Origin mismatch" };
2671
+ }
2672
+ const attestationObject = base64UrlDecode2(response.response.attestationObject);
2673
+ let authData;
2674
+ try {
2675
+ const attestation = parseCBORMap(attestationObject);
2676
+ authData = attestation["authData"];
2677
+ } catch {
2678
+ return { success: false, error: "Failed to parse attestation object" };
2679
+ }
2680
+ const parsedAuthData = parseAuthenticatorData(authData);
2681
+ const expectedRpIdHash = await crypto.subtle.digest(
2682
+ "SHA-256",
2683
+ new TextEncoder().encode(this.config.rpId)
2684
+ );
2685
+ if (!arrayBufferEqual(parsedAuthData.rpIdHash.buffer, expectedRpIdHash)) {
2686
+ return { success: false, error: "RP ID hash mismatch" };
2687
+ }
2688
+ if (!parsedAuthData.flags.userPresent) {
2689
+ return { success: false, error: "User not present" };
2690
+ }
2691
+ if (!parsedAuthData.attestedCredentialData) {
2692
+ return { success: false, error: "No credential data" };
2693
+ }
2694
+ const { credentialId, publicKey } = parsedAuthData.attestedCredentialData;
2695
+ const credentialIdBase64 = base64UrlEncode2(credentialId);
2696
+ const publicKeyBase64 = base64UrlEncode2(publicKey);
2697
+ const existing = await this.storage.get(`webauthn:cred:${credentialIdBase64}`);
2698
+ if (existing) {
2699
+ return { success: false, error: "Credential already registered" };
2700
+ }
2701
+ const credential = {
2702
+ id: crypto.randomUUID(),
2703
+ credentialId: credentialIdBase64,
2704
+ userId,
2705
+ publicKey: publicKeyBase64,
2706
+ counter: parsedAuthData.signCount,
2707
+ transports: response.response.transports ?? [],
2708
+ name: credentialName ?? "Passkey",
2709
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2710
+ };
2711
+ await this.storage.set(`webauthn:cred:${credentialIdBase64}`, credential);
2712
+ const userCredIds = await this.storage.get(`webauthn:user:${userId}`) ?? [];
2713
+ userCredIds.push(credentialIdBase64);
2714
+ await this.storage.set(`webauthn:user:${userId}`, userCredIds);
2715
+ await this.storage.delete(`webauthn:challenge:${challenge}`);
2716
+ return { success: true, credentialId: credentialIdBase64 };
2717
+ }
2718
+ /**
2719
+ * Generate authentication options
2720
+ */
2721
+ async generateAuthenticationOptions(userId) {
2722
+ const challenge = generateChallenge();
2723
+ const challengeData = {
2724
+ challenge,
2725
+ userId,
2726
+ type: "authentication",
2727
+ expiresAt: new Date(Date.now() + this.config.timeout).toISOString()
2728
+ };
2729
+ await this.storage.set(`webauthn:challenge:${challenge}`, challengeData, this.config.timeout / 1e3);
2730
+ let allowCredentials = [];
2731
+ if (userId) {
2732
+ const userCredentials = await this.getUserCredentials(userId);
2733
+ allowCredentials = userCredentials.map((cred) => ({
2734
+ id: cred.credentialId,
2735
+ type: "public-key",
2736
+ transports: cred.transports
2737
+ }));
2738
+ }
2739
+ return {
2740
+ challenge,
2741
+ timeout: this.config.timeout,
2742
+ rpId: this.config.rpId,
2743
+ allowCredentials,
2744
+ userVerification: this.config.userVerification
2745
+ };
2746
+ }
2747
+ /**
2748
+ * Verify authentication response
2749
+ */
2750
+ async verifyAuthentication(response, challenge) {
2751
+ const pending = await this.storage.get(`webauthn:challenge:${challenge}`);
2752
+ if (!pending || new Date(pending.expiresAt) < /* @__PURE__ */ new Date()) {
2753
+ return { success: false, error: "Invalid or expired challenge" };
2754
+ }
2755
+ const credential = await this.storage.get(`webauthn:cred:${response.id}`);
2756
+ if (!credential) {
2757
+ return { success: false, error: "Credential not found" };
2758
+ }
2759
+ const clientDataJSON = base64UrlDecode2(response.response.clientDataJSON);
2760
+ const clientData = JSON.parse(new TextDecoder().decode(clientDataJSON));
2761
+ if (clientData["type"] !== "webauthn.get") {
2762
+ return { success: false, error: "Invalid client data type" };
2763
+ }
2764
+ if (clientData["challenge"] !== challenge) {
2765
+ return { success: false, error: "Challenge mismatch" };
2766
+ }
2767
+ if (clientData["origin"] !== this.config.origin) {
2768
+ return { success: false, error: "Origin mismatch" };
2769
+ }
2770
+ const authenticatorData = base64UrlDecode2(response.response.authenticatorData);
2771
+ const parsedAuthData = parseAuthenticatorData(authenticatorData);
2772
+ const expectedRpIdHash = await crypto.subtle.digest(
2773
+ "SHA-256",
2774
+ new TextEncoder().encode(this.config.rpId)
2775
+ );
2776
+ if (!arrayBufferEqual(parsedAuthData.rpIdHash.buffer, expectedRpIdHash)) {
2777
+ return { success: false, error: "RP ID hash mismatch" };
2778
+ }
2779
+ if (!parsedAuthData.flags.userPresent) {
2780
+ return { success: false, error: "User not present" };
2781
+ }
2782
+ if (parsedAuthData.signCount > 0 && parsedAuthData.signCount <= credential.counter) {
2783
+ return { success: false, error: "Possible credential cloning detected" };
2784
+ }
2785
+ credential.counter = parsedAuthData.signCount;
2786
+ credential.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
2787
+ await this.storage.set(`webauthn:cred:${response.id}`, credential);
2788
+ await this.storage.delete(`webauthn:challenge:${challenge}`);
2789
+ return {
2790
+ success: true,
2791
+ userId: credential.userId,
2792
+ credentialId: credential.credentialId
2793
+ };
2794
+ }
2795
+ /**
2796
+ * Get user's credentials
2797
+ */
2798
+ async getUserCredentials(userId) {
2799
+ const credIds = await this.storage.get(`webauthn:user:${userId}`) ?? [];
2800
+ const credentials = [];
2801
+ for (const credId of credIds) {
2802
+ const cred = await this.storage.get(`webauthn:cred:${credId}`);
2803
+ if (cred) {
2804
+ credentials.push(cred);
2805
+ }
2806
+ }
2807
+ return credentials;
2808
+ }
2809
+ /**
2810
+ * Remove a credential
2811
+ */
2812
+ async removeCredential(userId, credentialId) {
2813
+ const credential = await this.storage.get(`webauthn:cred:${credentialId}`);
2814
+ if (!credential || credential.userId !== userId) {
2815
+ return false;
2816
+ }
2817
+ await this.storage.delete(`webauthn:cred:${credentialId}`);
2818
+ const credIds = await this.storage.get(`webauthn:user:${userId}`) ?? [];
2819
+ const filtered = credIds.filter((id) => id !== credentialId);
2820
+ await this.storage.set(`webauthn:user:${userId}`, filtered);
2821
+ return true;
2822
+ }
2823
+ /**
2824
+ * Check if user has any passkeys
2825
+ */
2826
+ async hasPasskeys(userId) {
2827
+ const credentials = await this.getUserCredentials(userId);
2828
+ return credentials.length > 0;
2829
+ }
2830
+ /**
2831
+ * Setup (for TwoFactorProvider interface)
2832
+ */
2833
+ async setup(userId) {
2834
+ const options = await this.generateRegistrationOptions(userId, userId, userId);
2835
+ return {
2836
+ secret: options.challenge,
2837
+ qrCode: "",
2838
+ // Not applicable for WebAuthn
2839
+ backupCodes: [],
2840
+ challenge: options.challenge
2841
+ };
2842
+ }
2843
+ /**
2844
+ * Verify setup (for TwoFactorProvider interface)
2845
+ */
2846
+ async verifySetup(userId, _code) {
2847
+ const credentials = await this.getUserCredentials(userId);
2848
+ return credentials.length > 0;
2849
+ }
2850
+ /**
2851
+ * Verify login (for TwoFactorProvider interface)
2852
+ */
2853
+ async verifyLogin(userId, _code) {
2854
+ const credentials = await this.getUserCredentials(userId);
2855
+ return credentials.length > 0;
2856
+ }
2857
+ /**
2858
+ * Disable 2FA for user
2859
+ */
2860
+ async disable(userId) {
2861
+ const credentials = await this.getUserCredentials(userId);
2862
+ for (const cred of credentials) {
2863
+ await this.removeCredential(userId, cred.credentialId);
2864
+ }
2865
+ }
2866
+ /**
2867
+ * Authenticate (for AuthProvider interface)
2868
+ */
2869
+ async authenticate(_input) {
2870
+ return {
2871
+ success: false,
2872
+ error: "Use generateAuthenticationOptions and verifyAuthentication for WebAuthn authentication",
2873
+ errorCode: "USE_WEBAUTHN_METHODS"
2874
+ };
2875
+ }
2876
+ /**
2877
+ * Get provider info
2878
+ */
2879
+ getInfo() {
2880
+ return {
2881
+ name: this.name,
2882
+ type: this.type,
2883
+ enabled: this.enabled,
2884
+ displayName: "Passkey"
2885
+ };
2886
+ }
2887
+ };
2888
+ function createWebAuthnProvider(storage, config) {
2889
+ return new WebAuthnProvider(storage, config);
2890
+ }
2891
+
2892
+ // src/providers/password/index.ts
2893
+ var DEFAULT_CONFIG2 = {
2894
+ minLength: 8,
2895
+ maxLength: 128,
2896
+ requireUppercase: true,
2897
+ requireLowercase: true,
2898
+ requireNumber: true,
2899
+ requireSpecial: false,
2900
+ bcryptCost: 12
2901
+ };
2902
+ var PasswordProvider = class {
2903
+ name = "password";
2904
+ type = "password";
2905
+ storage;
2906
+ config;
2907
+ _enabled;
2908
+ constructor(storage, config) {
2909
+ this.storage = storage;
2910
+ this.config = {
2911
+ ...DEFAULT_CONFIG2,
2912
+ ...config
2913
+ };
2914
+ this._enabled = false;
2915
+ console.warn(
2916
+ "[Pars Auth] Password provider initialized. Consider using passwordless authentication for better security."
2917
+ );
2918
+ }
2919
+ get enabled() {
2920
+ return this._enabled;
2921
+ }
2922
+ /**
2923
+ * Enable the password provider
2924
+ * Must be explicitly called to enable password authentication
2925
+ */
2926
+ enable() {
2927
+ this._enabled = true;
2928
+ console.warn(
2929
+ "[Pars Auth] Password provider enabled. Ensure you have proper security measures in place (rate limiting, account lockout, etc.)"
2930
+ );
2931
+ }
2932
+ /**
2933
+ * Disable the password provider
2934
+ */
2935
+ disable() {
2936
+ this._enabled = false;
2937
+ }
2938
+ /**
2939
+ * Validate password against policy
2940
+ */
2941
+ validatePassword(password) {
2942
+ const errors = [];
2943
+ if (password.length < this.config.minLength) {
2944
+ errors.push(`Password must be at least ${this.config.minLength} characters`);
2945
+ }
2946
+ if (password.length > this.config.maxLength) {
2947
+ errors.push(`Password must be at most ${this.config.maxLength} characters`);
2948
+ }
2949
+ if (this.config.requireUppercase && !/[A-Z]/.test(password)) {
2950
+ errors.push("Password must contain at least one uppercase letter");
2951
+ }
2952
+ if (this.config.requireLowercase && !/[a-z]/.test(password)) {
2953
+ errors.push("Password must contain at least one lowercase letter");
2954
+ }
2955
+ if (this.config.requireNumber && !/\d/.test(password)) {
2956
+ errors.push("Password must contain at least one number");
2957
+ }
2958
+ if (this.config.requireSpecial && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
2959
+ errors.push("Password must contain at least one special character");
2960
+ }
2961
+ return {
2962
+ valid: errors.length === 0,
2963
+ errors
2964
+ };
2965
+ }
2966
+ /**
2967
+ * Check password strength
2968
+ */
2969
+ checkStrength(password) {
2970
+ let score = 0;
2971
+ if (password.length >= 8) score++;
2972
+ if (password.length >= 12) score++;
2973
+ if (password.length >= 16) score++;
2974
+ if (/[a-z]/.test(password)) score++;
2975
+ if (/[A-Z]/.test(password)) score++;
2976
+ if (/\d/.test(password)) score++;
2977
+ if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) score++;
2978
+ if (/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) score++;
2979
+ if (score <= 3) return "weak";
2980
+ if (score <= 5) return "fair";
2981
+ if (score <= 7) return "strong";
2982
+ return "very-strong";
2983
+ }
2984
+ /**
2985
+ * Hash a password
2986
+ * Uses Web Crypto API for PBKDF2 (bcrypt alternative for edge runtime)
2987
+ */
2988
+ async hashPassword(password) {
2989
+ if (this.config.hashPassword) {
2990
+ return this.config.hashPassword(password);
2991
+ }
2992
+ const encoder = new TextEncoder();
2993
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2994
+ const keyMaterial = await crypto.subtle.importKey(
2995
+ "raw",
2996
+ encoder.encode(password),
2997
+ "PBKDF2",
2998
+ false,
2999
+ ["deriveBits"]
3000
+ );
3001
+ const iterations = 1e5 * (this.config.bcryptCost / 10);
3002
+ const derivedBits = await crypto.subtle.deriveBits(
3003
+ {
3004
+ name: "PBKDF2",
3005
+ salt,
3006
+ iterations,
3007
+ hash: "SHA-256"
3008
+ },
3009
+ keyMaterial,
3010
+ 256
3011
+ );
3012
+ const hashArray = new Uint8Array(derivedBits);
3013
+ const saltB64 = btoa(String.fromCharCode(...salt));
3014
+ const hashB64 = btoa(String.fromCharCode(...hashArray));
3015
+ return `$pbkdf2-sha256$${iterations}$${saltB64}$${hashB64}`;
3016
+ }
3017
+ /**
3018
+ * Verify a password against a hash
3019
+ */
3020
+ async verifyPassword(password, hash) {
3021
+ if (this.config.verifyPassword) {
3022
+ return this.config.verifyPassword(password, hash);
3023
+ }
3024
+ const parts = hash.split("$");
3025
+ if (parts.length !== 5 || parts[1] !== "pbkdf2-sha256") {
3026
+ return false;
3027
+ }
3028
+ const iterations = parseInt(parts[2], 10);
3029
+ const salt = Uint8Array.from(atob(parts[3]), (c) => c.charCodeAt(0));
3030
+ const storedHash = Uint8Array.from(atob(parts[4]), (c) => c.charCodeAt(0));
3031
+ const encoder = new TextEncoder();
3032
+ const keyMaterial = await crypto.subtle.importKey(
3033
+ "raw",
3034
+ encoder.encode(password),
3035
+ "PBKDF2",
3036
+ false,
3037
+ ["deriveBits"]
3038
+ );
3039
+ const derivedBits = await crypto.subtle.deriveBits(
3040
+ {
3041
+ name: "PBKDF2",
3042
+ salt,
3043
+ iterations,
3044
+ hash: "SHA-256"
3045
+ },
3046
+ keyMaterial,
3047
+ 256
3048
+ );
3049
+ const computedHash = new Uint8Array(derivedBits);
3050
+ return this.constantTimeEquals(storedHash, computedHash);
3051
+ }
3052
+ /**
3053
+ * Authenticate with password (implements AuthProvider)
3054
+ */
3055
+ async authenticate(input) {
3056
+ if (!this._enabled) {
3057
+ return {
3058
+ success: false,
3059
+ error: "Password authentication is disabled",
3060
+ errorCode: "PROVIDER_DISABLED"
3061
+ };
3062
+ }
3063
+ const { identifier, credential } = input;
3064
+ if (!identifier || !credential) {
3065
+ return {
3066
+ success: false,
3067
+ error: "Email and password are required",
3068
+ errorCode: "INVALID_INPUT"
3069
+ };
3070
+ }
3071
+ const validation = this.validatePassword(credential);
3072
+ if (!validation.valid) {
3073
+ return {
3074
+ success: false,
3075
+ error: validation.errors.join(", "),
3076
+ errorCode: "INVALID_PASSWORD"
3077
+ };
3078
+ }
3079
+ return {
3080
+ success: true
3081
+ // Auth engine will handle user lookup and password verification
3082
+ };
3083
+ }
3084
+ /**
3085
+ * Store password hash for a user
3086
+ */
3087
+ async setPassword(userId, password) {
3088
+ const validation = this.validatePassword(password);
3089
+ if (!validation.valid) {
3090
+ return { success: false, errors: validation.errors };
3091
+ }
3092
+ const hash = await this.hashPassword(password);
3093
+ await this.storage.set(`password:user:${userId}`, {
3094
+ hash,
3095
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3096
+ });
3097
+ return { success: true };
3098
+ }
3099
+ /**
3100
+ * Verify password for a user
3101
+ */
3102
+ async verifyUserPassword(userId, password) {
3103
+ const data = await this.storage.get(`password:user:${userId}`);
3104
+ if (!data) {
3105
+ return false;
3106
+ }
3107
+ return this.verifyPassword(password, data.hash);
3108
+ }
3109
+ /**
3110
+ * Check if user has password set
3111
+ */
3112
+ async hasPassword(userId) {
3113
+ return this.storage.has(`password:user:${userId}`);
3114
+ }
3115
+ /**
3116
+ * Remove password for a user (switch to passwordless)
3117
+ */
3118
+ async removePassword(userId) {
3119
+ await this.storage.delete(`password:user:${userId}`);
3120
+ }
3121
+ /**
3122
+ * Get provider info
3123
+ */
3124
+ getInfo() {
3125
+ return {
3126
+ name: this.name,
3127
+ type: this.type,
3128
+ enabled: this.enabled,
3129
+ displayName: "Password"
3130
+ };
3131
+ }
3132
+ /**
3133
+ * Constant-time comparison to prevent timing attacks
3134
+ */
3135
+ constantTimeEquals(a, b) {
3136
+ if (a.length !== b.length) {
3137
+ return false;
3138
+ }
3139
+ let result = 0;
3140
+ for (let i = 0; i < a.length; i++) {
3141
+ result |= a[i] ^ b[i];
3142
+ }
3143
+ return result === 0;
3144
+ }
3145
+ };
3146
+ function createPasswordProvider(storage, config) {
3147
+ return new PasswordProvider(storage, config);
3148
+ }
3149
+ function toAdapterUser(user, authMethod) {
3150
+ return {
3151
+ id: user.id,
3152
+ email: authMethod?.provider === "email" ? authMethod.providerId : null,
3153
+ phone: authMethod?.provider === "phone" ? authMethod.providerId : null,
3154
+ name: user.displayName ?? null,
3155
+ avatar: user.avatarUrl ?? null,
3156
+ emailVerified: user.emailVerified,
3157
+ phoneVerified: user.phoneVerified,
3158
+ twoFactorEnabled: user.twoFactorEnabled,
3159
+ status: user.status,
3160
+ createdAt: user.insertedAt,
3161
+ updatedAt: user.updatedAt
3162
+ };
3163
+ }
3164
+ function toAdapterSession(session) {
3165
+ return {
3166
+ id: session.id,
3167
+ userId: session.userId,
3168
+ tenantId: session.currentTenantId ?? null,
3169
+ expiresAt: session.expiresAt,
3170
+ refreshExpiresAt: session.refreshExpiresAt ?? null,
3171
+ deviceType: session.deviceType ?? null,
3172
+ deviceName: session.deviceName ?? null,
3173
+ userAgent: session.userAgent ?? null,
3174
+ ipAddress: session.ipAddress ?? null,
3175
+ status: session.status,
3176
+ createdAt: session.insertedAt,
3177
+ updatedAt: session.updatedAt
3178
+ };
3179
+ }
3180
+ function toAdapterAuthMethod(method) {
3181
+ return {
3182
+ id: method.id,
3183
+ userId: method.userId,
3184
+ provider: method.provider,
3185
+ providerId: method.providerId,
3186
+ verified: method.verified,
3187
+ metadata: method.metadata ?? void 0,
3188
+ createdAt: method.insertedAt,
3189
+ updatedAt: method.updatedAt
3190
+ };
3191
+ }
3192
+ function toAdapterTenant(tenant) {
3193
+ return {
3194
+ id: tenant.id,
3195
+ name: tenant.name,
3196
+ slug: tenant.slug,
3197
+ status: tenant.status,
3198
+ createdAt: tenant.insertedAt,
3199
+ updatedAt: tenant.updatedAt
3200
+ };
3201
+ }
3202
+ function toAdapterMembership(membership, roleName) {
3203
+ return {
3204
+ id: membership.id,
3205
+ userId: membership.userId,
3206
+ tenantId: membership.tenantId,
3207
+ role: roleName ?? "member",
3208
+ permissions: membership.permissions ? Object.keys(membership.permissions) : void 0,
3209
+ status: membership.status,
3210
+ createdAt: membership.insertedAt,
3211
+ updatedAt: membership.updatedAt
3212
+ };
3213
+ }
3214
+ function createDrizzleAdapter(config) {
3215
+ const { db, schema, softDelete = true } = config;
3216
+ const { users, sessions, authMethods, tenants, tenantMemberships, roles } = schema;
3217
+ return {
3218
+ // ============================================
3219
+ // User Operations
3220
+ // ============================================
3221
+ async createUser(input) {
3222
+ const [user] = await db.insert(users).values({
3223
+ displayName: input.name,
3224
+ avatarUrl: input.avatar,
3225
+ emailVerified: input.emailVerified ?? false,
3226
+ phoneVerified: input.phoneVerified ?? false,
3227
+ twoFactorEnabled: false,
3228
+ status: "active",
3229
+ metadata: {}
3230
+ }).returning();
3231
+ let authMethod;
3232
+ if (input.email) {
3233
+ [authMethod] = await db.insert(authMethods).values({
3234
+ userId: user.id,
3235
+ provider: "email",
3236
+ providerId: input.email.toLowerCase(),
3237
+ verified: input.emailVerified ?? false
3238
+ }).returning();
3239
+ } else if (input.phone) {
3240
+ [authMethod] = await db.insert(authMethods).values({
3241
+ userId: user.id,
3242
+ provider: "phone",
3243
+ providerId: input.phone,
3244
+ verified: input.phoneVerified ?? false
3245
+ }).returning();
3246
+ }
3247
+ return toAdapterUser(user, authMethod);
3248
+ },
3249
+ async findUserById(id) {
3250
+ const [result] = await db.select().from(users).where(and(eq(users.id, id), softDelete ? isNull(users.deletedAt) : void 0)).limit(1);
3251
+ if (!result) return null;
3252
+ const [authMethod] = await db.select().from(authMethods).where(and(eq(authMethods.userId, id), isNull(authMethods.deletedAt))).limit(1);
3253
+ return toAdapterUser(result, authMethod);
3254
+ },
3255
+ async findUserByEmail(email) {
3256
+ const normalizedEmail = email.toLowerCase().trim();
3257
+ const [result] = await db.select({
3258
+ user: users,
3259
+ authMethod: authMethods
3260
+ }).from(authMethods).innerJoin(users, eq(authMethods.userId, users.id)).where(
3261
+ and(
3262
+ eq(authMethods.provider, "email"),
3263
+ eq(authMethods.providerId, normalizedEmail),
3264
+ softDelete ? isNull(users.deletedAt) : void 0
3265
+ )
3266
+ ).limit(1);
3267
+ if (!result) return null;
3268
+ return toAdapterUser(result.user, result.authMethod);
3269
+ },
3270
+ async findUserByPhone(phone) {
3271
+ const [result] = await db.select({
3272
+ user: users,
3273
+ authMethod: authMethods
3274
+ }).from(authMethods).innerJoin(users, eq(authMethods.userId, users.id)).where(
3275
+ and(
3276
+ eq(authMethods.provider, "phone"),
3277
+ eq(authMethods.providerId, phone),
3278
+ softDelete ? isNull(users.deletedAt) : void 0
3279
+ )
3280
+ ).limit(1);
3281
+ if (!result) return null;
3282
+ return toAdapterUser(result.user, result.authMethod);
3283
+ },
3284
+ async updateUser(id, data) {
3285
+ const updateData = {
3286
+ updatedAt: /* @__PURE__ */ new Date()
3287
+ };
3288
+ if (data.name !== void 0) updateData["displayName"] = data.name;
3289
+ if (data.avatar !== void 0) updateData["avatarUrl"] = data.avatar;
3290
+ if (data.emailVerified !== void 0) updateData["emailVerified"] = data.emailVerified;
3291
+ if (data.phoneVerified !== void 0) updateData["phoneVerified"] = data.phoneVerified;
3292
+ if (data.twoFactorEnabled !== void 0) updateData["twoFactorEnabled"] = data.twoFactorEnabled;
3293
+ if (data.status !== void 0) updateData["status"] = data.status;
3294
+ const [user] = await db.update(users).set(updateData).where(eq(users.id, id)).returning();
3295
+ const [authMethod] = await db.select().from(authMethods).where(eq(authMethods.userId, id)).limit(1);
3296
+ return toAdapterUser(user, authMethod);
3297
+ },
3298
+ async deleteUser(id) {
3299
+ if (softDelete) {
3300
+ await db.update(users).set({ deletedAt: /* @__PURE__ */ new Date() }).where(eq(users.id, id));
3301
+ } else {
3302
+ await db.delete(users).where(eq(users.id, id));
3303
+ }
3304
+ },
3305
+ // ============================================
3306
+ // Session Operations
3307
+ // ============================================
3308
+ async createSession(input) {
3309
+ const [session] = await db.insert(sessions).values({
3310
+ userId: input.userId,
3311
+ currentTenantId: input.tenantId,
3312
+ expiresAt: input.expiresAt,
3313
+ refreshExpiresAt: input.refreshExpiresAt,
3314
+ deviceType: input.deviceType,
3315
+ deviceName: input.deviceName,
3316
+ userAgent: input.userAgent,
3317
+ ipAddress: input.ipAddress,
3318
+ status: "active",
3319
+ csrfTokenHash: "",
3320
+ lastActivityAt: /* @__PURE__ */ new Date()
3321
+ }).returning();
3322
+ return toAdapterSession(session);
3323
+ },
3324
+ async findSessionById(id) {
3325
+ const [session] = await db.select().from(sessions).where(and(eq(sessions.id, id), eq(sessions.status, "active"))).limit(1);
3326
+ if (!session) return null;
3327
+ return toAdapterSession(session);
3328
+ },
3329
+ async findSessionsByUserId(userId) {
3330
+ const result = await db.select().from(sessions).where(and(eq(sessions.userId, userId), eq(sessions.status, "active"))).orderBy(desc(sessions.lastActivityAt));
3331
+ return result.map(toAdapterSession);
3332
+ },
3333
+ async updateSession(id, data) {
3334
+ const updateData = {
3335
+ updatedAt: /* @__PURE__ */ new Date()
3336
+ };
3337
+ if (data.tenantId !== void 0) updateData["currentTenantId"] = data.tenantId;
3338
+ if (data.expiresAt !== void 0) updateData["expiresAt"] = data.expiresAt;
3339
+ if (data.refreshExpiresAt !== void 0) updateData["refreshExpiresAt"] = data.refreshExpiresAt;
3340
+ if (data.status !== void 0) updateData["status"] = data.status;
3341
+ const [session] = await db.update(sessions).set(updateData).where(eq(sessions.id, id)).returning();
3342
+ return toAdapterSession(session);
3343
+ },
3344
+ async deleteSession(id) {
3345
+ await db.update(sessions).set({ status: "revoked", revokedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id));
3346
+ },
3347
+ async deleteSessionsByUserId(userId) {
3348
+ await db.update(sessions).set({ status: "revoked", revokedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.userId, userId));
3349
+ },
3350
+ // ============================================
3351
+ // Auth Method Operations
3352
+ // ============================================
3353
+ async createAuthMethod(input) {
3354
+ const [method] = await db.insert(authMethods).values({
3355
+ userId: input.userId,
3356
+ provider: input.provider,
3357
+ providerId: input.providerId,
3358
+ verified: input.verified ?? false,
3359
+ metadata: input.metadata ?? {}
3360
+ }).returning();
3361
+ return toAdapterAuthMethod(method);
3362
+ },
3363
+ async findAuthMethod(provider, providerId) {
3364
+ const [method] = await db.select().from(authMethods).where(
3365
+ and(
3366
+ eq(authMethods.provider, provider),
3367
+ eq(authMethods.providerId, providerId),
3368
+ softDelete ? isNull(authMethods.deletedAt) : void 0
3369
+ )
3370
+ ).limit(1);
3371
+ if (!method) return null;
3372
+ return toAdapterAuthMethod(method);
3373
+ },
3374
+ async findAuthMethodsByUserId(userId) {
3375
+ const result = await db.select().from(authMethods).where(
3376
+ and(eq(authMethods.userId, userId), softDelete ? isNull(authMethods.deletedAt) : void 0)
3377
+ );
3378
+ return result.map(toAdapterAuthMethod);
3379
+ },
3380
+ async deleteAuthMethod(id) {
3381
+ if (softDelete) {
3382
+ await db.update(authMethods).set({ deletedAt: /* @__PURE__ */ new Date() }).where(eq(authMethods.id, id));
3383
+ } else {
3384
+ await db.delete(authMethods).where(eq(authMethods.id, id));
3385
+ }
3386
+ },
3387
+ // ============================================
3388
+ // Tenant Operations (Optional)
3389
+ // ============================================
3390
+ async findTenantById(id) {
3391
+ if (!tenants) return null;
3392
+ const [tenant] = await db.select().from(tenants).where(and(eq(tenants.id, id), softDelete ? isNull(tenants.deletedAt) : void 0)).limit(1);
3393
+ if (!tenant) return null;
3394
+ return toAdapterTenant(tenant);
3395
+ },
3396
+ async findTenantBySlug(slug) {
3397
+ if (!tenants) return null;
3398
+ const [tenant] = await db.select().from(tenants).where(and(eq(tenants.slug, slug), softDelete ? isNull(tenants.deletedAt) : void 0)).limit(1);
3399
+ if (!tenant) return null;
3400
+ return toAdapterTenant(tenant);
3401
+ },
3402
+ // ============================================
3403
+ // Membership Operations (Optional)
3404
+ // ============================================
3405
+ async findMembership(userId, tenantId) {
3406
+ if (!tenantMemberships) return null;
3407
+ const [membership] = await db.select().from(tenantMemberships).where(
3408
+ and(
3409
+ eq(tenantMemberships.userId, userId),
3410
+ eq(tenantMemberships.tenantId, tenantId),
3411
+ softDelete ? isNull(tenantMemberships.deletedAt) : void 0
3412
+ )
3413
+ ).limit(1);
3414
+ if (!membership) return null;
3415
+ let roleName;
3416
+ if (roles && membership.roleId) {
3417
+ const [role] = await db.select().from(roles).where(eq(roles.id, membership.roleId)).limit(1);
3418
+ roleName = role?.name;
3419
+ }
3420
+ return toAdapterMembership(membership, roleName);
3421
+ },
3422
+ async findMembershipsByUserId(userId) {
3423
+ if (!tenantMemberships) return [];
3424
+ const result = await db.select().from(tenantMemberships).where(
3425
+ and(
3426
+ eq(tenantMemberships.userId, userId),
3427
+ eq(tenantMemberships.status, "active"),
3428
+ softDelete ? isNull(tenantMemberships.deletedAt) : void 0
3429
+ )
3430
+ );
3431
+ const membershipsWithRoles = await Promise.all(
3432
+ result.map(async (membership) => {
3433
+ let roleName;
3434
+ if (roles && membership.roleId) {
3435
+ const [role] = await db.select().from(roles).where(eq(roles.id, membership.roleId)).limit(1);
3436
+ roleName = role?.name;
3437
+ }
3438
+ return toAdapterMembership(membership, roleName);
3439
+ })
3440
+ );
3441
+ return membershipsWithRoles;
3442
+ },
3443
+ async createMembership(input) {
3444
+ if (!tenantMemberships) {
3445
+ throw new Error("Tenant memberships table not configured");
3446
+ }
3447
+ let roleId;
3448
+ if (roles && input.role) {
3449
+ const [role] = await db.select().from(roles).where(eq(roles.name, input.role)).limit(1);
3450
+ roleId = role?.id;
3451
+ }
3452
+ const [membership] = await db.insert(tenantMemberships).values({
3453
+ userId: input.userId,
3454
+ tenantId: input.tenantId,
3455
+ roleId: roleId ?? null,
3456
+ status: "active",
3457
+ permissions: input.permissions ? Object.fromEntries(input.permissions.map((p) => [p, true])) : {},
3458
+ accessLevel: "full",
3459
+ resourceRestrictions: {},
3460
+ joinedAt: /* @__PURE__ */ new Date()
3461
+ }).returning();
3462
+ return toAdapterMembership(membership, input.role);
3463
+ },
3464
+ async updateMembership(id, data) {
3465
+ if (!tenantMemberships) {
3466
+ throw new Error("Tenant memberships table not configured");
3467
+ }
3468
+ const updateData = {
3469
+ updatedAt: /* @__PURE__ */ new Date()
3470
+ };
3471
+ if (data.role !== void 0 && roles) {
3472
+ const [role] = await db.select().from(roles).where(eq(roles.name, data.role)).limit(1);
3473
+ updateData["roleId"] = role?.id ?? null;
3474
+ }
3475
+ if (data.status !== void 0) updateData["status"] = data.status;
3476
+ if (data.permissions !== void 0) {
3477
+ updateData["permissions"] = Object.fromEntries(data.permissions.map((p) => [p, true]));
3478
+ }
3479
+ const [membership] = await db.update(tenantMemberships).set(updateData).where(eq(tenantMemberships.id, id)).returning();
3480
+ let roleName;
3481
+ if (roles && membership.roleId) {
3482
+ const [role] = await db.select().from(roles).where(eq(roles.id, membership.roleId)).limit(1);
3483
+ roleName = role?.name;
3484
+ }
3485
+ return toAdapterMembership(membership, roleName);
3486
+ },
3487
+ async deleteMembership(id) {
3488
+ if (!tenantMemberships) return;
3489
+ if (softDelete) {
3490
+ await db.update(tenantMemberships).set({ deletedAt: /* @__PURE__ */ new Date(), status: "inactive" }).where(eq(tenantMemberships.id, id));
3491
+ } else {
3492
+ await db.delete(tenantMemberships).where(eq(tenantMemberships.id, id));
3493
+ }
3494
+ }
3495
+ };
3496
+ }
3497
+
3498
+ // src/services/email/resend-provider.ts
3499
+ var ResendEmailProvider = class {
3500
+ apiKey;
3501
+ fromEmail;
3502
+ fromName;
3503
+ baseUrl;
3504
+ constructor(config) {
3505
+ this.apiKey = config.apiKey;
3506
+ this.fromEmail = config.fromEmail ?? "noreply@example.com";
3507
+ this.fromName = config.fromName ?? "App";
3508
+ this.baseUrl = config.baseUrl ?? "https://api.resend.com";
3509
+ }
3510
+ /**
3511
+ * Send email via Resend API
3512
+ */
3513
+ async sendEmail(options) {
3514
+ const from = options.from ?? `${this.fromName} <${this.fromEmail}>`;
3515
+ try {
3516
+ const response = await fetch(`${this.baseUrl}/emails`, {
3517
+ method: "POST",
3518
+ headers: {
3519
+ "Authorization": `Bearer ${this.apiKey}`,
3520
+ "Content-Type": "application/json"
3521
+ },
3522
+ body: JSON.stringify({
3523
+ from,
3524
+ to: [options.to],
3525
+ subject: options.subject,
3526
+ html: options.html,
3527
+ text: options.text,
3528
+ reply_to: options.replyTo,
3529
+ cc: options.cc,
3530
+ bcc: options.bcc,
3531
+ headers: options.headers,
3532
+ attachments: options.attachments?.map((a) => ({
3533
+ filename: a.filename,
3534
+ content: typeof a.content === "string" ? a.content : a.content.toString("base64"),
3535
+ content_type: a.contentType
3536
+ }))
3537
+ })
3538
+ });
3539
+ if (!response.ok) {
3540
+ const errorData = await response.json().catch(() => ({}));
3541
+ return {
3542
+ success: false,
3543
+ error: errorData["message"] ?? `HTTP ${response.status}: ${response.statusText}`
3544
+ };
3545
+ }
3546
+ const data = await response.json();
3547
+ return {
3548
+ success: true,
3549
+ messageId: data["id"]
3550
+ };
3551
+ } catch (error) {
3552
+ return {
3553
+ success: false,
3554
+ error: error instanceof Error ? error.message : "Unknown error"
3555
+ };
3556
+ }
3557
+ }
3558
+ };
3559
+ function createResendProvider(config) {
3560
+ return new ResendEmailProvider(config);
3561
+ }
3562
+
3563
+ // src/services/email/templates/otp.ts
3564
+ function generateOTPEmailHTML(options) {
3565
+ const { code, expiresInMinutes, userName, appName } = options;
3566
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3567
+ return `
3568
+ <!DOCTYPE html>
3569
+ <html lang="en">
3570
+ <head>
3571
+ <meta charset="UTF-8">
3572
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3573
+ <title>Your Verification Code</title>
3574
+ </head>
3575
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f8fafc;">
3576
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
3577
+ <div style="background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); overflow: hidden;">
3578
+ <!-- Header -->
3579
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-align: center; padding: 40px 20px;">
3580
+ <h1 style="margin: 0; font-size: 24px; font-weight: 600;">${appName}</h1>
3581
+ <p style="margin: 10px 0 0; opacity: 0.9;">Verification Code</p>
3582
+ </div>
3583
+
3584
+ <!-- Content -->
3585
+ <div style="padding: 40px 30px;">
3586
+ <p style="font-size: 16px; margin-bottom: 20px;">${greeting},</p>
3587
+ <p style="color: #4a5568;">Use the following code to verify your identity:</p>
3588
+
3589
+ <!-- Code Box -->
3590
+ <div style="text-align: center; margin: 30px 0;">
3591
+ <div style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border: 2px dashed #cbd5e0; border-radius: 12px; padding: 25px; display: inline-block;">
3592
+ <span style="font-size: 36px; font-weight: 700; color: #4c51bf; letter-spacing: 8px; font-family: 'Monaco', 'Menlo', monospace;">
3593
+ ${code}
3594
+ </span>
3595
+ </div>
3596
+ </div>
3597
+
3598
+ <!-- Expiry Warning -->
3599
+ <div style="background: #fef7e0; border: 1px solid #f6e05e; border-radius: 8px; padding: 12px; margin: 20px 0; color: #744210; text-align: center; font-weight: 500;">
3600
+ \u23F1\uFE0F This code expires in ${expiresInMinutes} minutes
3601
+ </div>
3602
+
3603
+ <!-- Security Notice -->
3604
+ <div style="background: #fed7d7; border: 1px solid #fc8181; border-radius: 8px; padding: 12px; margin: 20px 0; color: #c53030; font-size: 14px;">
3605
+ \u{1F512} Never share this code with anyone. ${appName} will never ask for your code.
3606
+ </div>
3607
+
3608
+ <p style="color: #718096; font-size: 14px; margin-top: 30px;">
3609
+ If you didn't request this code, you can safely ignore this email.
3610
+ </p>
3611
+ </div>
3612
+
3613
+ <!-- Footer -->
3614
+ <div style="background: #f7fafc; padding: 20px 30px; text-align: center; border-top: 1px solid #e2e8f0;">
3615
+ <p style="color: #a0aec0; font-size: 12px; margin: 0;">
3616
+ &copy; ${(/* @__PURE__ */ new Date()).getFullYear()} ${appName}. All rights reserved.
3617
+ </p>
3618
+ </div>
3619
+ </div>
3620
+ </div>
3621
+ </body>
3622
+ </html>
3623
+ `;
3624
+ }
3625
+ function generateOTPEmailText(options) {
3626
+ const { code, expiresInMinutes, userName, appName } = options;
3627
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3628
+ return `${greeting},
3629
+
3630
+ Your ${appName} verification code is: ${code}
3631
+
3632
+ This code expires in ${expiresInMinutes} minutes.
3633
+
3634
+ Never share this code with anyone. ${appName} will never ask for your code.
3635
+
3636
+ If you didn't request this code, you can safely ignore this email.
3637
+
3638
+ - The ${appName} Team`;
3639
+ }
3640
+
3641
+ // src/services/email/templates/verification.ts
3642
+ function generateVerificationEmailHTML(options) {
3643
+ const { verificationUrl, userName, expiresInHours, appName } = options;
3644
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3645
+ return `
3646
+ <!DOCTYPE html>
3647
+ <html lang="en">
3648
+ <head>
3649
+ <meta charset="UTF-8">
3650
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3651
+ <title>Verify Your Email</title>
3652
+ </head>
3653
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f8fafc;">
3654
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
3655
+ <div style="background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); overflow: hidden;">
3656
+ <!-- Header -->
3657
+ <div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; text-align: center; padding: 40px 20px;">
3658
+ <h1 style="margin: 0; font-size: 24px; font-weight: 600;">${appName}</h1>
3659
+ <p style="margin: 10px 0 0; opacity: 0.9;">Email Verification</p>
3660
+ </div>
3661
+
3662
+ <!-- Content -->
3663
+ <div style="padding: 40px 30px;">
3664
+ <p style="font-size: 16px; margin-bottom: 20px;">${greeting},</p>
3665
+ <p style="color: #4a5568;">Please verify your email address by clicking the button below:</p>
3666
+
3667
+ <!-- Button -->
3668
+ <div style="text-align: center; margin: 30px 0;">
3669
+ <a href="${verificationUrl}" style="display: inline-block; padding: 14px 32px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 16px;">
3670
+ Verify Email Address
3671
+ </a>
3672
+ </div>
3673
+
3674
+ <!-- Expiry Notice -->
3675
+ <div style="background: #fef7e0; border: 1px solid #f6e05e; border-radius: 8px; padding: 12px; margin: 20px 0; color: #744210; text-align: center;">
3676
+ \u23F1\uFE0F This link expires in ${expiresInHours} hour${expiresInHours > 1 ? "s" : ""}
3677
+ </div>
3678
+
3679
+ <!-- Alternative Link -->
3680
+ <p style="color: #718096; font-size: 14px; margin-top: 20px;">
3681
+ If the button doesn't work, copy and paste this link into your browser:
3682
+ </p>
3683
+ <p style="background: #f7fafc; padding: 12px; border-radius: 6px; word-break: break-all; font-size: 12px; color: #4a5568;">
3684
+ ${verificationUrl}
3685
+ </p>
3686
+
3687
+ <p style="color: #718096; font-size: 14px; margin-top: 30px;">
3688
+ If you didn't create an account, you can safely ignore this email.
3689
+ </p>
3690
+ </div>
3691
+
3692
+ <!-- Footer -->
3693
+ <div style="background: #f7fafc; padding: 20px 30px; text-align: center; border-top: 1px solid #e2e8f0;">
3694
+ <p style="color: #a0aec0; font-size: 12px; margin: 0;">
3695
+ &copy; ${(/* @__PURE__ */ new Date()).getFullYear()} ${appName}. All rights reserved.
3696
+ </p>
3697
+ </div>
3698
+ </div>
3699
+ </div>
3700
+ </body>
3701
+ </html>
3702
+ `;
3703
+ }
3704
+ function generateVerificationEmailText(options) {
3705
+ const { verificationUrl, userName, expiresInHours, appName } = options;
3706
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3707
+ return `${greeting},
3708
+
3709
+ Please verify your email address by clicking the link below:
3710
+
3711
+ ${verificationUrl}
3712
+
3713
+ This link expires in ${expiresInHours} hour${expiresInHours > 1 ? "s" : ""}.
3714
+
3715
+ If you didn't create an account, you can safely ignore this email.
3716
+
3717
+ - The ${appName} Team`;
3718
+ }
3719
+
3720
+ // src/services/email/templates/magic-link.ts
3721
+ function generateMagicLinkEmailHTML(options) {
3722
+ const { magicLinkUrl, expiresInMinutes, userName, appName } = options;
3723
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3724
+ return `
3725
+ <!DOCTYPE html>
3726
+ <html lang="en">
3727
+ <head>
3728
+ <meta charset="UTF-8">
3729
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3730
+ <title>Sign in to ${appName}</title>
3731
+ </head>
3732
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f8fafc;">
3733
+ <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
3734
+ <div style="background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); overflow: hidden;">
3735
+ <!-- Header -->
3736
+ <div style="background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: white; text-align: center; padding: 40px 20px;">
3737
+ <h1 style="margin: 0; font-size: 24px; font-weight: 600;">${appName}</h1>
3738
+ <p style="margin: 10px 0 0; opacity: 0.9;">Sign In Link</p>
3739
+ </div>
3740
+
3741
+ <!-- Content -->
3742
+ <div style="padding: 40px 30px;">
3743
+ <p style="font-size: 16px; margin-bottom: 20px;">${greeting},</p>
3744
+ <p style="color: #4a5568;">Click the button below to sign in to your account:</p>
3745
+
3746
+ <!-- Button -->
3747
+ <div style="text-align: center; margin: 30px 0;">
3748
+ <a href="${magicLinkUrl}" style="display: inline-block; padding: 14px 32px; background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 16px;">
3749
+ Sign In to ${appName}
3750
+ </a>
3751
+ </div>
3752
+
3753
+ <!-- Expiry Notice -->
3754
+ <div style="background: #fef7e0; border: 1px solid #f6e05e; border-radius: 8px; padding: 12px; margin: 20px 0; color: #744210; text-align: center;">
3755
+ \u23F1\uFE0F This link expires in ${expiresInMinutes} minutes
3756
+ </div>
3757
+
3758
+ <!-- Security Notice -->
3759
+ <div style="background: #e0e7ff; border: 1px solid #818cf8; border-radius: 8px; padding: 12px; margin: 20px 0; color: #3730a3; font-size: 14px;">
3760
+ \u{1F512} This is a single-use link. After you click it, you'll be signed in automatically.
3761
+ </div>
3762
+
3763
+ <!-- Alternative Link -->
3764
+ <p style="color: #718096; font-size: 14px; margin-top: 20px;">
3765
+ If the button doesn't work, copy and paste this link into your browser:
3766
+ </p>
3767
+ <p style="background: #f7fafc; padding: 12px; border-radius: 6px; word-break: break-all; font-size: 12px; color: #4a5568;">
3768
+ ${magicLinkUrl}
3769
+ </p>
3770
+
3771
+ <p style="color: #718096; font-size: 14px; margin-top: 30px;">
3772
+ If you didn't request this sign-in link, you can safely ignore this email.
3773
+ </p>
3774
+ </div>
3775
+
3776
+ <!-- Footer -->
3777
+ <div style="background: #f7fafc; padding: 20px 30px; text-align: center; border-top: 1px solid #e2e8f0;">
3778
+ <p style="color: #a0aec0; font-size: 12px; margin: 0;">
3779
+ &copy; ${(/* @__PURE__ */ new Date()).getFullYear()} ${appName}. All rights reserved.
3780
+ </p>
3781
+ </div>
3782
+ </div>
3783
+ </div>
3784
+ </body>
3785
+ </html>
3786
+ `;
3787
+ }
3788
+ function generateMagicLinkEmailText(options) {
3789
+ const { magicLinkUrl, expiresInMinutes, userName, appName } = options;
3790
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3791
+ return `${greeting},
3792
+
3793
+ Click the link below to sign in to your ${appName} account:
3794
+
3795
+ ${magicLinkUrl}
3796
+
3797
+ This link expires in ${expiresInMinutes} minutes.
3798
+
3799
+ This is a single-use link. After you click it, you'll be signed in automatically.
3800
+
3801
+ If you didn't request this sign-in link, you can safely ignore this email.
3802
+
3803
+ - The ${appName} Team`;
3804
+ }
3805
+
3806
+ // src/services/email/index.ts
3807
+ var EmailService = class {
3808
+ provider;
3809
+ fromEmail;
3810
+ fromName;
3811
+ devMode;
3812
+ constructor(config) {
3813
+ this.devMode = config.devMode ?? process.env["NODE_ENV"] !== "production";
3814
+ this.fromEmail = config.fromEmail ?? process.env["EMAIL_FROM_ADDRESS"] ?? "noreply@example.com";
3815
+ this.fromName = config.fromName ?? process.env["EMAIL_FROM_NAME"] ?? "App";
3816
+ if (config.provider) {
3817
+ this.provider = config.provider;
3818
+ } else {
3819
+ const apiKey = config.apiKey ?? process.env["RESEND_API_KEY"] ?? process.env["EMAIL_API_KEY"];
3820
+ if (!apiKey) {
3821
+ throw new Error("Email API key is required (RESEND_API_KEY or EMAIL_API_KEY)");
3822
+ }
3823
+ this.provider = new ResendEmailProvider({
3824
+ apiKey,
3825
+ fromEmail: this.fromEmail,
3826
+ fromName: this.fromName
3827
+ });
3828
+ }
3829
+ }
3830
+ /**
3831
+ * Send email
3832
+ */
3833
+ async sendEmail(options) {
3834
+ if (this.devMode) {
3835
+ console.log("\u{1F4E7} EMAIL (Development Mode):");
3836
+ console.log("To:", options.to);
3837
+ console.log("Subject:", options.subject);
3838
+ console.log("From:", options.from ?? `${this.fromName} <${this.fromEmail}>`);
3839
+ console.log("Content:", options.html?.substring(0, 200) ?? options.text?.substring(0, 200));
3840
+ console.log("\u2500".repeat(50));
3841
+ return { success: true, messageId: "dev-mode-" + Date.now() };
3842
+ }
3843
+ return this.provider.sendEmail(options);
3844
+ }
3845
+ /**
3846
+ * Send OTP verification email
3847
+ */
3848
+ async sendOTPEmail(options) {
3849
+ const { email, code, expiresInMinutes = 10, userName, appName = this.fromName } = options;
3850
+ return this.sendEmail({
3851
+ to: email,
3852
+ subject: `Your ${appName} Verification Code`,
3853
+ html: generateOTPEmailHTML({ code, expiresInMinutes, userName, appName }),
3854
+ text: generateOTPEmailText({ code, expiresInMinutes, userName, appName })
3855
+ });
3856
+ }
3857
+ /**
3858
+ * Send email verification email
3859
+ */
3860
+ async sendVerificationEmail(options) {
3861
+ const { email, verificationUrl, userName, expiresInHours = 24, appName = this.fromName } = options;
3862
+ return this.sendEmail({
3863
+ to: email,
3864
+ subject: `Verify your ${appName} email`,
3865
+ html: generateVerificationEmailHTML({ verificationUrl, userName, expiresInHours, appName }),
3866
+ text: generateVerificationEmailText({ verificationUrl, userName, expiresInHours, appName })
3867
+ });
3868
+ }
3869
+ /**
3870
+ * Send magic link email
3871
+ */
3872
+ async sendMagicLinkEmail(options) {
3873
+ const { email, magicLinkUrl, expiresInMinutes = 15, userName, appName = this.fromName } = options;
3874
+ return this.sendEmail({
3875
+ to: email,
3876
+ subject: `Sign in to ${appName}`,
3877
+ html: generateMagicLinkEmailHTML({ magicLinkUrl, expiresInMinutes, userName, appName }),
3878
+ text: generateMagicLinkEmailText({ magicLinkUrl, expiresInMinutes, userName, appName })
3879
+ });
3880
+ }
3881
+ /**
3882
+ * Send welcome email
3883
+ */
3884
+ async sendWelcomeEmail(options) {
3885
+ const { email, userName, appName = this.fromName, loginUrl } = options;
3886
+ const greeting = userName ? `Hi ${userName}` : "Welcome";
3887
+ return this.sendEmail({
3888
+ to: email,
3889
+ subject: `Welcome to ${appName}!`,
3890
+ html: `
3891
+ <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
3892
+ <h1>${greeting},</h1>
3893
+ <p>Thank you for joining ${appName}!</p>
3894
+ <p>We're excited to have you on board.</p>
3895
+ ${loginUrl ? `<p><a href="${loginUrl}" style="display: inline-block; padding: 12px 24px; background: #4F46E5; color: white; text-decoration: none; border-radius: 6px;">Get Started</a></p>` : ""}
3896
+ <p>Best regards,<br>The ${appName} Team</p>
3897
+ </div>
3898
+ `,
3899
+ text: `${greeting},
3900
+
3901
+ Thank you for joining ${appName}!
3902
+
3903
+ We're excited to have you on board.
3904
+
3905
+ ${loginUrl ? `Get Started: ${loginUrl}
3906
+
3907
+ ` : ""}Best regards,
3908
+ The ${appName} Team`
3909
+ });
3910
+ }
3911
+ /**
3912
+ * Send password reset email
3913
+ */
3914
+ async sendPasswordResetEmail(options) {
3915
+ const { email, resetUrl, expiresInHours = 1, userName, appName = this.fromName } = options;
3916
+ const greeting = userName ? `Hi ${userName}` : "Hello";
3917
+ return this.sendEmail({
3918
+ to: email,
3919
+ subject: `Reset your ${appName} password`,
3920
+ html: `
3921
+ <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
3922
+ <h1>${greeting},</h1>
3923
+ <p>We received a request to reset your password.</p>
3924
+ <p>Click the button below to reset it. This link expires in ${expiresInHours} hour${expiresInHours > 1 ? "s" : ""}.</p>
3925
+ <p><a href="${resetUrl}" style="display: inline-block; padding: 12px 24px; background: #4F46E5; color: white; text-decoration: none; border-radius: 6px;">Reset Password</a></p>
3926
+ <p style="color: #666; font-size: 14px;">If you didn't request this, you can safely ignore this email.</p>
3927
+ <p>Best regards,<br>The ${appName} Team</p>
3928
+ </div>
3929
+ `,
3930
+ text: `${greeting},
3931
+
3932
+ We received a request to reset your password.
3933
+
3934
+ Click the link below to reset it. This link expires in ${expiresInHours} hour${expiresInHours > 1 ? "s" : ""}.
3935
+
3936
+ ${resetUrl}
3937
+
3938
+ If you didn't request this, you can safely ignore this email.
3939
+
3940
+ Best regards,
3941
+ The ${appName} Team`
3942
+ });
3943
+ }
3944
+ /**
3945
+ * Send invitation email
3946
+ */
3947
+ async sendInvitationEmail(options) {
3948
+ const {
3949
+ email,
3950
+ invitationUrl,
3951
+ inviterName,
3952
+ organizationName,
3953
+ roleName,
3954
+ expiresInDays = 7,
3955
+ appName = this.fromName
3956
+ } = options;
3957
+ const roleText = roleName ? ` as a ${roleName}` : "";
3958
+ return this.sendEmail({
3959
+ to: email,
3960
+ subject: `You're invited to join ${organizationName} on ${appName}`,
3961
+ html: `
3962
+ <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
3963
+ <h1>You're Invited!</h1>
3964
+ <p>${inviterName} has invited you to join <strong>${organizationName}</strong>${roleText}.</p>
3965
+ <p>Click the button below to accept the invitation. This link expires in ${expiresInDays} day${expiresInDays > 1 ? "s" : ""}.</p>
3966
+ <p><a href="${invitationUrl}" style="display: inline-block; padding: 12px 24px; background: #4F46E5; color: white; text-decoration: none; border-radius: 6px;">Accept Invitation</a></p>
3967
+ <p style="color: #666; font-size: 14px;">If you don't want to join, you can safely ignore this email.</p>
3968
+ <p>Best regards,<br>The ${appName} Team</p>
3969
+ </div>
3970
+ `,
3971
+ text: `You're Invited!
3972
+
3973
+ ${inviterName} has invited you to join ${organizationName}${roleText}.
3974
+
3975
+ Click the link below to accept the invitation. This link expires in ${expiresInDays} day${expiresInDays > 1 ? "s" : ""}.
3976
+
3977
+ ${invitationUrl}
3978
+
3979
+ If you don't want to join, you can safely ignore this email.
3980
+
3981
+ Best regards,
3982
+ The ${appName} Team`
3983
+ });
3984
+ }
3985
+ };
3986
+ function createEmailService(config) {
3987
+ return new EmailService(config);
3988
+ }
3989
+
3990
+ // src/services/sms/netgsm-provider.ts
3991
+ var NetGSMProvider = class {
3992
+ config;
3993
+ constructor(config) {
3994
+ this.config = {
3995
+ apiUrl: "https://api.netgsm.com.tr/sms/send/otp",
3996
+ ...config
3997
+ };
3998
+ }
3999
+ /**
4000
+ * Send SMS via NetGSM API
4001
+ */
4002
+ async sendSMS(options) {
4003
+ try {
4004
+ const xmlData = `<?xml version='1.0' encoding='iso-8859-9'?>
4005
+ <mainbody>
4006
+ <header>
4007
+ <usercode>${this.escapeXml(this.config.username)}</usercode>
4008
+ <password>${this.escapeXml(this.config.password)}</password>
4009
+ <msgheader>${this.escapeXml(options.from ?? this.config.header)}</msgheader>
4010
+ <encoding>TR</encoding>
4011
+ </header>
4012
+ <body>
4013
+ <msg><![CDATA[${options.message}]]></msg>
4014
+ <no>${this.sanitizePhoneNumber(options.to)}</no>
4015
+ </body>
4016
+ </mainbody>`;
4017
+ const response = await fetch(this.config.apiUrl, {
4018
+ method: "POST",
4019
+ headers: {
4020
+ "Content-Type": "application/xml"
4021
+ },
4022
+ body: xmlData
4023
+ });
4024
+ if (!response.ok) {
4025
+ return {
4026
+ success: false,
4027
+ error: `HTTP ${response.status}: ${response.statusText}`
4028
+ };
4029
+ }
4030
+ const responseText = await response.text();
4031
+ const result = this.parseResponse(responseText);
4032
+ if (result.success) {
4033
+ return {
4034
+ success: true,
4035
+ messageId: result.code
4036
+ };
4037
+ }
4038
+ return {
4039
+ success: false,
4040
+ error: `NetGSM error code: ${result.code} - ${this.getErrorMessage(result.code ?? "")}`
4041
+ };
4042
+ } catch (error) {
4043
+ return {
4044
+ success: false,
4045
+ error: error instanceof Error ? error.message : "Unknown error"
4046
+ };
4047
+ }
4048
+ }
4049
+ /**
4050
+ * Parse NetGSM XML response
4051
+ */
4052
+ parseResponse(xml) {
4053
+ try {
4054
+ const codeMatch = xml.match(/<code>(.*?)<\/code>/);
4055
+ if (!codeMatch) {
4056
+ const trimmed = xml.trim();
4057
+ if (trimmed.length < 20 && /^\d+$/.test(trimmed)) {
4058
+ if (trimmed === "00" || trimmed === "0") {
4059
+ return { success: true, code: trimmed };
4060
+ }
4061
+ return { success: false, code: trimmed };
4062
+ }
4063
+ return { success: false };
4064
+ }
4065
+ const code = codeMatch[1].trim();
4066
+ if (code === "00" || code === "0" || code.length > 10) {
4067
+ return { success: true, code };
4068
+ }
4069
+ return { success: false, code };
4070
+ } catch {
4071
+ return { success: false };
4072
+ }
4073
+ }
4074
+ /**
4075
+ * Get human-readable error message
4076
+ */
4077
+ getErrorMessage(code) {
4078
+ const errors = {
4079
+ "20": "Message text is missing",
4080
+ "30": "Invalid credentials",
4081
+ "40": "Sender ID not approved",
4082
+ "50": "Invalid recipient number",
4083
+ "51": "Duplicate recipient",
4084
+ "60": "OTP sending blocked",
4085
+ "70": "Incorrect parameter format",
4086
+ "80": "Query limit exceeded",
4087
+ "85": "Same content sent too frequently"
4088
+ };
4089
+ return errors[code] ?? "Unknown error";
4090
+ }
4091
+ /**
4092
+ * Escape XML special characters
4093
+ */
4094
+ escapeXml(unsafe) {
4095
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4096
+ }
4097
+ /**
4098
+ * Sanitize phone number
4099
+ */
4100
+ sanitizePhoneNumber(phone) {
4101
+ let cleaned = phone.replace(/[\s\-\(\)]/g, "");
4102
+ if (cleaned.startsWith("+")) {
4103
+ cleaned = cleaned.substring(1);
4104
+ }
4105
+ if (cleaned.startsWith("5") && cleaned.length === 10) {
4106
+ cleaned = "90" + cleaned;
4107
+ }
4108
+ return cleaned;
4109
+ }
4110
+ };
4111
+ function createNetGSMProvider(config) {
4112
+ return new NetGSMProvider(config);
4113
+ }
4114
+
4115
+ // src/services/sms/index.ts
4116
+ var SMSService = class {
4117
+ provider;
4118
+ senderId;
4119
+ devMode;
4120
+ constructor(config) {
4121
+ this.devMode = config.devMode ?? process.env["NODE_ENV"] !== "production";
4122
+ this.senderId = config.senderId ?? process.env["SMS_SENDER_ID"] ?? "App";
4123
+ this.provider = config.provider ?? null;
4124
+ }
4125
+ /**
4126
+ * Send SMS
4127
+ */
4128
+ async sendSMS(options) {
4129
+ if (this.devMode) {
4130
+ console.log("\u{1F4F1} SMS (Development Mode):");
4131
+ console.log("To:", options.to);
4132
+ console.log("From:", options.from ?? this.senderId);
4133
+ console.log("Message:", options.message);
4134
+ console.log("\u2500".repeat(50));
4135
+ return { success: true, messageId: "dev-mode-" + Date.now() };
4136
+ }
4137
+ if (!this.provider) {
4138
+ return { success: false, error: "SMS provider not configured" };
4139
+ }
4140
+ return this.provider.sendSMS({
4141
+ ...options,
4142
+ from: options.from ?? this.senderId
4143
+ });
4144
+ }
4145
+ /**
4146
+ * Send OTP SMS
4147
+ */
4148
+ async sendOTPSMS(options) {
4149
+ const { phone, code, expiresInMinutes = 10, appName = this.senderId } = options;
4150
+ const message = `Your ${appName} verification code is: ${code}. Valid for ${expiresInMinutes} minutes.`;
4151
+ return this.sendSMS({
4152
+ to: phone,
4153
+ message
4154
+ });
4155
+ }
4156
+ };
4157
+ function createSMSService(config) {
4158
+ return new SMSService(config);
4159
+ }
4160
+
4161
+ // src/services/email-verification/index.ts
4162
+ var EmailVerificationService = class {
4163
+ storage;
4164
+ config;
4165
+ constructor(storage, config) {
4166
+ this.storage = storage;
4167
+ this.config = {
4168
+ callbackPath: "/auth/verify-email",
4169
+ expiresIn: 86400,
4170
+ // 24 hours
4171
+ tokenLength: 32,
4172
+ ...config
4173
+ };
4174
+ }
4175
+ /**
4176
+ * Create verification token for an email
4177
+ */
4178
+ async createVerificationToken(email, options) {
4179
+ const normalizedEmail = email.toLowerCase().trim();
4180
+ const token = await generateRandomHex(this.config.tokenLength);
4181
+ const tokenHash = await sha256Hex(token);
4182
+ const expiresAt = new Date(Date.now() + this.config.expiresIn * 1e3);
4183
+ await this.deleteTokensByEmail(normalizedEmail);
4184
+ const tokenData = {
4185
+ email: normalizedEmail,
4186
+ tokenHash,
4187
+ expiresAt: expiresAt.toISOString(),
4188
+ createdBy: options?.createdBy
4189
+ };
4190
+ await this.storage.set(`email-verify:hash:${tokenHash}`, tokenData, this.config.expiresIn);
4191
+ await this.storage.set(`email-verify:email:${normalizedEmail}`, tokenHash, this.config.expiresIn);
4192
+ const params = new URLSearchParams({ token });
4193
+ const verificationUrl = `${this.config.baseUrl}${this.config.callbackPath}?${params}`;
4194
+ return {
4195
+ success: true,
4196
+ token,
4197
+ verificationUrl,
4198
+ expiresAt
4199
+ };
4200
+ }
4201
+ /**
4202
+ * Verify an email token
4203
+ */
4204
+ async verifyToken(token) {
4205
+ const tokenHash = await sha256Hex(token);
4206
+ const tokenData = await this.storage.get(`email-verify:hash:${tokenHash}`);
4207
+ if (!tokenData) {
4208
+ return { success: false, error: "Invalid or expired verification token" };
4209
+ }
4210
+ if (tokenData.usedAt) {
4211
+ return { success: false, error: "Verification token already used" };
4212
+ }
4213
+ if (new Date(tokenData.expiresAt) < /* @__PURE__ */ new Date()) {
4214
+ await this.deleteToken(tokenHash);
4215
+ return { success: false, error: "Verification token expired" };
4216
+ }
4217
+ tokenData.usedAt = (/* @__PURE__ */ new Date()).toISOString();
4218
+ await this.storage.set(`email-verify:hash:${tokenHash}`, tokenData, 300);
4219
+ await this.storage.delete(`email-verify:email:${tokenData.email}`);
4220
+ return {
4221
+ success: true,
4222
+ email: tokenData.email
4223
+ };
4224
+ }
4225
+ /**
4226
+ * Check verification status for an email
4227
+ */
4228
+ async getStatus(email) {
4229
+ const normalizedEmail = email.toLowerCase().trim();
4230
+ const tokenHash = await this.storage.get(`email-verify:email:${normalizedEmail}`);
4231
+ if (tokenHash) {
4232
+ const tokenData = await this.storage.get(`email-verify:hash:${tokenHash}`);
4233
+ if (tokenData && !tokenData.usedAt) {
4234
+ return {
4235
+ email: normalizedEmail,
4236
+ verified: false,
4237
+ pendingVerification: true,
4238
+ expiresAt: new Date(tokenData.expiresAt)
4239
+ };
4240
+ }
4241
+ }
4242
+ return {
4243
+ email: normalizedEmail,
4244
+ verified: false,
4245
+ pendingVerification: false
4246
+ };
4247
+ }
4248
+ /**
4249
+ * Resend verification email
4250
+ * Returns a new token for the same email
4251
+ */
4252
+ async resendVerification(email, options) {
4253
+ return this.createVerificationToken(email, options);
4254
+ }
4255
+ /**
4256
+ * Cancel pending verification
4257
+ */
4258
+ async cancelVerification(email) {
4259
+ const normalizedEmail = email.toLowerCase().trim();
4260
+ await this.deleteTokensByEmail(normalizedEmail);
4261
+ }
4262
+ /**
4263
+ * Delete token by hash
4264
+ */
4265
+ async deleteToken(tokenHash) {
4266
+ const tokenData = await this.storage.get(`email-verify:hash:${tokenHash}`);
4267
+ if (tokenData) {
4268
+ await this.storage.delete(`email-verify:email:${tokenData.email}`);
4269
+ }
4270
+ await this.storage.delete(`email-verify:hash:${tokenHash}`);
4271
+ }
4272
+ /**
4273
+ * Delete all tokens for an email
4274
+ */
4275
+ async deleteTokensByEmail(email) {
4276
+ const tokenHash = await this.storage.get(`email-verify:email:${email}`);
4277
+ if (tokenHash) {
4278
+ await this.storage.delete(`email-verify:hash:${tokenHash}`);
4279
+ }
4280
+ await this.storage.delete(`email-verify:email:${email}`);
4281
+ }
4282
+ };
4283
+ function createEmailVerificationService(storage, config) {
4284
+ return new EmailVerificationService(storage, config);
4285
+ }
4286
+
4287
+ // src/index.ts
4288
+ function createAuth(config) {
4289
+ return createAuthEngine(config);
4290
+ }
4291
+
4292
+ export { AppleProvider, EmailService, EmailVerificationService, GitHubProvider, GoogleProvider, InvitationService, MagicLinkProvider, MicrosoftProvider, MultiStrategyTenantResolver, NetGSMProvider, OAuthManager, ParsAuthEngine, PasswordProvider, ResendEmailProvider, SMSService, TOTPProvider, TenantManager, TenantResolver, WebAuthnProvider, createAuth, createDrizzleAdapter, createEmailService, createEmailVerificationService, createInvitationService, createMagicLinkProvider, createMultiStrategyResolver, createNetGSMProvider, createOAuthManager, createPasswordProvider, createResendProvider, createSMSService, createTOTPProvider, createTenantManager, createTenantResolver, createWebAuthnProvider, defaultConfig, generatePKCE, generateState, mergeConfig, validateConfig };
4293
+ //# sourceMappingURL=index.js.map
4294
+ //# sourceMappingURL=index.js.map