@snapback/cli 1.1.12 → 1.1.15

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 (59) hide show
  1. package/README.md +79 -18
  2. package/dist/SkippedTestDetector-AXTMWWHC.js +5 -0
  3. package/dist/SkippedTestDetector-QLSQV7K7.js +5 -0
  4. package/dist/analysis-6WTBZJH3.js +6 -0
  5. package/dist/analysis-C472LUGW.js +2475 -0
  6. package/dist/auth-HFJRXXG2.js +1446 -0
  7. package/dist/auto-provision-organization-SF6XM7X4.js +161 -0
  8. package/dist/chunk-23G5VYA3.js +4259 -0
  9. package/dist/{chunk-QAKFE3NE.js → chunk-4YTE4JEW.js} +3 -4
  10. package/dist/chunk-5EOPYJ4Y.js +12 -0
  11. package/dist/{chunk-G7QXHNGB.js → chunk-5SQA44V7.js} +1125 -32
  12. package/dist/{chunk-BW7RALUZ.js → chunk-7ADPL4Q3.js} +11 -4
  13. package/dist/chunk-CBGOC6RV.js +293 -0
  14. package/dist/chunk-DNEADD2G.js +3499 -0
  15. package/dist/{chunk-NKBZIXCN.js → chunk-DPWFZNMY.js} +122 -15
  16. package/dist/chunk-GQ73B37K.js +314 -0
  17. package/dist/chunk-HR34NJP7.js +6133 -0
  18. package/dist/chunk-ICKSHS3A.js +2264 -0
  19. package/dist/{chunk-KPETDXQO.js → chunk-OI2HNNT6.js} +565 -50
  20. package/dist/chunk-PL4HF4M2.js +593 -0
  21. package/dist/chunk-WS36HDEU.js +3735 -0
  22. package/dist/chunk-XYU5FFE3.js +111 -0
  23. package/dist/chunk-ZBQDE6WJ.js +108 -0
  24. package/dist/client-WIO6W447.js +8 -0
  25. package/dist/dist-E7E2T3DQ.js +9 -0
  26. package/dist/dist-TEWNOZYS.js +5 -0
  27. package/dist/dist-YZBJAYEJ.js +12 -0
  28. package/dist/index.js +65215 -26627
  29. package/dist/local-service-adapter-3JHN6G4O.js +6 -0
  30. package/dist/pioneer-oauth-hook-V2JKEXM7.js +12 -0
  31. package/dist/{secure-credentials-6UMEU22H.js → secure-credentials-UEPG7GWW.js} +15 -8
  32. package/dist/snapback-dir-MG7DTRMF.js +6 -0
  33. package/package.json +8 -42
  34. package/scripts/postinstall.mjs +2 -3
  35. package/dist/SkippedTestDetector-B3JZUE5G.js +0 -5
  36. package/dist/SkippedTestDetector-B3JZUE5G.js.map +0 -1
  37. package/dist/analysis-Z53F5FT2.js +0 -6
  38. package/dist/analysis-Z53F5FT2.js.map +0 -1
  39. package/dist/chunk-6MR2TINI.js +0 -27
  40. package/dist/chunk-6MR2TINI.js.map +0 -1
  41. package/dist/chunk-BW7RALUZ.js.map +0 -1
  42. package/dist/chunk-G7QXHNGB.js.map +0 -1
  43. package/dist/chunk-ISVRGBWT.js +0 -16223
  44. package/dist/chunk-ISVRGBWT.js.map +0 -1
  45. package/dist/chunk-KPETDXQO.js.map +0 -1
  46. package/dist/chunk-NKBZIXCN.js.map +0 -1
  47. package/dist/chunk-QAKFE3NE.js.map +0 -1
  48. package/dist/chunk-YOVA65PS.js +0 -12745
  49. package/dist/chunk-YOVA65PS.js.map +0 -1
  50. package/dist/dist-7UKXVKH3.js +0 -5
  51. package/dist/dist-7UKXVKH3.js.map +0 -1
  52. package/dist/dist-VDK7WEF4.js +0 -5
  53. package/dist/dist-VDK7WEF4.js.map +0 -1
  54. package/dist/dist-WKLJSPJT.js +0 -8
  55. package/dist/dist-WKLJSPJT.js.map +0 -1
  56. package/dist/index.js.map +0 -1
  57. package/dist/secure-credentials-6UMEU22H.js.map +0 -1
  58. package/dist/snapback-dir-T3CRQRY6.js +0 -6
  59. package/dist/snapback-dir-T3CRQRY6.js.map +0 -1
@@ -0,0 +1,1446 @@
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+ import { config, EntitlementsServiceImpl, getBaseUrl, ENABLE_ENHANCED_2FA, ENABLE_SSO, ENABLE_CAPTCHA, ENABLE_MULTI_SESSION, getOrganizationWithPurchasesAndMembersCount, getPendingInvitationByEmail } from './chunk-23G5VYA3.js';
3
+ import './chunk-GQ73B37K.js';
4
+ import { combinedSchema, db } from './chunk-HR34NJP7.js';
5
+ import { trackEvent } from './chunk-XYU5FFE3.js';
6
+ import './chunk-ICKSHS3A.js';
7
+ import { logger } from './chunk-PL4HF4M2.js';
8
+ import { createLogger, LogLevel } from './chunk-WS36HDEU.js';
9
+ import './chunk-5EOPYJ4Y.js';
10
+ import './chunk-CBGOC6RV.js';
11
+ import { __name } from './chunk-7ADPL4Q3.js';
12
+ import { passkey } from '@better-auth/passkey';
13
+ import { sso } from '@better-auth/sso';
14
+ import { render } from '@react-email/render';
15
+ import { betterAuth } from 'better-auth';
16
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
17
+ import { bearer, anonymous, deviceAuthorization, admin as admin$1, apiKey, jwt, magicLink, openAPI, organization, haveIBeenPwned, twoFactor, username, captcha, multiSession, createAuthMiddleware } from 'better-auth/plugins';
18
+ import 'cookie';
19
+ import { nanoid } from 'nanoid';
20
+ import { createAccessControl } from 'better-auth/plugins/access';
21
+ import { APIError } from 'better-auth/api';
22
+ import 'drizzle-orm';
23
+ import Stripe from 'stripe';
24
+
25
+ process.env.SNAPBACK_CLI='true';
26
+
27
+ // ../../packages/config/dist/client.js
28
+ config.payments.plans;
29
+
30
+ // ../../packages/integrations/dist/email/handlers/billing-events.js
31
+ (class {
32
+ static {
33
+ __name(this, "BillingEmailHandler");
34
+ }
35
+ emailService;
36
+ constructor(emailService2) {
37
+ this.emailService = emailService2;
38
+ }
39
+ /**
40
+ * Send welcome email after successful subscription
41
+ */
42
+ async sendWelcome(params) {
43
+ const request = {
44
+ to: {
45
+ email: params.to,
46
+ userId: params.userId
47
+ },
48
+ category: "onboarding",
49
+ priority: "high",
50
+ template: {
51
+ id: "product.welcome",
52
+ props: {
53
+ firstName: params.firstName,
54
+ planName: params.planName,
55
+ planFeatures: params.planFeatures,
56
+ dashboardUrl: params.dashboardUrl,
57
+ docsUrl: params.docsUrl,
58
+ pioneerTier: params.pioneerTier,
59
+ pioneerPoints: params.pioneerPoints
60
+ }
61
+ }
62
+ };
63
+ await this.emailService.send(request);
64
+ }
65
+ /**
66
+ * Send subscription cancellation confirmation
67
+ */
68
+ async sendCancellationConfirmation(params) {
69
+ const request = {
70
+ to: {
71
+ email: params.to,
72
+ userId: params.userId
73
+ },
74
+ category: "billing",
75
+ priority: "high",
76
+ template: {
77
+ id: "product.system-alert",
78
+ props: {
79
+ alertType: "subscription_cancelled",
80
+ title: "Subscription Cancelled",
81
+ message: `Your ${params.planName} subscription has been cancelled and will remain active until ${params.cancellationDate.toLocaleDateString()}. Your data will be retained for ${params.dataRetentionDays} days after that date.`,
82
+ severity: "info",
83
+ actionRequired: false,
84
+ additionalDetails: "You can reactivate your subscription at any time before the retention period ends.",
85
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
86
+ }
87
+ }
88
+ };
89
+ await this.emailService.send(request);
90
+ }
91
+ /**
92
+ * Send payment failure alert
93
+ */
94
+ async sendPaymentFailed(params) {
95
+ const request = {
96
+ to: {
97
+ email: params.to,
98
+ userId: params.userId
99
+ },
100
+ category: "billing",
101
+ priority: "critical",
102
+ template: {
103
+ id: "product.system-alert",
104
+ props: {
105
+ alertType: "payment_failed",
106
+ title: "Payment Failed",
107
+ message: `We were unable to process your payment of ${params.amount} ${params.currency} for your ${params.planName} subscription. We'll automatically retry on ${params.retryDate.toLocaleDateString()}.`,
108
+ severity: "warning",
109
+ actionRequired: true,
110
+ actionUrl: params.updatePaymentUrl,
111
+ actionLabel: "Update Payment Method",
112
+ additionalDetails: "To avoid service interruption, please update your payment method or ensure sufficient funds are available.",
113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
114
+ }
115
+ }
116
+ };
117
+ await this.emailService.send(request);
118
+ }
119
+ /**
120
+ * Send subscription upgraded notification
121
+ */
122
+ async sendSubscriptionUpgraded(params) {
123
+ const request = {
124
+ to: {
125
+ email: params.to,
126
+ userId: params.userId
127
+ },
128
+ category: "billing",
129
+ priority: "normal",
130
+ template: {
131
+ id: "product.system-alert",
132
+ props: {
133
+ alertType: "subscription_upgraded",
134
+ title: "Subscription Upgraded",
135
+ message: `You've successfully upgraded from ${params.oldPlan} to ${params.newPlan}. Your new features are now active!`,
136
+ severity: "info",
137
+ actionRequired: false,
138
+ actionUrl: params.dashboardUrl,
139
+ actionLabel: "Explore New Features",
140
+ additionalDetails: `New features: ${params.newFeatures.join(", ")}`,
141
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
142
+ }
143
+ }
144
+ };
145
+ await this.emailService.send(request);
146
+ }
147
+ /**
148
+ * Send trial ending reminder
149
+ */
150
+ async sendTrialEnding(params) {
151
+ const request = {
152
+ to: {
153
+ email: params.to,
154
+ userId: params.userId
155
+ },
156
+ category: "billing",
157
+ priority: "high",
158
+ template: {
159
+ id: "product.system-alert",
160
+ props: {
161
+ alertType: "trial_ending",
162
+ title: `Your ${params.planName} Trial Ends Soon`,
163
+ message: `Your free trial ends in ${params.daysRemaining} day${params.daysRemaining !== 1 ? "s" : ""} on ${params.trialEndDate.toLocaleDateString()}. Upgrade now to keep your data and continue using SnapBack.`,
164
+ severity: "warning",
165
+ actionRequired: true,
166
+ actionUrl: params.upgradeUrl,
167
+ actionLabel: "Upgrade Now",
168
+ additionalDetails: "After your trial ends, you can still access your account but snapshot capture will be disabled until you upgrade.",
169
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
170
+ }
171
+ }
172
+ };
173
+ await this.emailService.send(request);
174
+ }
175
+ /**
176
+ * Send suspension notice (Day 8 of grace period)
177
+ *
178
+ * Called when subscription transitions from past_due to suspended
179
+ * after 7-day grace period expires without successful payment.
180
+ */
181
+ async sendSuspensionNotice(params) {
182
+ const request = {
183
+ to: {
184
+ email: params.to,
185
+ userId: params.userId
186
+ },
187
+ category: "billing",
188
+ priority: "critical",
189
+ template: {
190
+ id: "product.system-alert",
191
+ props: {
192
+ alertType: "subscription_suspended",
193
+ title: "Your Subscription Has Been Suspended",
194
+ message: `Your ${params.planName} subscription has been suspended due to payment issues. Cloud features including snapshot sync, pattern library, and history are now paused.`,
195
+ severity: "error",
196
+ actionRequired: true,
197
+ actionUrl: params.reactivateUrl,
198
+ actionLabel: "Reactivate Now",
199
+ additionalDetails: "Update your payment method to restore full access. Your local CLI features remain available, but cloud syncing is disabled until payment is resolved.",
200
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
201
+ }
202
+ }
203
+ };
204
+ await this.emailService.send(request);
205
+ }
206
+ });
207
+
208
+ // ../../packages/integrations/dist/email/provider/resend.js
209
+ var from = process.env.MAIL_FROM || "noreply@snapback.dev";
210
+ var send = /* @__PURE__ */ __name(async ({ to, subject, html }) => {
211
+ logger.info("\u{1F4E7} Resend: Sending email", {
212
+ to,
213
+ subject,
214
+ from
215
+ });
216
+ const response = await fetch("https://api.resend.com/emails", {
217
+ method: "POST",
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ Authorization: `Bearer ${process.env.RESEND_API_KEY}`
221
+ },
222
+ body: JSON.stringify({
223
+ from,
224
+ to,
225
+ subject,
226
+ html
227
+ })
228
+ });
229
+ if (!response.ok) {
230
+ const errorData = await response.json();
231
+ logger.error("\u274C Resend: Email send failed", {
232
+ status: response.status,
233
+ statusText: response.statusText,
234
+ error: errorData,
235
+ to,
236
+ subject
237
+ });
238
+ throw new Error("Could not send email");
239
+ }
240
+ const data = await response.json();
241
+ logger.info("\u2705 Resend: Email sent successfully", {
242
+ to,
243
+ subject,
244
+ emailId: data.id
245
+ });
246
+ }, "send");
247
+ (class {
248
+ static {
249
+ __name(this, "EmailService");
250
+ }
251
+ middlewares = [];
252
+ templateRegistry;
253
+ providers = /* @__PURE__ */ new Map();
254
+ /**
255
+ * Add middleware to the processing chain
256
+ */
257
+ use(middleware) {
258
+ this.middlewares.push(middleware);
259
+ return this;
260
+ }
261
+ /**
262
+ * Primary send method with composable middleware
263
+ */
264
+ async send(request) {
265
+ if (request.dryRun) {
266
+ return this.handleDryRun(request);
267
+ }
268
+ const execute = this.middlewares.reduceRight((next, middleware) => () => middleware(request, next), () => this.executeDelivery(request));
269
+ return execute();
270
+ }
271
+ /**
272
+ * Template-based convenience method
273
+ */
274
+ async sendFromTemplate(options) {
275
+ const template = await this.templateRegistry?.get(options.templateId);
276
+ if (!template) {
277
+ return {
278
+ success: false,
279
+ error: `Template not found: ${options.templateId}`
280
+ };
281
+ }
282
+ const validation = template.schema.safeParse(options.context);
283
+ if (!validation.success) {
284
+ return {
285
+ success: false,
286
+ error: `Invalid template context: ${validation.error.message}`
287
+ };
288
+ }
289
+ const category = template.category;
290
+ const priority = this.getPriorityForCategory(category);
291
+ return this.send({
292
+ to: options.to,
293
+ provider: "auto",
294
+ category,
295
+ priority,
296
+ template: {
297
+ id: options.templateId,
298
+ props: validation.data
299
+ },
300
+ metadata: options.metadata
301
+ });
302
+ }
303
+ /**
304
+ * Batch sending for digests
305
+ */
306
+ async sendBatch(requests) {
307
+ const results = await Promise.allSettled(requests.map((request) => this.send(request)));
308
+ const succeeded = results.filter((r) => r.status === "fulfilled" && r.value.success).length;
309
+ return {
310
+ total: requests.length,
311
+ succeeded,
312
+ failed: requests.length - succeeded,
313
+ results: results.map((r) => r.status === "fulfilled" ? r.value : {
314
+ success: false,
315
+ error: "Promise rejected"
316
+ })
317
+ };
318
+ }
319
+ /**
320
+ * Execute email delivery (called after middleware chain)
321
+ */
322
+ async executeDelivery(request) {
323
+ try {
324
+ const template = await this.templateRegistry?.get(request.template.id);
325
+ if (!template) {
326
+ throw new Error(`Template not found: ${request.template.id}`);
327
+ }
328
+ const html = await render(template.component(request.template.props));
329
+ const text = await render(template.component(request.template.props), {
330
+ plainText: true
331
+ });
332
+ const provider = this.selectProvider(request);
333
+ const result = await provider.send({
334
+ from: "SnapBack <noreply@snapback.dev>",
335
+ to: request.to.email,
336
+ subject: template.subject(request.template.props),
337
+ html,
338
+ text,
339
+ metadata: request.metadata
340
+ });
341
+ await this.trackDelivery({
342
+ userId: request.to.userId,
343
+ templateId: request.template.id,
344
+ category: request.category,
345
+ provider: request.provider || "resend",
346
+ recipientEmail: request.to.email,
347
+ subject: template.subject(request.template.props),
348
+ status: result.status,
349
+ emailId: result.id,
350
+ metadata: request.metadata
351
+ });
352
+ return {
353
+ success: result.status === "sent",
354
+ emailId: result.id,
355
+ error: result.error
356
+ };
357
+ } catch (error) {
358
+ return {
359
+ success: false,
360
+ error: error instanceof Error ? error.message : "Unknown error"
361
+ };
362
+ }
363
+ }
364
+ /**
365
+ * Handle dry-run mode (no actual email sent)
366
+ */
367
+ async handleDryRun(request) {
368
+ console.log("\u{1F50D} DRY RUN MODE - Email not sent");
369
+ console.log(" Template:", request.template.id);
370
+ console.log(" To:", request.to.email);
371
+ console.log(" Category:", request.category);
372
+ console.log(" Priority:", request.priority);
373
+ return {
374
+ success: true,
375
+ emailId: `dry-run-${Date.now()}`
376
+ };
377
+ }
378
+ /**
379
+ * Select provider based on category or explicit request
380
+ */
381
+ selectProvider(request) {
382
+ if (request.provider && request.provider !== "auto") {
383
+ return this.providers.get(request.provider);
384
+ }
385
+ const provider = request.category === "marketing" ? "hubspot" : "resend";
386
+ return this.providers.get(provider);
387
+ }
388
+ /**
389
+ * Get default priority for email category
390
+ */
391
+ getPriorityForCategory(category) {
392
+ const priorityMap = {
393
+ authentication: "critical",
394
+ billing: "high",
395
+ onboarding: "high",
396
+ product: "normal",
397
+ marketing: "low"
398
+ };
399
+ return priorityMap[category] || "normal";
400
+ }
401
+ /**
402
+ * Track email delivery in database
403
+ */
404
+ async trackDelivery(_data) {
405
+ }
406
+ /**
407
+ * Register email provider
408
+ */
409
+ registerProvider(name, provider) {
410
+ this.providers.set(name, provider);
411
+ }
412
+ /**
413
+ * Set template registry
414
+ */
415
+ setTemplateRegistry(registry) {
416
+ this.templateRegistry = registry;
417
+ }
418
+ });
419
+ var statement = {
420
+ // Snapshot management permissions
421
+ snapshot: [
422
+ "create",
423
+ "read",
424
+ "update",
425
+ "delete",
426
+ "restore"
427
+ ],
428
+ // API key management permissions
429
+ apiKey: [
430
+ "create",
431
+ "read",
432
+ "revoke"
433
+ ],
434
+ // Member management permissions
435
+ member: [
436
+ "invite",
437
+ "remove",
438
+ "update"
439
+ ],
440
+ // Organization settings permissions
441
+ organization: [
442
+ "read",
443
+ "update",
444
+ "delete"
445
+ ],
446
+ // Billing and subscription permissions
447
+ billing: [
448
+ "read",
449
+ "update"
450
+ ],
451
+ // Analytics and reporting permissions
452
+ analytics: [
453
+ "read"
454
+ ]
455
+ };
456
+ var ac = createAccessControl(statement);
457
+ var member = ac.newRole({
458
+ snapshot: [
459
+ "create",
460
+ "read",
461
+ "restore"
462
+ ],
463
+ analytics: [
464
+ "read"
465
+ ]
466
+ });
467
+ var admin = ac.newRole({
468
+ snapshot: [
469
+ "create",
470
+ "read",
471
+ "update",
472
+ "delete",
473
+ "restore"
474
+ ],
475
+ apiKey: [
476
+ "create",
477
+ "read",
478
+ "revoke"
479
+ ],
480
+ member: [
481
+ "invite",
482
+ "update"
483
+ ],
484
+ organization: [
485
+ "read",
486
+ "update"
487
+ ],
488
+ billing: [
489
+ "read"
490
+ ],
491
+ analytics: [
492
+ "read"
493
+ ]
494
+ });
495
+ var owner = ac.newRole({
496
+ snapshot: [
497
+ "create",
498
+ "read",
499
+ "update",
500
+ "delete",
501
+ "restore"
502
+ ],
503
+ apiKey: [
504
+ "create",
505
+ "read",
506
+ "revoke"
507
+ ],
508
+ member: [
509
+ "invite",
510
+ "remove",
511
+ "update"
512
+ ],
513
+ organization: [
514
+ "read",
515
+ "update",
516
+ "delete"
517
+ ],
518
+ billing: [
519
+ "read",
520
+ "update"
521
+ ],
522
+ analytics: [
523
+ "read"
524
+ ]
525
+ });
526
+ var config2 = {
527
+ auth: {
528
+ enableSignup: process.env.ENABLE_SIGNUP !== "false"
529
+ }
530
+ };
531
+ var invitationOnlyPlugin = /* @__PURE__ */ __name(() => ({
532
+ id: "invitationOnlyPlugin",
533
+ hooks: {
534
+ before: [
535
+ {
536
+ matcher: /* @__PURE__ */ __name((context) => context.path?.startsWith("/sign-up/email") ?? false, "matcher"),
537
+ handler: createAuthMiddleware(async (ctx) => {
538
+ if (config2.auth.enableSignup) {
539
+ return;
540
+ }
541
+ const { email } = ctx.body;
542
+ const hasInvitation = await getPendingInvitationByEmail(email);
543
+ if (!hasInvitation) {
544
+ throw new APIError("BAD_REQUEST", {
545
+ code: "INVALID_INVITATION",
546
+ message: "No invitation found for this email"
547
+ });
548
+ }
549
+ })
550
+ }
551
+ ]
552
+ },
553
+ $ERROR_CODES: {
554
+ INVALID_INVITATION: "No invitation found for this email"
555
+ }
556
+ }), "invitationOnlyPlugin");
557
+ var { subscriptions, webhookEvents, user } = combinedSchema;
558
+ var stripeClient = null;
559
+ new EntitlementsServiceImpl();
560
+ ({
561
+ pro: process.env.STRIPE_PRO_MONTHLY_PRICE_ID,
562
+ team: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID,
563
+ enterprise: process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID
564
+ });
565
+ function getStripeClient() {
566
+ if (stripeClient) {
567
+ return stripeClient;
568
+ }
569
+ const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
570
+ if (!stripeSecretKey) {
571
+ throw new Error("Missing env variable STRIPE_SECRET_KEY");
572
+ }
573
+ stripeClient = new Stripe(stripeSecretKey);
574
+ return stripeClient;
575
+ }
576
+ __name(getStripeClient, "getStripeClient");
577
+ var setSubscriptionSeats = /* @__PURE__ */ __name(async (options) => {
578
+ const stripeClient2 = getStripeClient();
579
+ const subscription = await stripeClient2.subscriptions.retrieve(options.id);
580
+ if (!subscription) {
581
+ throw new Error("Subscription not found.");
582
+ }
583
+ await stripeClient2.subscriptions.update(options.id, {
584
+ items: [
585
+ {
586
+ id: subscription.items.data[0].id,
587
+ quantity: options.seats
588
+ }
589
+ ]
590
+ });
591
+ }, "setSubscriptionSeats");
592
+
593
+ // ../../packages/integrations/dist/stripe/lib/customer.js
594
+ createLogger({
595
+ name: "payments",
596
+ level: LogLevel.INFO
597
+ });
598
+
599
+ // ../../packages/integrations/dist/stripe/lib/helper.js
600
+ config.payments.plans;
601
+
602
+ // ../../packages/auth/dist/lib/organization.js
603
+ async function updateSeatsInOrganizationSubscription(organizationId) {
604
+ const organization2 = await getOrganizationWithPurchasesAndMembersCount(organizationId);
605
+ if (!organization2?.purchases || !Array.isArray(organization2.purchases) || organization2.purchases.length === 0) {
606
+ return;
607
+ }
608
+ const activeSubscription = organization2.purchases.find((purchase) => purchase.type === "SUBSCRIPTION");
609
+ if (!activeSubscription?.subscriptionId) {
610
+ return;
611
+ }
612
+ try {
613
+ await setSubscriptionSeats({
614
+ id: activeSubscription.subscriptionId,
615
+ seats: organization2.membersCount
616
+ });
617
+ } catch (error) {
618
+ logger.error("Could not update seats in organization subscription", {
619
+ organizationId,
620
+ error
621
+ });
622
+ }
623
+ }
624
+ __name(updateSeatsInOrganizationSubscription, "updateSeatsInOrganizationSubscription");
625
+
626
+ // ../../packages/auth/dist/auth.js
627
+ var appUrl = process.env.APP_URL || getBaseUrl();
628
+ var authBaseUrl = process.env.BETTER_AUTH_URL || process.env.BETTER_AUTH_BASE_URL || process.env.APP_URL || `http://localhost:${process.env.PORT || 3e3}`;
629
+ var isLocalDev = (process.env.BETTER_AUTH_URL || "").includes("localhost");
630
+ var isDevelopment = process.env.NODE_ENV !== "production" || isLocalDev;
631
+ var trustedOrigins = isDevelopment ? [
632
+ appUrl,
633
+ authBaseUrl,
634
+ "http://localhost:3000",
635
+ "http://localhost:3001",
636
+ "http://localhost:3002",
637
+ "http://localhost:3003"
638
+ ] : [
639
+ appUrl,
640
+ authBaseUrl
641
+ ].filter((url, index, arr) => arr.indexOf(url) === index);
642
+ var g = global;
643
+ if (!g.__SNAPBACK_AUTH_REDIS__) {
644
+ g.__SNAPBACK_AUTH_REDIS__ = {
645
+ client: null,
646
+ available: false,
647
+ reconnectAttempts: 0
648
+ };
649
+ }
650
+ function getRedisClient() {
651
+ return g.__SNAPBACK_AUTH_REDIS__.client;
652
+ }
653
+ __name(getRedisClient, "getRedisClient");
654
+ function isRedisAvailable() {
655
+ return g.__SNAPBACK_AUTH_REDIS__.available;
656
+ }
657
+ __name(isRedisAvailable, "isRedisAvailable");
658
+ var redisClient = g.__SNAPBACK_AUTH_REDIS__.client;
659
+ var redisAvailable = g.__SNAPBACK_AUTH_REDIS__.available;
660
+ var authRedisReconnectAttempts = g.__SNAPBACK_AUTH_REDIS__.reconnectAttempts;
661
+ async function initializeRedis() {
662
+ if (g.__SNAPBACK_AUTH_REDIS__.client) {
663
+ redisClient = g.__SNAPBACK_AUTH_REDIS__.client;
664
+ redisAvailable = g.__SNAPBACK_AUTH_REDIS__.available;
665
+ authRedisReconnectAttempts = g.__SNAPBACK_AUTH_REDIS__.reconnectAttempts;
666
+ return;
667
+ }
668
+ if (!process.env.REDIS_URL) {
669
+ if (process.env.MCP_QUIET !== "1") {
670
+ logger.warn("REDIS_URL not configured - rate limiting will use database fallback");
671
+ }
672
+ return;
673
+ }
674
+ try {
675
+ const redis = await import('redis').catch(() => null);
676
+ if (!redis) {
677
+ logger.warn("Redis module not available");
678
+ return;
679
+ }
680
+ const redisUrl = process.env.REDIS_URL;
681
+ if (!redisUrl.startsWith("redis://") && !redisUrl.startsWith("rediss://")) {
682
+ logger.error("Invalid REDIS_URL format", {
683
+ error: "REDIS_URL must start with 'redis://' or 'rediss://' protocol",
684
+ example: "redis://localhost:6379",
685
+ provided: `${redisUrl.split(":")[0]}:...`
686
+ });
687
+ g.__SNAPBACK_AUTH_REDIS__.available = false;
688
+ redisAvailable = false;
689
+ return;
690
+ }
691
+ const client = redis.createClient({
692
+ url: redisUrl,
693
+ socket: {
694
+ // Connection timeout - how long to wait for initial connection
695
+ connectTimeout: 1e4,
696
+ // TCP keepalive - prevents silent connection drops
697
+ keepAlive: 5e3,
698
+ // Reconnection strategy with exponential backoff + jitter
699
+ reconnectStrategy: /* @__PURE__ */ __name((retries, cause) => {
700
+ if (cause?.name === "SocketTimeoutError" || cause?.message?.includes("socket timeout")) {
701
+ logger.warn("Redis socket timeout for Better Auth - not reconnecting", {
702
+ cause: cause?.message
703
+ });
704
+ return false;
705
+ }
706
+ g.__SNAPBACK_AUTH_REDIS__.reconnectAttempts++;
707
+ authRedisReconnectAttempts++;
708
+ if (retries > 20) {
709
+ logger.error("Redis max retries exceeded", {
710
+ retries,
711
+ cause: cause?.message
712
+ });
713
+ return new Error("Max retries reached");
714
+ }
715
+ const baseDelay = Math.min(2 ** retries * 100, 3e4);
716
+ const jitter = Math.floor(Math.random() * 200);
717
+ const delay = baseDelay + jitter;
718
+ if (process.env.MCP_QUIET !== "1" && retries % 5 === 0) {
719
+ logger.debug("Redis reconnecting for Better Auth", {
720
+ retries,
721
+ delay
722
+ });
723
+ }
724
+ return delay;
725
+ }, "reconnectStrategy")
726
+ },
727
+ // Application-level ping to keep connection alive
728
+ pingInterval: 6e4
729
+ });
730
+ client.on("error", (err) => {
731
+ if (err.message.includes("ECONNRESET") || err.message.includes("ECONNREFUSED")) {
732
+ logger.debug("Redis connection error (will reconnect)", {
733
+ error: err.message
734
+ });
735
+ } else {
736
+ logger.warn("Redis connection error", {
737
+ error: err.message
738
+ });
739
+ }
740
+ g.__SNAPBACK_AUTH_REDIS__.available = false;
741
+ redisAvailable = false;
742
+ });
743
+ client.on("connect", () => {
744
+ if (process.env.MCP_QUIET !== "1" && !g.__SNAPBACK_REDIS_CONNECT_LOGGED__) {
745
+ g.__SNAPBACK_REDIS_CONNECT_LOGGED__ = true;
746
+ logger.info("Redis connected for Better Auth secondary storage");
747
+ }
748
+ g.__SNAPBACK_AUTH_REDIS__.available = true;
749
+ redisAvailable = true;
750
+ });
751
+ client.on("ready", () => {
752
+ g.__SNAPBACK_AUTH_REDIS__.reconnectAttempts = 0;
753
+ authRedisReconnectAttempts = 0;
754
+ g.__SNAPBACK_AUTH_REDIS__.available = true;
755
+ redisAvailable = true;
756
+ });
757
+ client.on("reconnecting", () => {
758
+ if (process.env.MCP_QUIET !== "1") {
759
+ logger.debug("Redis reconnecting for Better Auth...");
760
+ }
761
+ });
762
+ await client.connect();
763
+ g.__SNAPBACK_AUTH_REDIS__.client = client;
764
+ g.__SNAPBACK_AUTH_REDIS__.available = true;
765
+ redisClient = client;
766
+ redisAvailable = true;
767
+ if (process.env.MCP_QUIET !== "1" && !g.__SNAPBACK_REDIS_INIT_LOGGED__) {
768
+ g.__SNAPBACK_REDIS_INIT_LOGGED__ = true;
769
+ logger.info("\u2705 Better Auth Redis secondary storage initialized with production config");
770
+ }
771
+ } catch (error) {
772
+ logger.warn("Redis initialization failed - using database fallback", {
773
+ error: error instanceof Error ? error.message : String(error)
774
+ });
775
+ g.__SNAPBACK_AUTH_REDIS__.available = false;
776
+ redisAvailable = false;
777
+ }
778
+ }
779
+ __name(initializeRedis, "initializeRedis");
780
+ function getAuthRedisReconnectAttempts() {
781
+ return authRedisReconnectAttempts;
782
+ }
783
+ __name(getAuthRedisReconnectAttempts, "getAuthRedisReconnectAttempts");
784
+ initializeRedis().catch((err) => {
785
+ logger.error("Failed to initialize Redis for Better Auth", {
786
+ error: err instanceof Error ? err.message : String(err)
787
+ });
788
+ });
789
+ var _auth = betterAuth({
790
+ // ✅ Base URL for callbacks and redirects (required by Better Auth)
791
+ // Uses BETTER_AUTH_URL env var, falling back to localhost with PORT
792
+ baseURL: authBaseUrl,
793
+ // Extend the user schema with additional fields
794
+ schema: {
795
+ user: {
796
+ fields: {
797
+ onboardingComplete: {
798
+ type: "boolean",
799
+ required: false,
800
+ defaultValue: false
801
+ },
802
+ deviceFingerprint: {
803
+ type: "string",
804
+ required: false
805
+ }
806
+ }
807
+ }
808
+ },
809
+ appName: "SnapBack",
810
+ // ✅ SECURITY: Prevent account enumeration attacks
811
+ // OWASP ASVS 2.2.1 - Don't reveal whether username exists
812
+ disablePaths: [
813
+ "/is-username-available"
814
+ ],
815
+ endpoints: {
816
+ GET: {
817
+ "/health": {
818
+ async handler() {
819
+ return new Response("OK", {
820
+ status: 200
821
+ });
822
+ }
823
+ }
824
+ }
825
+ },
826
+ emailAndPassword: {
827
+ enabled: true
828
+ },
829
+ socialProviders: {
830
+ // Only include social providers if credentials are configured
831
+ // This prevents Better Auth from logging warnings that corrupt MCP stdio
832
+ // Note: Access process.env directly for Vercel compatibility (t3-env wrapper issue)
833
+ ...process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET ? {
834
+ github: {
835
+ clientId: process.env.GITHUB_CLIENT_ID,
836
+ clientSecret: process.env.GITHUB_CLIENT_SECRET
837
+ }
838
+ } : {},
839
+ ...process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET ? {
840
+ google: {
841
+ clientId: process.env.GOOGLE_CLIENT_ID,
842
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET
843
+ }
844
+ } : {}
845
+ },
846
+ database: drizzleAdapter(db, {
847
+ provider: "pg",
848
+ schema: combinedSchema
849
+ }),
850
+ session: {
851
+ expiresIn: 60 * 60 * 24 * 7,
852
+ updateAge: 60 * 60 * 24,
853
+ // ✅ CRITICAL FIX: Always store sessions in DB so findSession() falls back
854
+ // to the primary DB when Redis (secondaryStorage) is unavailable or misses.
855
+ // Without this, a Redis miss in serverless (Vercel cold start, connection lag)
856
+ // causes findSession to return null WITHOUT checking the DB, silently breaking
857
+ // bearer token validation (CLI device auth, auto-provision API key).
858
+ storeSessionInDatabase: true,
859
+ // ✅ OPTIMIZATION: Cookie cache for 80% database load reduction
860
+ // Note: When database/secondaryStorage is configured, we use cookieCache
861
+ // WITHOUT refreshCache (which is only for stateless/DB-less setups)
862
+ // The cache reduces DB queries by storing session data in signed cookies
863
+ cookieCache: {
864
+ enabled: true,
865
+ maxAge: 5 * 60,
866
+ strategy: "jwe"
867
+ }
868
+ },
869
+ account: {
870
+ accountLinking: {
871
+ enabled: true,
872
+ trustedProviders: [
873
+ "google",
874
+ "github"
875
+ ]
876
+ }
877
+ },
878
+ trustedOrigins,
879
+ // ✅ OPTIMIZATION: Redis secondary storage for distributed rate limiting
880
+ // Uses a lazy wrapper so Redis connects asynchronously without blocking
881
+ // betterAuth() initialization. Each call checks redisAvailable at runtime.
882
+ secondaryStorage: {
883
+ get: /* @__PURE__ */ __name(async (key) => {
884
+ if (!redisAvailable || !redisClient) return null;
885
+ try {
886
+ return await redisClient.get(key);
887
+ } catch (error) {
888
+ logger.error("Redis get failed", {
889
+ key,
890
+ error
891
+ });
892
+ return null;
893
+ }
894
+ }, "get"),
895
+ set: /* @__PURE__ */ __name(async (key, value, ttl) => {
896
+ if (!redisAvailable || !redisClient) return;
897
+ try {
898
+ if (ttl) {
899
+ await redisClient.set(key, value, {
900
+ EX: ttl
901
+ });
902
+ } else {
903
+ await redisClient.set(key, value);
904
+ }
905
+ } catch (error) {
906
+ logger.error("Redis set failed", {
907
+ key,
908
+ error
909
+ });
910
+ }
911
+ }, "set"),
912
+ delete: /* @__PURE__ */ __name(async (key) => {
913
+ if (!redisAvailable || !redisClient) return;
914
+ try {
915
+ await redisClient.del(key);
916
+ } catch (error) {
917
+ logger.error("Redis delete failed", {
918
+ key,
919
+ error
920
+ });
921
+ }
922
+ }, "delete")
923
+ },
924
+ advanced: {
925
+ // ✅ FIX: Use isDevelopment (not NODE_ENV) so Doppler prd config running locally
926
+ // still gets dev-friendly cookie settings. isDevelopment checks BETTER_AUTH_URL
927
+ // for localhost, which Doppler overrides correctly.
928
+ useSecureCookies: !isDevelopment,
929
+ crossSiteRequestForgery: {
930
+ enabled: true,
931
+ // Verify origin header matches trusted origins
932
+ checkOrigin: true
933
+ },
934
+ // ✅ OPTIMIZATION: Explicit ID generation using nanoid
935
+ database: {
936
+ generateId: /* @__PURE__ */ __name(() => nanoid(), "generateId"),
937
+ defaultFindManyLimit: 100,
938
+ experimentalJoins: false
939
+ },
940
+ // ✅ OPTIMIZATION: IP tracking configuration for security audit
941
+ ipAddress: {
942
+ ipAddressHeaders: [
943
+ "cf-connecting-ip",
944
+ "x-real-ip",
945
+ "x-forwarded-for",
946
+ "x-client-ip"
947
+ ],
948
+ disableIpTracking: false
949
+ },
950
+ // ✅ OPTIMIZATION: Enhanced cookie configuration
951
+ // ✅ FIX: Use isDevelopment consistently (not env.NODE_ENV === "production")
952
+ // Doppler prd sets NODE_ENV=production even when running locally, which would
953
+ // incorrectly set domain=".snapback.dev" on localhost cookies.
954
+ crossSubDomainCookies: {
955
+ enabled: !isDevelopment,
956
+ domain: !isDevelopment ? ".snapback.dev" : void 0
957
+ },
958
+ defaultCookieAttributes: {
959
+ sameSite: "lax",
960
+ secure: !isDevelopment,
961
+ httpOnly: true,
962
+ path: "/"
963
+ },
964
+ // ✅ FIX: OAuth state/pkce cookies need SameSite=None for cross-site redirects
965
+ // See: https://github.com/better-auth/better-auth/issues/6483
966
+ // Note: In development (localhost), browsers allow SameSite=None without Secure
967
+ cookies: {
968
+ state: {
969
+ name: "snapback.state",
970
+ attributes: {
971
+ sameSite: isDevelopment ? "lax" : "none",
972
+ secure: !isDevelopment,
973
+ httpOnly: true,
974
+ path: "/",
975
+ maxAge: 600
976
+ }
977
+ },
978
+ pkce: {
979
+ name: "snapback.pkce",
980
+ attributes: {
981
+ sameSite: isDevelopment ? "lax" : "none",
982
+ secure: !isDevelopment,
983
+ httpOnly: true,
984
+ path: "/",
985
+ maxAge: 600
986
+ }
987
+ }
988
+ },
989
+ cookiePrefix: "snapback"
990
+ },
991
+ // Rate limiting configuration (replaces 340+ lines of custom rate limit code)
992
+ rateLimit: {
993
+ window: 60,
994
+ max: 100,
995
+ // ✅ OPTIMIZATION: Use Redis for distributed rate limiting via secondaryStorage.
996
+ // secondaryStorage lazy-checks redisAvailable at runtime, so this is always safe.
997
+ storage: "secondary-storage",
998
+ customRules: {
999
+ // Strict limits for authentication endpoints
1000
+ "/sign-in/email": {
1001
+ window: 10,
1002
+ max: 3
1003
+ },
1004
+ "/sign-in/social": {
1005
+ window: 10,
1006
+ max: 5
1007
+ },
1008
+ "/sign-up": {
1009
+ window: 60,
1010
+ max: 5
1011
+ },
1012
+ "/password-reset": {
1013
+ window: 60,
1014
+ max: 3
1015
+ },
1016
+ // RFC 8628 Device Authorization - protect against brute-force of user codes
1017
+ "/device/*": {
1018
+ window: 60,
1019
+ max: isDevelopment ? 100 : 10
1020
+ },
1021
+ // Higher limits for normal API endpoints
1022
+ "/api/*": {
1023
+ window: 60,
1024
+ max: 500
1025
+ },
1026
+ // No rate limiting for health checks
1027
+ "/health": false,
1028
+ "/health/ready": false,
1029
+ "/health/live": false
1030
+ }
1031
+ },
1032
+ // Use database hooks for audit logging (replaces 371 lines of custom auth-audit.ts)
1033
+ // Also includes rate limiting configuration (replaces 340+ lines of custom rate limit code)
1034
+ databaseHooks: {
1035
+ session: {
1036
+ create: {
1037
+ after: /* @__PURE__ */ __name(async (session) => {
1038
+ await trackEvent("session.created", {
1039
+ userId: session.userId
1040
+ });
1041
+ await trackEvent("auth.signin", {
1042
+ userId: session.userId,
1043
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1044
+ });
1045
+ try {
1046
+ const { db: db2 } = await import('./dist-YZBJAYEJ.js');
1047
+ const { sql } = await import('drizzle-orm');
1048
+ if (db2) {
1049
+ const result = await db2.execute(sql`
1050
+ DELETE FROM session
1051
+ WHERE "userId" = ${session.userId}
1052
+ AND id != ${session.id}
1053
+ `);
1054
+ const rotatedCount = result.rowCount || 0;
1055
+ if (rotatedCount > 0) {
1056
+ logger.info("Session regenerated on login - old sessions invalidated", {
1057
+ userId: session.userId,
1058
+ sessionId: session.id,
1059
+ rotatedCount
1060
+ });
1061
+ await trackEvent("session.regenerated", {
1062
+ userId: session.userId,
1063
+ reason: "login",
1064
+ rotatedCount
1065
+ });
1066
+ }
1067
+ }
1068
+ } catch (error) {
1069
+ logger.warn("Session regeneration failed on login", {
1070
+ userId: session.userId,
1071
+ error: error instanceof Error ? error.message : String(error)
1072
+ });
1073
+ }
1074
+ try {
1075
+ if (!session.activeOrganizationId) {
1076
+ const { db: db2, combinedSchema: combinedSchema2 } = await import('./dist-YZBJAYEJ.js');
1077
+ const { sql } = await import('drizzle-orm');
1078
+ if (db2) {
1079
+ const { member: member2, session: sessionTable } = combinedSchema2;
1080
+ const membership = await db2.select({
1081
+ organizationId: member2.organizationId
1082
+ }).from(member2).where(sql`${member2.userId} = ${session.userId}`).orderBy(member2.createdAt).limit(1);
1083
+ if (membership.length > 0 && membership[0]?.organizationId) {
1084
+ await db2.update(sessionTable).set({
1085
+ activeOrganizationId: membership[0].organizationId
1086
+ }).where(sql`${sessionTable.id} = ${session.id}`);
1087
+ logger.info("Auto-set active organization for session", {
1088
+ userId: session.userId,
1089
+ sessionId: session.id,
1090
+ organizationId: membership[0].organizationId
1091
+ });
1092
+ }
1093
+ }
1094
+ }
1095
+ } catch (error) {
1096
+ logger.warn("Failed to auto-set active organization", {
1097
+ userId: session.userId,
1098
+ sessionId: session.id,
1099
+ error: error instanceof Error ? error.message : String(error)
1100
+ });
1101
+ }
1102
+ }, "after")
1103
+ },
1104
+ delete: {
1105
+ after: /* @__PURE__ */ __name(async (session) => {
1106
+ await trackEvent("session.revoked", {
1107
+ userId: session.userId
1108
+ });
1109
+ await trackEvent("auth.signout", {
1110
+ userId: session.userId,
1111
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1112
+ });
1113
+ }, "after")
1114
+ }
1115
+ },
1116
+ user: {
1117
+ create: {
1118
+ after: /* @__PURE__ */ __name(async (user2) => {
1119
+ await trackEvent("auth.signup", {
1120
+ userId: user2.id,
1121
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1122
+ });
1123
+ try {
1124
+ const { onOAuthSuccess } = await import('./pioneer-oauth-hook-V2JKEXM7.js');
1125
+ await onOAuthSuccess(user2);
1126
+ } catch (error) {
1127
+ logger.error("Pioneer OAuth hook failed", {
1128
+ userId: user2.id,
1129
+ error
1130
+ });
1131
+ }
1132
+ try {
1133
+ const { autoProvisionOrganization } = await import('./auto-provision-organization-SF6XM7X4.js');
1134
+ const result = await autoProvisionOrganization(user2);
1135
+ if (!result.success) {
1136
+ logger.warn("Auto-org provisioning failed", {
1137
+ userId: user2.id,
1138
+ error: result.error
1139
+ });
1140
+ }
1141
+ } catch (error) {
1142
+ logger.error("Auto-org provisioning error", {
1143
+ userId: user2.id,
1144
+ error
1145
+ });
1146
+ }
1147
+ }, "after")
1148
+ }
1149
+ }
1150
+ },
1151
+ plugins: [
1152
+ // ✅ CRITICAL: Bearer plugin enables getSession to validate Bearer tokens
1153
+ // Required for CLI device flow where access_token is passed via Authorization header
1154
+ // Without this, getSession only checks session cookies, not Bearer tokens
1155
+ bearer(),
1156
+ // ✅ ZERO-CONFIG: Anonymous plugin enables unauthenticated MCP usage (free tier)
1157
+ // Users get a real anonymous session on first install — no credentials required.
1158
+ // When user runs `snap login`, Better Auth links the anonymous account to the real
1159
+ // user via onLinkAccount, migrating any session history automatically.
1160
+ // Requires: isAnonymous boolean column on users table (see migration note below)
1161
+ anonymous({
1162
+ onLinkAccount: /* @__PURE__ */ __name(async ({ anonymousUser, newUser }) => {
1163
+ logger.info("[Auth] Anonymous account linked", {
1164
+ anonymousUserId: anonymousUser.user.id,
1165
+ newUserId: newUser.user.id
1166
+ });
1167
+ }, "onLinkAccount")
1168
+ }),
1169
+ // ✅ RFC 8628 Device Authorization Grant Flow (for WSL, Remote SSH, Codespaces)
1170
+ deviceAuthorization({
1171
+ // IMPORTANT: Must point to /link page on web app where users verify their device code
1172
+ // Better Auth uses baseURL to construct full URI, but baseURL might point to API
1173
+ // Users need to visit the web app to verify their device code
1174
+ verificationUri: `${appUrl}/link`
1175
+ }),
1176
+ admin$1(),
1177
+ apiKey({
1178
+ // ✅ CONSOLIDATED: Single source of truth for API key management
1179
+ // Replaces: KeysDb, apikeys-service, in-memory keys.ts
1180
+ // Key format: sk_live_[64 chars] or sk_test_[64 chars]
1181
+ defaultPrefix: "sk_live_",
1182
+ defaultKeyLength: 64,
1183
+ // Schema mapping - use our existing apiKeys table with snake_case columns
1184
+ schema: {
1185
+ apikey: {
1186
+ modelName: "apiKeys",
1187
+ fields: {
1188
+ // Map Better Auth field names to our Drizzle schema property names
1189
+ userId: "userId",
1190
+ name: "name",
1191
+ key: "key",
1192
+ start: "start",
1193
+ prefix: "prefix",
1194
+ createdAt: "createdAt",
1195
+ updatedAt: "updatedAt"
1196
+ }
1197
+ }
1198
+ },
1199
+ // Permissions model: { resource: [actions] }
1200
+ permissions: {
1201
+ defaultPermissions: {
1202
+ "snapback:analyze": [
1203
+ "read"
1204
+ ],
1205
+ "snapback:snapshot": [
1206
+ "read",
1207
+ "write"
1208
+ ],
1209
+ "snapback:context": [
1210
+ "read"
1211
+ ],
1212
+ // MCP server tool access - enables API key usage with MCP protocol
1213
+ mcp: [
1214
+ "tools"
1215
+ ],
1216
+ // API access levels
1217
+ api: [
1218
+ "read",
1219
+ "write"
1220
+ ],
1221
+ // CLI operations
1222
+ cli: [
1223
+ "snapshots"
1224
+ ]
1225
+ }
1226
+ },
1227
+ // Enable automatic session creation for API key-authenticated requests
1228
+ // This eliminates need for separate verifyApiKey + getSession calls
1229
+ enableSessionForAPIKeys: true,
1230
+ // Global rate limiting for all API keys
1231
+ rateLimit: {
1232
+ enabled: true,
1233
+ timeWindow: 864e5,
1234
+ maxRequests: 1e4
1235
+ },
1236
+ // Enable metadata storage for additional key info
1237
+ enableMetadata: true
1238
+ }),
1239
+ // ✅ LEVEL 4: JWT plugin with device-specific token issuance
1240
+ jwt({
1241
+ issuer: appUrl,
1242
+ audience: [
1243
+ "vscode",
1244
+ "mcp",
1245
+ "cli"
1246
+ ],
1247
+ expirationTime: 60 * 15
1248
+ }),
1249
+ magicLink({
1250
+ // SECURITY: Token expiration (default 5 min per Better Auth)
1251
+ // Links older than this are rejected
1252
+ expiresIn: 300,
1253
+ async sendMagicLink({ email, url }) {
1254
+ await send({
1255
+ to: email,
1256
+ subject: "Sign in to SnapBack",
1257
+ text: `Click the link to sign in: ${url}`,
1258
+ html: `<p>Click the link to sign in: <a href="${url}">${url}</a></p>`
1259
+ });
1260
+ }
1261
+ }),
1262
+ openAPI(),
1263
+ // ✅ OPTIMIZATION: Organization plugin with RBAC configuration
1264
+ // ✅ SECURITY: Session rotation on privilege escalation
1265
+ // Note: Call rotateSessionsOnOrgRoleChange() manually after updateMemberRole()
1266
+ // See: src/lib/session-rotation.ts
1267
+ organization({
1268
+ // Access control instance from organization-permissions.ts
1269
+ ac,
1270
+ // Define roles and permissions
1271
+ roles: {
1272
+ owner,
1273
+ admin,
1274
+ member
1275
+ },
1276
+ // Organization creation settings
1277
+ allowUserToCreateOrganization: true,
1278
+ organizationLimit: 5,
1279
+ // SECURITY: Invitation expiration (7 days per OWASP recommendation)
1280
+ // After this period, invitations must be re-sent
1281
+ invitationExpiresIn: 7 * 24 * 60 * 60,
1282
+ // Send invitation emails
1283
+ async sendInvitationEmail({ invitation, organization: organization2 }) {
1284
+ const { id, email, expiresAt } = invitation;
1285
+ const url = new URL(appUrl);
1286
+ url.pathname = "/accept-invitation";
1287
+ url.searchParams.set("invitationId", id);
1288
+ url.searchParams.set("email", email);
1289
+ const expiresInDays = Math.ceil((expiresAt.getTime() - Date.now()) / (1e3 * 60 * 60 * 24));
1290
+ await send({
1291
+ to: email,
1292
+ subject: `You've been invited to join ${organization2.name} on SnapBack`,
1293
+ text: `You've been invited to join ${organization2.name}. Click the link to accept: ${url.toString()}
1294
+
1295
+ This invitation expires in ${expiresInDays} days.`,
1296
+ html: `<p>You've been invited to join <strong>${organization2.name}</strong> on SnapBack.</p><p><a href="${url.toString()}">Click here to accept the invitation</a></p><p><small>This invitation expires in ${expiresInDays} days.</small></p>`
1297
+ });
1298
+ }
1299
+ }),
1300
+ invitationOnlyPlugin(),
1301
+ // ✅ SECURITY: Password breach detection via HaveIBeenPwned
1302
+ // OWASP A07:2021, NIST SP 800-63B Section 5.1.1.2
1303
+ // Automatically checks passwords during signup and password changes
1304
+ haveIBeenPwned(),
1305
+ // ✅ SECURITY: Two-Factor Authentication
1306
+ // NIST SP 800-63B compliant configuration
1307
+ twoFactor({
1308
+ // Application name shown in authenticator apps
1309
+ issuer: "SnapBack",
1310
+ // Require verification on enable for security
1311
+ skipVerificationOnEnable: false,
1312
+ // TOTP configuration
1313
+ totpOptions: {
1314
+ // 6-digit codes (standard)
1315
+ digits: 6,
1316
+ // 30-second validity period (TOTP standard)
1317
+ period: 30
1318
+ },
1319
+ // Backup codes configuration (enhanced when ENABLE_ENHANCED_2FA=true)
1320
+ backupCodeOptions: {
1321
+ // Number of backup codes to generate
1322
+ amount: ENABLE_ENHANCED_2FA ? 10 : 6,
1323
+ // Backup code length (longer for enterprise)
1324
+ length: ENABLE_ENHANCED_2FA ? 12 : 8
1325
+ }
1326
+ }),
1327
+ username({}),
1328
+ passkey({}),
1329
+ // =============================================================================
1330
+ // Enterprise Auth Plugins (Feature Flag Gated)
1331
+ // =============================================================================
1332
+ // ✅ ENTERPRISE: SSO Plugin (SAML 2.0 / OIDC)
1333
+ // Requires: ENABLE_SSO=true, @better-auth/sso package
1334
+ ...ENABLE_SSO ? [
1335
+ sso({
1336
+ // Auto-provision users to organization based on domain
1337
+ provisionUser: /* @__PURE__ */ __name(async ({ user: user2, userInfo }) => {
1338
+ const emailHash = user2.email ? `email_${Math.abs(user2.email.split("").reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0) | 0, 0)).toString(16).padStart(8, "0")}` : "none";
1339
+ logger.info("SSO user provisioning", {
1340
+ userId: user2.id,
1341
+ emailHash
1342
+ });
1343
+ if (userInfo?.email_verified && !user2.emailVerified) ;
1344
+ }, "provisionUser"),
1345
+ // Organization auto-provisioning settings
1346
+ organizationProvisioning: {
1347
+ defaultRole: "member"
1348
+ },
1349
+ // Allow new users via SSO (don't require pre-existing account)
1350
+ disableImplicitSignUp: false,
1351
+ // Trust IdP email verification status
1352
+ trustEmailVerified: true,
1353
+ // Domain verification for SSO providers
1354
+ domainVerification: {
1355
+ enabled: true
1356
+ }
1357
+ })
1358
+ ] : [],
1359
+ // ✅ SECURITY: Captcha Plugin (Bot Protection)
1360
+ // OWASP A07:2021 - Prevents automated attacks on auth endpoints
1361
+ // Requires: ENABLE_CAPTCHA=true, CAPTCHA_SECRET_KEY env var
1362
+ ...ENABLE_CAPTCHA && process.env.CAPTCHA_SECRET_KEY ? [
1363
+ captcha({
1364
+ provider: "cloudflare-turnstile",
1365
+ secretKey: process.env.CAPTCHA_SECRET_KEY,
1366
+ // Protect critical auth endpoints
1367
+ endpoints: [
1368
+ "/sign-up/email",
1369
+ "/sign-in/email",
1370
+ "/forget-password",
1371
+ "/password-reset"
1372
+ ]
1373
+ })
1374
+ ] : [],
1375
+ // ✅ ENTERPRISE: Multi-Session Plugin (Device Management)
1376
+ // Allows users to manage active sessions across devices
1377
+ // Requires: ENABLE_MULTI_SESSION=true
1378
+ ...ENABLE_MULTI_SESSION ? [
1379
+ multiSession({
1380
+ // Enterprise limit: 10 concurrent sessions
1381
+ maximumSessions: 10
1382
+ })
1383
+ ] : []
1384
+ ],
1385
+ onAPIError: {
1386
+ onError(error, ctx) {
1387
+ const errorDetails = {
1388
+ error,
1389
+ context: ctx
1390
+ };
1391
+ if (error && typeof error === "object") {
1392
+ if ("code" in error) {
1393
+ errorDetails.errorCode = error.code;
1394
+ }
1395
+ if ("message" in error) {
1396
+ errorDetails.errorMessage = error.message;
1397
+ }
1398
+ if ("statusCode" in error) {
1399
+ errorDetails.statusCode = error.statusCode;
1400
+ }
1401
+ }
1402
+ let isOAuthError = false;
1403
+ if (ctx && typeof ctx === "object") {
1404
+ if ("request" in ctx) {
1405
+ const request = ctx.request;
1406
+ errorDetails.requestUrl = request.url;
1407
+ errorDetails.requestMethod = request.method;
1408
+ if (request.url?.includes("/api/auth/callback/")) {
1409
+ const provider = request.url.split("/callback/")[1]?.split("?")[0];
1410
+ errorDetails.oauthProvider = provider;
1411
+ errorDetails.errorType = "OAuth Callback Error";
1412
+ isOAuthError = true;
1413
+ logger.error("OAuth Callback Error", errorDetails);
1414
+ }
1415
+ }
1416
+ }
1417
+ if (!isOAuthError) {
1418
+ logger.error("Better Auth API Error", errorDetails);
1419
+ }
1420
+ trackEvent("auth.signin_failed", {
1421
+ errorCode: errorDetails.errorCode,
1422
+ errorMessage: errorDetails.errorMessage,
1423
+ path: errorDetails.requestUrl,
1424
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1425
+ }).catch(() => {
1426
+ });
1427
+ import('./dist-E7E2T3DQ.js').then(({ captureError }) => {
1428
+ if (captureError && error instanceof Error) {
1429
+ captureError(error, {
1430
+ tags: {
1431
+ errorType: isOAuthError ? "oauth_callback" : "auth_api",
1432
+ ...errorDetails.errorCode ? {
1433
+ errorCode: String(errorDetails.errorCode)
1434
+ } : {}
1435
+ },
1436
+ extra: errorDetails
1437
+ });
1438
+ }
1439
+ }).catch(() => {
1440
+ });
1441
+ }
1442
+ }
1443
+ });
1444
+ var auth = _auth;
1445
+
1446
+ export { auth, getAuthRedisReconnectAttempts, getRedisClient, isRedisAvailable, redisAvailable, redisClient, updateSeatsInOrganizationSubscription };