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