@scalemule/nextjs 0.0.1

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.
@@ -0,0 +1,1083 @@
1
+ import { cookies } from 'next/headers';
2
+ import 'next/server';
3
+
4
+ // src/server/context.ts
5
+ function buildClientContextHeaders(context) {
6
+ const headers = {};
7
+ if (!context) {
8
+ return headers;
9
+ }
10
+ if (context.ip) {
11
+ headers["x-sm-forwarded-client-ip"] = context.ip;
12
+ headers["X-Client-IP"] = context.ip;
13
+ }
14
+ if (context.userAgent) {
15
+ headers["X-Client-User-Agent"] = context.userAgent;
16
+ }
17
+ if (context.deviceFingerprint) {
18
+ headers["X-Client-Device-Fingerprint"] = context.deviceFingerprint;
19
+ }
20
+ if (context.referrer) {
21
+ headers["X-Client-Referrer"] = context.referrer;
22
+ }
23
+ return headers;
24
+ }
25
+
26
+ // src/server/client.ts
27
+ var GATEWAY_URLS = {
28
+ dev: "https://api-dev.scalemule.com",
29
+ prod: "https://api.scalemule.com"
30
+ };
31
+ function resolveGatewayUrl(config) {
32
+ if (config.gatewayUrl) return config.gatewayUrl;
33
+ if (process.env.SCALEMULE_API_URL) return process.env.SCALEMULE_API_URL;
34
+ return GATEWAY_URLS[config.environment || "prod"];
35
+ }
36
+ var ScaleMuleServer = class {
37
+ constructor(config) {
38
+ // ==========================================================================
39
+ // Auth Methods
40
+ // ==========================================================================
41
+ this.auth = {
42
+ /**
43
+ * Register a new user
44
+ */
45
+ register: async (data) => {
46
+ return this.request("POST", "/v1/auth/register", { body: data });
47
+ },
48
+ /**
49
+ * Login user - returns session token (store in HTTP-only cookie)
50
+ */
51
+ login: async (data) => {
52
+ return this.request("POST", "/v1/auth/login", { body: data });
53
+ },
54
+ /**
55
+ * Logout user
56
+ */
57
+ logout: async (sessionToken) => {
58
+ return this.request("POST", "/v1/auth/logout", {
59
+ body: { session_token: sessionToken }
60
+ });
61
+ },
62
+ /**
63
+ * Get current user from session token
64
+ */
65
+ me: async (sessionToken) => {
66
+ return this.request("GET", "/v1/auth/me", { sessionToken });
67
+ },
68
+ /**
69
+ * Refresh session token
70
+ */
71
+ refresh: async (sessionToken) => {
72
+ return this.request("POST", "/v1/auth/refresh", {
73
+ body: { session_token: sessionToken }
74
+ });
75
+ },
76
+ /**
77
+ * Request password reset email
78
+ */
79
+ forgotPassword: async (email) => {
80
+ return this.request("POST", "/v1/auth/forgot-password", { body: { email } });
81
+ },
82
+ /**
83
+ * Reset password with token
84
+ */
85
+ resetPassword: async (token, newPassword) => {
86
+ return this.request("POST", "/v1/auth/reset-password", {
87
+ body: { token, new_password: newPassword }
88
+ });
89
+ },
90
+ /**
91
+ * Verify email with token
92
+ */
93
+ verifyEmail: async (token) => {
94
+ return this.request("POST", "/v1/auth/verify-email", { body: { token } });
95
+ },
96
+ /**
97
+ * Resend verification email.
98
+ * Can be called with a session token (authenticated) or email (unauthenticated).
99
+ */
100
+ resendVerification: async (sessionTokenOrEmail, options) => {
101
+ if (options?.email) {
102
+ return this.request("POST", "/v1/auth/resend-verification", {
103
+ sessionToken: sessionTokenOrEmail,
104
+ body: { email: options.email }
105
+ });
106
+ }
107
+ if (sessionTokenOrEmail.includes("@")) {
108
+ return this.request("POST", "/v1/auth/resend-verification", {
109
+ body: { email: sessionTokenOrEmail }
110
+ });
111
+ }
112
+ return this.request("POST", "/v1/auth/resend-verification", {
113
+ sessionToken: sessionTokenOrEmail
114
+ });
115
+ }
116
+ };
117
+ // ==========================================================================
118
+ // User/Profile Methods
119
+ // ==========================================================================
120
+ this.user = {
121
+ /**
122
+ * Update user profile
123
+ */
124
+ update: async (sessionToken, data) => {
125
+ return this.request("PATCH", "/v1/auth/profile", {
126
+ sessionToken,
127
+ body: data
128
+ });
129
+ },
130
+ /**
131
+ * Change password
132
+ */
133
+ changePassword: async (sessionToken, currentPassword, newPassword) => {
134
+ return this.request("POST", "/v1/auth/change-password", {
135
+ sessionToken,
136
+ body: { current_password: currentPassword, new_password: newPassword }
137
+ });
138
+ },
139
+ /**
140
+ * Change email
141
+ */
142
+ changeEmail: async (sessionToken, newEmail, password) => {
143
+ return this.request("POST", "/v1/auth/change-email", {
144
+ sessionToken,
145
+ body: { new_email: newEmail, password }
146
+ });
147
+ },
148
+ /**
149
+ * Delete account
150
+ */
151
+ deleteAccount: async (sessionToken, password) => {
152
+ return this.request("DELETE", "/v1/auth/me", {
153
+ sessionToken,
154
+ body: { password }
155
+ });
156
+ }
157
+ };
158
+ // ==========================================================================
159
+ // Storage/Content Methods
160
+ // ==========================================================================
161
+ // ==========================================================================
162
+ // Secrets Methods (Tenant Vault)
163
+ // ==========================================================================
164
+ this.secrets = {
165
+ /**
166
+ * Get a secret from the tenant vault
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * const result = await scalemule.secrets.get('ANONYMOUS_USER_SALT')
171
+ * if (result.success) {
172
+ * console.log('Salt:', result.data.value)
173
+ * }
174
+ * ```
175
+ */
176
+ get: async (key) => {
177
+ return this.request("GET", `/v1/vault/secrets/${encodeURIComponent(key)}`);
178
+ },
179
+ /**
180
+ * Set a secret in the tenant vault
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * await scalemule.secrets.set('ANONYMOUS_USER_SALT', 'my-secret-salt')
185
+ * ```
186
+ */
187
+ set: async (key, value) => {
188
+ return this.request("PUT", `/v1/vault/secrets/${encodeURIComponent(key)}`, {
189
+ body: { value }
190
+ });
191
+ },
192
+ /**
193
+ * Delete a secret from the tenant vault
194
+ */
195
+ delete: async (key) => {
196
+ return this.request("DELETE", `/v1/vault/secrets/${encodeURIComponent(key)}`);
197
+ },
198
+ /**
199
+ * List all secrets in the tenant vault
200
+ */
201
+ list: async () => {
202
+ return this.request("GET", "/v1/vault/secrets");
203
+ },
204
+ /**
205
+ * Get secret version history
206
+ */
207
+ versions: async (key) => {
208
+ return this.request(
209
+ "GET",
210
+ `/v1/vault/versions/${encodeURIComponent(key)}`
211
+ );
212
+ },
213
+ /**
214
+ * Rollback to a specific version
215
+ */
216
+ rollback: async (key, version) => {
217
+ return this.request(
218
+ "POST",
219
+ `/v1/vault/actions/rollback/${encodeURIComponent(key)}`,
220
+ { body: { version } }
221
+ );
222
+ },
223
+ /**
224
+ * Rotate a secret (copy current version as new version)
225
+ */
226
+ rotate: async (key, newValue) => {
227
+ return this.request(
228
+ "POST",
229
+ `/v1/vault/actions/rotate/${encodeURIComponent(key)}`,
230
+ { body: { value: newValue } }
231
+ );
232
+ }
233
+ };
234
+ // ==========================================================================
235
+ // Bundle Methods (Structured Secrets with Inheritance)
236
+ // ==========================================================================
237
+ this.bundles = {
238
+ /**
239
+ * Get a bundle (structured secret like database credentials)
240
+ *
241
+ * @param key - Bundle key (e.g., 'database/prod')
242
+ * @param resolve - Whether to resolve inheritance (default: true)
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * const result = await scalemule.bundles.get('database/prod')
247
+ * if (result.success) {
248
+ * console.log('DB Host:', result.data.data.host)
249
+ * }
250
+ * ```
251
+ */
252
+ get: async (key, resolve = true) => {
253
+ const params = new URLSearchParams({ resolve: resolve.toString() });
254
+ return this.request(
255
+ "GET",
256
+ `/v1/vault/bundles/${encodeURIComponent(key)}?${params}`
257
+ );
258
+ },
259
+ /**
260
+ * Set a bundle (structured secret)
261
+ *
262
+ * @param key - Bundle key
263
+ * @param type - Bundle type: 'mysql', 'postgres', 'redis', 's3', 'oauth', 'smtp', 'generic'
264
+ * @param data - Bundle data (structure depends on type)
265
+ * @param inheritsFrom - Optional parent bundle key for inheritance
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * // Create a MySQL bundle
270
+ * await scalemule.bundles.set('database/prod', 'mysql', {
271
+ * host: 'db.example.com',
272
+ * port: 3306,
273
+ * username: 'app',
274
+ * password: 'secret',
275
+ * database: 'myapp'
276
+ * })
277
+ *
278
+ * // Create a bundle that inherits from another
279
+ * await scalemule.bundles.set('database/staging', 'mysql', {
280
+ * host: 'staging-db.example.com', // Override just the host
281
+ * }, 'database/prod')
282
+ * ```
283
+ */
284
+ set: async (key, type, data, inheritsFrom) => {
285
+ return this.request(
286
+ "PUT",
287
+ `/v1/vault/bundles/${encodeURIComponent(key)}`,
288
+ {
289
+ body: {
290
+ type,
291
+ value: data,
292
+ inherits_from: inheritsFrom
293
+ }
294
+ }
295
+ );
296
+ },
297
+ /**
298
+ * Delete a bundle
299
+ */
300
+ delete: async (key) => {
301
+ return this.request("DELETE", `/v1/vault/bundles/${encodeURIComponent(key)}`);
302
+ },
303
+ /**
304
+ * List all bundles
305
+ */
306
+ list: async () => {
307
+ return this.request(
308
+ "GET",
309
+ "/v1/vault/bundles"
310
+ );
311
+ },
312
+ /**
313
+ * Get connection URL for a database bundle
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * const result = await scalemule.bundles.connectionUrl('database/prod')
318
+ * if (result.success) {
319
+ * const client = mysql.createConnection(result.data.url)
320
+ * }
321
+ * ```
322
+ */
323
+ connectionUrl: async (key) => {
324
+ return this.request(
325
+ "GET",
326
+ `/v1/vault/bundles/${encodeURIComponent(key)}?connection_url=true`
327
+ );
328
+ }
329
+ };
330
+ // ==========================================================================
331
+ // Vault Audit Methods
332
+ // ==========================================================================
333
+ this.vaultAudit = {
334
+ /**
335
+ * Query audit logs for your tenant's vault operations
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * const result = await scalemule.vaultAudit.query({
340
+ * action: 'read',
341
+ * path: 'database/*',
342
+ * since: '2026-01-01'
343
+ * })
344
+ * ```
345
+ */
346
+ query: async (options) => {
347
+ const params = new URLSearchParams();
348
+ if (options?.action) params.set("action", options.action);
349
+ if (options?.path) params.set("path", options.path);
350
+ if (options?.since) params.set("since", options.since);
351
+ if (options?.until) params.set("until", options.until);
352
+ if (options?.limit) params.set("limit", options.limit.toString());
353
+ const queryStr = params.toString();
354
+ return this.request("GET", `/v1/vault/audit${queryStr ? `?${queryStr}` : ""}`);
355
+ }
356
+ };
357
+ this.storage = {
358
+ /**
359
+ * List user's files
360
+ */
361
+ list: async (userId, params) => {
362
+ const query = new URLSearchParams();
363
+ if (params?.content_type) query.set("content_type", params.content_type);
364
+ if (params?.search) query.set("search", params.search);
365
+ if (params?.limit) query.set("limit", params.limit.toString());
366
+ if (params?.offset) query.set("offset", params.offset.toString());
367
+ const queryStr = query.toString();
368
+ const path = `/v1/storage/my-files${queryStr ? `?${queryStr}` : ""}`;
369
+ return this.request("GET", path, { userId });
370
+ },
371
+ /**
372
+ * Get file info
373
+ */
374
+ get: async (fileId) => {
375
+ return this.request("GET", `/v1/storage/files/${fileId}/info`);
376
+ },
377
+ /**
378
+ * Delete file
379
+ */
380
+ delete: async (userId, fileId) => {
381
+ return this.request("DELETE", `/v1/storage/files/${fileId}`, { userId });
382
+ },
383
+ /**
384
+ * Upload file (from server - use FormData)
385
+ *
386
+ * @param userId - The user ID who owns this file
387
+ * @param file - File data to upload
388
+ * @param options - Upload options
389
+ * @param options.clientContext - End user context to forward (IP, user agent, etc.)
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * // Forward end user context for proper attribution
394
+ * const result = await scalemule.storage.upload(
395
+ * userId,
396
+ * { buffer, filename, contentType },
397
+ * { clientContext: extractClientContext(request) }
398
+ * )
399
+ * ```
400
+ */
401
+ upload: async (userId, file, options) => {
402
+ const formData = new FormData();
403
+ const blob = new Blob([file.buffer], { type: file.contentType });
404
+ formData.append("file", blob, file.filename);
405
+ formData.append("sm_user_id", userId);
406
+ const url = `${this.gatewayUrl}/v1/storage/upload`;
407
+ const headers = {
408
+ "x-api-key": this.apiKey,
409
+ "x-user-id": userId,
410
+ ...buildClientContextHeaders(options?.clientContext)
411
+ };
412
+ if (this.debug && options?.clientContext) {
413
+ console.log(`[ScaleMule Server] Upload with client context: IP=${options.clientContext.ip}`);
414
+ }
415
+ try {
416
+ const response = await fetch(url, {
417
+ method: "POST",
418
+ headers,
419
+ body: formData
420
+ });
421
+ const data = await response.json();
422
+ if (!response.ok) {
423
+ return {
424
+ success: false,
425
+ error: data.error || { code: "UPLOAD_FAILED", message: "Upload failed" }
426
+ };
427
+ }
428
+ return data;
429
+ } catch (err) {
430
+ return {
431
+ success: false,
432
+ error: {
433
+ code: "UPLOAD_ERROR",
434
+ message: err instanceof Error ? err.message : "Upload failed"
435
+ }
436
+ };
437
+ }
438
+ }
439
+ };
440
+ // ==========================================================================
441
+ // Analytics Methods
442
+ // ==========================================================================
443
+ // ==========================================================================
444
+ // Webhooks Methods
445
+ // ==========================================================================
446
+ this.webhooks = {
447
+ /**
448
+ * Create a new webhook subscription
449
+ *
450
+ * @example
451
+ * ```typescript
452
+ * const result = await scalemule.webhooks.create({
453
+ * webhook_name: 'Video Status Webhook',
454
+ * url: 'https://myapp.com/api/webhooks/scalemule',
455
+ * events: ['video.ready', 'video.failed']
456
+ * })
457
+ *
458
+ * // Store the secret for signature verification
459
+ * console.log('Webhook secret:', result.data.secret)
460
+ * ```
461
+ */
462
+ create: async (data) => {
463
+ return this.request(
464
+ "POST",
465
+ "/v1/webhooks",
466
+ { body: data }
467
+ );
468
+ },
469
+ /**
470
+ * List all webhook subscriptions
471
+ */
472
+ list: async () => {
473
+ return this.request("GET", "/v1/webhooks");
474
+ },
475
+ /**
476
+ * Delete a webhook subscription
477
+ */
478
+ delete: async (id) => {
479
+ return this.request("DELETE", `/v1/webhooks/${id}`);
480
+ },
481
+ /**
482
+ * Update a webhook subscription
483
+ */
484
+ update: async (id, data) => {
485
+ return this.request(
486
+ "PATCH",
487
+ `/v1/webhooks/${id}`,
488
+ { body: data }
489
+ );
490
+ },
491
+ /**
492
+ * Get available webhook event types
493
+ */
494
+ eventTypes: async () => {
495
+ return this.request("GET", "/v1/webhooks/events");
496
+ }
497
+ };
498
+ // ==========================================================================
499
+ // Analytics Methods
500
+ // ==========================================================================
501
+ this.analytics = {
502
+ /**
503
+ * Track an analytics event
504
+ *
505
+ * IMPORTANT: When calling from server-side code (API routes), always pass
506
+ * clientContext to ensure the real end user's IP is recorded, not the server's IP.
507
+ *
508
+ * @example
509
+ * ```typescript
510
+ * // In an API route
511
+ * import { extractClientContext, createServerClient } from '@scalemule/nextjs/server'
512
+ *
513
+ * export async function POST(request: NextRequest) {
514
+ * const clientContext = extractClientContext(request)
515
+ * const scalemule = createServerClient()
516
+ *
517
+ * await scalemule.analytics.trackEvent({
518
+ * event_name: 'button_clicked',
519
+ * properties: { button_id: 'signup' }
520
+ * }, { clientContext })
521
+ * }
522
+ * ```
523
+ */
524
+ trackEvent: async (event, options) => {
525
+ return this.request("POST", "/v1/analytics/v2/events", {
526
+ body: event,
527
+ clientContext: options?.clientContext
528
+ });
529
+ },
530
+ /**
531
+ * Track a page view
532
+ *
533
+ * @example
534
+ * ```typescript
535
+ * await scalemule.analytics.trackPageView({
536
+ * page_url: 'https://example.com/products',
537
+ * page_title: 'Products',
538
+ * referrer: 'https://google.com'
539
+ * }, { clientContext })
540
+ * ```
541
+ */
542
+ trackPageView: async (data, options) => {
543
+ return this.request("POST", "/v1/analytics/v2/events", {
544
+ body: {
545
+ event_name: "page_viewed",
546
+ event_category: "navigation",
547
+ page_url: data.page_url,
548
+ properties: {
549
+ page_title: data.page_title,
550
+ referrer: data.referrer
551
+ },
552
+ session_id: data.session_id,
553
+ user_id: data.user_id
554
+ },
555
+ clientContext: options?.clientContext
556
+ });
557
+ },
558
+ /**
559
+ * Track multiple events in a batch (max 100)
560
+ *
561
+ * @example
562
+ * ```typescript
563
+ * await scalemule.analytics.trackBatch([
564
+ * { event_name: 'item_viewed', properties: { item_id: '123' } },
565
+ * { event_name: 'item_added_to_cart', properties: { item_id: '123' } }
566
+ * ], { clientContext })
567
+ * ```
568
+ */
569
+ trackBatch: async (events, options) => {
570
+ return this.request("POST", "/v1/analytics/v2/events/batch", {
571
+ body: { events },
572
+ clientContext: options?.clientContext
573
+ });
574
+ }
575
+ };
576
+ this.apiKey = config.apiKey;
577
+ this.gatewayUrl = resolveGatewayUrl(config);
578
+ this.debug = config.debug || false;
579
+ }
580
+ /**
581
+ * Make a request to the ScaleMule API
582
+ *
583
+ * @param method - HTTP method
584
+ * @param path - API path (e.g., /v1/auth/login)
585
+ * @param options - Request options
586
+ * @param options.body - Request body (will be JSON stringified)
587
+ * @param options.userId - User ID (passed through for storage operations)
588
+ * @param options.sessionToken - Session token sent as Authorization: Bearer header
589
+ * @param options.clientContext - End user context to forward (IP, user agent, etc.)
590
+ */
591
+ async request(method, path, options = {}) {
592
+ const url = `${this.gatewayUrl}${path}`;
593
+ const headers = {
594
+ "x-api-key": this.apiKey,
595
+ "Content-Type": "application/json",
596
+ // Forward client context headers if provided
597
+ ...buildClientContextHeaders(options.clientContext)
598
+ };
599
+ if (options.sessionToken) {
600
+ headers["Authorization"] = `Bearer ${options.sessionToken}`;
601
+ }
602
+ if (this.debug) {
603
+ console.log(`[ScaleMule Server] ${method} ${path}`);
604
+ if (options.clientContext) {
605
+ console.log(`[ScaleMule Server] Client context: IP=${options.clientContext.ip}, UA=${options.clientContext.userAgent?.substring(0, 50)}...`);
606
+ }
607
+ }
608
+ try {
609
+ const response = await fetch(url, {
610
+ method,
611
+ headers,
612
+ body: options.body ? JSON.stringify(options.body) : void 0
613
+ });
614
+ const data = await response.json();
615
+ if (!response.ok) {
616
+ const error = data.error || {
617
+ code: `HTTP_${response.status}`,
618
+ message: data.message || response.statusText
619
+ };
620
+ return { success: false, error };
621
+ }
622
+ return data;
623
+ } catch (err) {
624
+ return {
625
+ success: false,
626
+ error: {
627
+ code: "SERVER_ERROR",
628
+ message: err instanceof Error ? err.message : "Request failed"
629
+ }
630
+ };
631
+ }
632
+ }
633
+ };
634
+ function createServerClient(config) {
635
+ const apiKey = config?.apiKey || process.env.SCALEMULE_API_KEY;
636
+ if (!apiKey) {
637
+ throw new Error(
638
+ "ScaleMule API key is required. Set SCALEMULE_API_KEY environment variable or pass apiKey in config."
639
+ );
640
+ }
641
+ const environment = config?.environment || process.env.SCALEMULE_ENV || "prod";
642
+ return new ScaleMuleServer({
643
+ apiKey,
644
+ environment,
645
+ gatewayUrl: config?.gatewayUrl,
646
+ debug: config?.debug || process.env.SCALEMULE_DEBUG === "true"
647
+ });
648
+ }
649
+ var SESSION_COOKIE_NAME = "sm_session";
650
+ var USER_ID_COOKIE_NAME = "sm_user_id";
651
+ ({
652
+ secure: process.env.NODE_ENV === "production"});
653
+ function createCookieHeader(name, value, options = {}) {
654
+ const maxAge = options.maxAge ?? 7 * 24 * 60 * 60;
655
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
656
+ const sameSite = options.sameSite ?? "lax";
657
+ const path = options.path ?? "/";
658
+ let cookie = `${name}=${encodeURIComponent(value)}; Path=${path}; Max-Age=${maxAge}; HttpOnly; SameSite=${sameSite}`;
659
+ if (secure) {
660
+ cookie += "; Secure";
661
+ }
662
+ if (options.domain) {
663
+ cookie += `; Domain=${options.domain}`;
664
+ }
665
+ return cookie;
666
+ }
667
+ function createClearCookieHeader(name, options = {}) {
668
+ const path = options.path ?? "/";
669
+ let cookie = `${name}=; Path=${path}; Max-Age=0; HttpOnly`;
670
+ if (options.domain) {
671
+ cookie += `; Domain=${options.domain}`;
672
+ }
673
+ return cookie;
674
+ }
675
+ function withSession(loginResponse, responseBody, options = {}) {
676
+ const headers = new Headers();
677
+ headers.set("Content-Type", "application/json");
678
+ headers.append(
679
+ "Set-Cookie",
680
+ createCookieHeader(SESSION_COOKIE_NAME, loginResponse.session_token, options)
681
+ );
682
+ headers.append(
683
+ "Set-Cookie",
684
+ createCookieHeader(USER_ID_COOKIE_NAME, loginResponse.user.id, options)
685
+ );
686
+ return new Response(JSON.stringify({ success: true, data: responseBody }), {
687
+ status: 200,
688
+ headers
689
+ });
690
+ }
691
+ function withRefreshedSession(sessionToken, userId, responseBody, options = {}) {
692
+ const headers = new Headers();
693
+ headers.set("Content-Type", "application/json");
694
+ headers.append(
695
+ "Set-Cookie",
696
+ createCookieHeader(SESSION_COOKIE_NAME, sessionToken, options)
697
+ );
698
+ headers.append(
699
+ "Set-Cookie",
700
+ createCookieHeader(USER_ID_COOKIE_NAME, userId, options)
701
+ );
702
+ return new Response(JSON.stringify({ success: true, data: responseBody }), {
703
+ status: 200,
704
+ headers
705
+ });
706
+ }
707
+ function clearSession(responseBody, options = {}, status = 200) {
708
+ const headers = new Headers();
709
+ headers.set("Content-Type", "application/json");
710
+ headers.append("Set-Cookie", createClearCookieHeader(SESSION_COOKIE_NAME, options));
711
+ headers.append("Set-Cookie", createClearCookieHeader(USER_ID_COOKIE_NAME, options));
712
+ return new Response(JSON.stringify({ success: status < 300, data: responseBody }), {
713
+ status,
714
+ headers
715
+ });
716
+ }
717
+ async function getSession() {
718
+ const cookieStore = await cookies();
719
+ const sessionCookie = cookieStore.get(SESSION_COOKIE_NAME);
720
+ const userIdCookie = cookieStore.get(USER_ID_COOKIE_NAME);
721
+ if (!sessionCookie?.value || !userIdCookie?.value) {
722
+ return null;
723
+ }
724
+ return {
725
+ sessionToken: sessionCookie.value,
726
+ userId: userIdCookie.value,
727
+ expiresAt: /* @__PURE__ */ new Date()
728
+ // Note: actual expiry is managed by ScaleMule backend
729
+ };
730
+ }
731
+
732
+ // src/server/timing.ts
733
+ function constantTimeEqual(a, b) {
734
+ const maxLength = Math.max(a.length, b.length);
735
+ let mismatch = a.length ^ b.length;
736
+ for (let i = 0; i < maxLength; i++) {
737
+ const aCode = i < a.length ? a.charCodeAt(i) : 0;
738
+ const bCode = i < b.length ? b.charCodeAt(i) : 0;
739
+ mismatch |= aCode ^ bCode;
740
+ }
741
+ return mismatch === 0;
742
+ }
743
+
744
+ // src/server/csrf.ts
745
+ var CSRF_COOKIE_NAME = "sm_csrf";
746
+ var CSRF_HEADER_NAME = "x-csrf-token";
747
+ function validateCSRFToken(request) {
748
+ const cookieToken = request.cookies.get(CSRF_COOKIE_NAME)?.value;
749
+ if (!cookieToken) {
750
+ return "Missing CSRF cookie";
751
+ }
752
+ const headerToken = request.headers.get(CSRF_HEADER_NAME);
753
+ if (!headerToken) {
754
+ return "Missing CSRF token header";
755
+ }
756
+ if (!constantTimeEqual(cookieToken, headerToken)) {
757
+ return "CSRF token mismatch";
758
+ }
759
+ return void 0;
760
+ }
761
+
762
+ // src/server/routes.ts
763
+ function errorResponse(code, message, status) {
764
+ return new Response(
765
+ JSON.stringify({ success: false, error: { code, message } }),
766
+ { status, headers: { "Content-Type": "application/json" } }
767
+ );
768
+ }
769
+ function successResponse(data, status = 200) {
770
+ return new Response(
771
+ JSON.stringify({ success: true, data }),
772
+ { status, headers: { "Content-Type": "application/json" } }
773
+ );
774
+ }
775
+ function createAuthRoutes(config = {}) {
776
+ const sm = createServerClient(config.client);
777
+ const cookieOptions = config.cookies || {};
778
+ const POST2 = async (request, context) => {
779
+ if (config.csrf) {
780
+ const csrfError = validateCSRFToken(request);
781
+ if (csrfError) {
782
+ return errorResponse("CSRF_ERROR", "CSRF validation failed", 403);
783
+ }
784
+ }
785
+ const params = await context?.params;
786
+ const path = params?.scalemule?.join("/") || "";
787
+ try {
788
+ const body = await request.json().catch(() => ({}));
789
+ switch (path) {
790
+ // ==================== Register ====================
791
+ case "register": {
792
+ const { email, password, full_name, username, phone } = body;
793
+ if (!email || !password) {
794
+ return errorResponse("VALIDATION_ERROR", "Email and password required", 400);
795
+ }
796
+ const result = await sm.auth.register({ email, password, full_name, username, phone });
797
+ if (!result.success) {
798
+ return errorResponse(
799
+ result.error?.code || "REGISTER_FAILED",
800
+ result.error?.message || "Registration failed",
801
+ 400
802
+ );
803
+ }
804
+ if (config.onRegister && result.data) {
805
+ await config.onRegister({ id: result.data.id, email: result.data.email });
806
+ }
807
+ return successResponse({ user: result.data, message: "Registration successful" }, 201);
808
+ }
809
+ // ==================== Login ====================
810
+ case "login": {
811
+ const { email, password, remember_me } = body;
812
+ if (!email || !password) {
813
+ return errorResponse("VALIDATION_ERROR", "Email and password required", 400);
814
+ }
815
+ const result = await sm.auth.login({ email, password, remember_me });
816
+ if (!result.success || !result.data) {
817
+ const errorCode = result.error?.code || "LOGIN_FAILED";
818
+ let status = 400;
819
+ if (errorCode === "INVALID_CREDENTIALS" || errorCode === "UNAUTHORIZED") status = 401;
820
+ if (["EMAIL_NOT_VERIFIED", "PHONE_NOT_VERIFIED", "ACCOUNT_LOCKED", "ACCOUNT_DISABLED", "MFA_REQUIRED"].includes(errorCode)) {
821
+ status = 403;
822
+ }
823
+ return errorResponse(
824
+ errorCode,
825
+ result.error?.message || "Login failed",
826
+ status
827
+ );
828
+ }
829
+ if (config.onLogin) {
830
+ await config.onLogin({
831
+ id: result.data.user.id,
832
+ email: result.data.user.email
833
+ });
834
+ }
835
+ return withSession(result.data, { user: result.data.user }, cookieOptions);
836
+ }
837
+ // ==================== Logout ====================
838
+ case "logout": {
839
+ const session = await getSession();
840
+ if (session) {
841
+ await sm.auth.logout(session.sessionToken);
842
+ }
843
+ if (config.onLogout) {
844
+ await config.onLogout();
845
+ }
846
+ return clearSession({ message: "Logged out successfully" }, cookieOptions);
847
+ }
848
+ // ==================== Forgot Password ====================
849
+ case "forgot-password": {
850
+ const { email } = body;
851
+ if (!email) {
852
+ return errorResponse("VALIDATION_ERROR", "Email required", 400);
853
+ }
854
+ const result = await sm.auth.forgotPassword(email);
855
+ return successResponse({ message: "If an account exists, a reset email has been sent" });
856
+ }
857
+ // ==================== Reset Password ====================
858
+ case "reset-password": {
859
+ const { token, new_password } = body;
860
+ if (!token || !new_password) {
861
+ return errorResponse("VALIDATION_ERROR", "Token and new password required", 400);
862
+ }
863
+ const result = await sm.auth.resetPassword(token, new_password);
864
+ if (!result.success) {
865
+ return errorResponse(
866
+ result.error?.code || "RESET_FAILED",
867
+ result.error?.message || "Password reset failed",
868
+ 400
869
+ );
870
+ }
871
+ return successResponse({ message: "Password reset successful" });
872
+ }
873
+ // ==================== Verify Email ====================
874
+ case "verify-email": {
875
+ const { token } = body;
876
+ if (!token) {
877
+ return errorResponse("VALIDATION_ERROR", "Token required", 400);
878
+ }
879
+ const result = await sm.auth.verifyEmail(token);
880
+ if (!result.success) {
881
+ return errorResponse(
882
+ result.error?.code || "VERIFY_FAILED",
883
+ result.error?.message || "Email verification failed",
884
+ 400
885
+ );
886
+ }
887
+ return successResponse({ message: "Email verified successfully" });
888
+ }
889
+ // ==================== Resend Verification ====================
890
+ // Supports both authenticated (session-based) and unauthenticated (email-based) resend
891
+ case "resend-verification": {
892
+ const { email } = body;
893
+ const session = await getSession();
894
+ if (email) {
895
+ const result2 = await sm.auth.resendVerification(email);
896
+ if (!result2.success) {
897
+ return errorResponse(
898
+ result2.error?.code || "RESEND_FAILED",
899
+ result2.error?.message || "Failed to resend verification",
900
+ result2.error?.code === "RATE_LIMITED" ? 429 : 400
901
+ );
902
+ }
903
+ return successResponse({ message: "Verification email sent" });
904
+ }
905
+ if (!session) {
906
+ return errorResponse("UNAUTHORIZED", "Email or session required", 401);
907
+ }
908
+ const result = await sm.auth.resendVerification(session.sessionToken);
909
+ if (!result.success) {
910
+ return errorResponse(
911
+ result.error?.code || "RESEND_FAILED",
912
+ result.error?.message || "Failed to resend verification",
913
+ 400
914
+ );
915
+ }
916
+ return successResponse({ message: "Verification email sent" });
917
+ }
918
+ // ==================== Refresh Session ====================
919
+ case "refresh": {
920
+ const session = await getSession();
921
+ if (!session) {
922
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
923
+ }
924
+ const result = await sm.auth.refresh(session.sessionToken);
925
+ if (!result.success || !result.data) {
926
+ return clearSession(
927
+ { message: "Session expired" },
928
+ cookieOptions
929
+ );
930
+ }
931
+ return withRefreshedSession(
932
+ result.data.session_token,
933
+ session.userId,
934
+ { message: "Session refreshed" },
935
+ cookieOptions
936
+ );
937
+ }
938
+ // ==================== Change Password ====================
939
+ case "change-password": {
940
+ const session = await getSession();
941
+ if (!session) {
942
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
943
+ }
944
+ const { current_password, new_password } = body;
945
+ if (!current_password || !new_password) {
946
+ return errorResponse("VALIDATION_ERROR", "Current and new password required", 400);
947
+ }
948
+ const result = await sm.user.changePassword(
949
+ session.sessionToken,
950
+ current_password,
951
+ new_password
952
+ );
953
+ if (!result.success) {
954
+ return errorResponse(
955
+ result.error?.code || "CHANGE_FAILED",
956
+ result.error?.message || "Failed to change password",
957
+ 400
958
+ );
959
+ }
960
+ return successResponse({ message: "Password changed successfully" });
961
+ }
962
+ default:
963
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
964
+ }
965
+ } catch (err) {
966
+ console.error("[ScaleMule Auth] Error:", err);
967
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
968
+ }
969
+ };
970
+ const GET2 = async (request, context) => {
971
+ const params = await context?.params;
972
+ const path = params?.scalemule?.join("/") || "";
973
+ try {
974
+ switch (path) {
975
+ // ==================== Get Current User ====================
976
+ case "me": {
977
+ const session = await getSession();
978
+ if (!session) {
979
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
980
+ }
981
+ const result = await sm.auth.me(session.sessionToken);
982
+ if (!result.success || !result.data) {
983
+ return clearSession(
984
+ { error: { code: "SESSION_EXPIRED", message: "Session expired" } },
985
+ cookieOptions
986
+ );
987
+ }
988
+ return successResponse({ user: result.data });
989
+ }
990
+ // ==================== Get Session Status ====================
991
+ case "session": {
992
+ const session = await getSession();
993
+ return successResponse({
994
+ authenticated: !!session,
995
+ userId: session?.userId || null
996
+ });
997
+ }
998
+ default:
999
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1000
+ }
1001
+ } catch (err) {
1002
+ console.error("[ScaleMule Auth] Error:", err);
1003
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1004
+ }
1005
+ };
1006
+ const DELETE2 = async (request, context) => {
1007
+ const params = await context?.params;
1008
+ const path = params?.scalemule?.join("/") || "";
1009
+ try {
1010
+ switch (path) {
1011
+ // ==================== Delete Account ====================
1012
+ case "me":
1013
+ case "account": {
1014
+ const session = await getSession();
1015
+ if (!session) {
1016
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1017
+ }
1018
+ const body = await request.json().catch(() => ({}));
1019
+ const { password } = body;
1020
+ if (!password) {
1021
+ return errorResponse("VALIDATION_ERROR", "Password required", 400);
1022
+ }
1023
+ const result = await sm.user.deleteAccount(session.sessionToken, password);
1024
+ if (!result.success) {
1025
+ return errorResponse(
1026
+ result.error?.code || "DELETE_FAILED",
1027
+ result.error?.message || "Failed to delete account",
1028
+ 400
1029
+ );
1030
+ }
1031
+ return clearSession({ message: "Account deleted successfully" }, cookieOptions);
1032
+ }
1033
+ default:
1034
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1035
+ }
1036
+ } catch (err) {
1037
+ console.error("[ScaleMule Auth] Error:", err);
1038
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1039
+ }
1040
+ };
1041
+ const PATCH2 = async (request, context) => {
1042
+ const params = await context?.params;
1043
+ const path = params?.scalemule?.join("/") || "";
1044
+ try {
1045
+ switch (path) {
1046
+ // ==================== Update Profile ====================
1047
+ case "me":
1048
+ case "profile": {
1049
+ const session = await getSession();
1050
+ if (!session) {
1051
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1052
+ }
1053
+ const body = await request.json().catch(() => ({}));
1054
+ const { full_name, avatar_url } = body;
1055
+ const result = await sm.user.update(session.sessionToken, { full_name, avatar_url });
1056
+ if (!result.success || !result.data) {
1057
+ return errorResponse(
1058
+ result.error?.code || "UPDATE_FAILED",
1059
+ result.error?.message || "Failed to update profile",
1060
+ 400
1061
+ );
1062
+ }
1063
+ return successResponse({ user: result.data });
1064
+ }
1065
+ default:
1066
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1067
+ }
1068
+ } catch (err) {
1069
+ console.error("[ScaleMule Auth] Error:", err);
1070
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1071
+ }
1072
+ };
1073
+ return { GET: GET2, POST: POST2, DELETE: DELETE2, PATCH: PATCH2 };
1074
+ }
1075
+
1076
+ // src/server/auth.ts
1077
+ var cookieDomain = typeof process !== "undefined" ? process.env.SCALEMULE_COOKIE_DOMAIN : void 0;
1078
+ var handlers = createAuthRoutes({
1079
+ cookies: cookieDomain ? { domain: cookieDomain } : void 0
1080
+ });
1081
+ var { GET, POST, DELETE, PATCH } = handlers;
1082
+
1083
+ export { DELETE, GET, PATCH, POST };