@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,1972 @@
1
+ import { cookies } from 'next/headers';
2
+ import { NextResponse } from 'next/server';
3
+ import { createHmac, timingSafeEqual } from 'crypto';
4
+
5
+ // src/server/context.ts
6
+ function validateIP(ip) {
7
+ if (!ip) return void 0;
8
+ const trimmed = ip.trim();
9
+ if (!trimmed) return void 0;
10
+ const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
11
+ const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){0,6}::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}$/;
12
+ const ipv4MappedRegex = /^::ffff:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
13
+ if (ipv4Regex.test(trimmed) || ipv6Regex.test(trimmed) || ipv4MappedRegex.test(trimmed)) {
14
+ return trimmed;
15
+ }
16
+ return void 0;
17
+ }
18
+ function extractClientContext(request) {
19
+ const headers = request.headers;
20
+ let ip;
21
+ const cfConnectingIp = headers.get("cf-connecting-ip");
22
+ if (cfConnectingIp) {
23
+ ip = validateIP(cfConnectingIp);
24
+ }
25
+ if (!ip) {
26
+ const doConnectingIp = headers.get("do-connecting-ip");
27
+ if (doConnectingIp) {
28
+ ip = validateIP(doConnectingIp);
29
+ }
30
+ }
31
+ if (!ip) {
32
+ const realIp = headers.get("x-real-ip");
33
+ if (realIp) {
34
+ ip = validateIP(realIp);
35
+ }
36
+ }
37
+ if (!ip) {
38
+ const forwardedFor = headers.get("x-forwarded-for");
39
+ if (forwardedFor) {
40
+ const firstIp = forwardedFor.split(",")[0]?.trim();
41
+ ip = validateIP(firstIp);
42
+ }
43
+ }
44
+ if (!ip) {
45
+ const vercelForwarded = headers.get("x-vercel-forwarded-for");
46
+ if (vercelForwarded) {
47
+ const firstIp = vercelForwarded.split(",")[0]?.trim();
48
+ ip = validateIP(firstIp);
49
+ }
50
+ }
51
+ if (!ip) {
52
+ const trueClientIp = headers.get("true-client-ip");
53
+ if (trueClientIp) {
54
+ ip = validateIP(trueClientIp);
55
+ }
56
+ }
57
+ if (!ip && request.ip) {
58
+ ip = validateIP(request.ip);
59
+ }
60
+ const userAgent = headers.get("user-agent") || void 0;
61
+ const deviceFingerprint = headers.get("x-device-fingerprint") || void 0;
62
+ const referrer = headers.get("referer") || void 0;
63
+ return {
64
+ ip,
65
+ userAgent,
66
+ deviceFingerprint,
67
+ referrer
68
+ };
69
+ }
70
+ function extractClientContextFromReq(req) {
71
+ const headers = req.headers;
72
+ const getHeader = (name) => {
73
+ const value = headers[name.toLowerCase()];
74
+ if (Array.isArray(value)) {
75
+ return value[0];
76
+ }
77
+ return value;
78
+ };
79
+ let ip;
80
+ const cfConnectingIp = getHeader("cf-connecting-ip");
81
+ if (cfConnectingIp) {
82
+ ip = validateIP(cfConnectingIp);
83
+ }
84
+ if (!ip) {
85
+ const doConnectingIp = getHeader("do-connecting-ip");
86
+ if (doConnectingIp) {
87
+ ip = validateIP(doConnectingIp);
88
+ }
89
+ }
90
+ if (!ip) {
91
+ const realIp = getHeader("x-real-ip");
92
+ if (realIp) {
93
+ ip = validateIP(realIp);
94
+ }
95
+ }
96
+ if (!ip) {
97
+ const forwardedFor = getHeader("x-forwarded-for");
98
+ if (forwardedFor) {
99
+ const firstIp = forwardedFor.split(",")[0]?.trim();
100
+ ip = validateIP(firstIp);
101
+ }
102
+ }
103
+ if (!ip) {
104
+ const vercelForwarded = getHeader("x-vercel-forwarded-for");
105
+ if (vercelForwarded) {
106
+ const firstIp = vercelForwarded.split(",")[0]?.trim();
107
+ ip = validateIP(firstIp);
108
+ }
109
+ }
110
+ if (!ip) {
111
+ const trueClientIp = getHeader("true-client-ip");
112
+ if (trueClientIp) {
113
+ ip = validateIP(trueClientIp);
114
+ }
115
+ }
116
+ if (!ip && req.socket?.remoteAddress) {
117
+ ip = validateIP(req.socket.remoteAddress);
118
+ }
119
+ const userAgent = getHeader("user-agent");
120
+ const deviceFingerprint = getHeader("x-device-fingerprint");
121
+ const referrer = getHeader("referer");
122
+ return {
123
+ ip,
124
+ userAgent,
125
+ deviceFingerprint,
126
+ referrer
127
+ };
128
+ }
129
+ function buildClientContextHeaders(context) {
130
+ const headers = {};
131
+ if (!context) {
132
+ return headers;
133
+ }
134
+ if (context.ip) {
135
+ headers["x-sm-forwarded-client-ip"] = context.ip;
136
+ headers["X-Client-IP"] = context.ip;
137
+ }
138
+ if (context.userAgent) {
139
+ headers["X-Client-User-Agent"] = context.userAgent;
140
+ }
141
+ if (context.deviceFingerprint) {
142
+ headers["X-Client-Device-Fingerprint"] = context.deviceFingerprint;
143
+ }
144
+ if (context.referrer) {
145
+ headers["X-Client-Referrer"] = context.referrer;
146
+ }
147
+ return headers;
148
+ }
149
+
150
+ // src/server/client.ts
151
+ var GATEWAY_URLS = {
152
+ dev: "https://api-dev.scalemule.com",
153
+ prod: "https://api.scalemule.com"
154
+ };
155
+ function resolveGatewayUrl(config) {
156
+ if (config.gatewayUrl) return config.gatewayUrl;
157
+ if (process.env.SCALEMULE_API_URL) return process.env.SCALEMULE_API_URL;
158
+ return GATEWAY_URLS[config.environment || "prod"];
159
+ }
160
+ var ScaleMuleServer = class {
161
+ constructor(config) {
162
+ // ==========================================================================
163
+ // Auth Methods
164
+ // ==========================================================================
165
+ this.auth = {
166
+ /**
167
+ * Register a new user
168
+ */
169
+ register: async (data) => {
170
+ return this.request("POST", "/v1/auth/register", { body: data });
171
+ },
172
+ /**
173
+ * Login user - returns session token (store in HTTP-only cookie)
174
+ */
175
+ login: async (data) => {
176
+ return this.request("POST", "/v1/auth/login", { body: data });
177
+ },
178
+ /**
179
+ * Logout user
180
+ */
181
+ logout: async (sessionToken) => {
182
+ return this.request("POST", "/v1/auth/logout", {
183
+ body: { session_token: sessionToken }
184
+ });
185
+ },
186
+ /**
187
+ * Get current user from session token
188
+ */
189
+ me: async (sessionToken) => {
190
+ return this.request("GET", "/v1/auth/me", { sessionToken });
191
+ },
192
+ /**
193
+ * Refresh session token
194
+ */
195
+ refresh: async (sessionToken) => {
196
+ return this.request("POST", "/v1/auth/refresh", {
197
+ body: { session_token: sessionToken }
198
+ });
199
+ },
200
+ /**
201
+ * Request password reset email
202
+ */
203
+ forgotPassword: async (email) => {
204
+ return this.request("POST", "/v1/auth/forgot-password", { body: { email } });
205
+ },
206
+ /**
207
+ * Reset password with token
208
+ */
209
+ resetPassword: async (token, newPassword) => {
210
+ return this.request("POST", "/v1/auth/reset-password", {
211
+ body: { token, new_password: newPassword }
212
+ });
213
+ },
214
+ /**
215
+ * Verify email with token
216
+ */
217
+ verifyEmail: async (token) => {
218
+ return this.request("POST", "/v1/auth/verify-email", { body: { token } });
219
+ },
220
+ /**
221
+ * Resend verification email.
222
+ * Can be called with a session token (authenticated) or email (unauthenticated).
223
+ */
224
+ resendVerification: async (sessionTokenOrEmail, options) => {
225
+ if (options?.email) {
226
+ return this.request("POST", "/v1/auth/resend-verification", {
227
+ sessionToken: sessionTokenOrEmail,
228
+ body: { email: options.email }
229
+ });
230
+ }
231
+ if (sessionTokenOrEmail.includes("@")) {
232
+ return this.request("POST", "/v1/auth/resend-verification", {
233
+ body: { email: sessionTokenOrEmail }
234
+ });
235
+ }
236
+ return this.request("POST", "/v1/auth/resend-verification", {
237
+ sessionToken: sessionTokenOrEmail
238
+ });
239
+ }
240
+ };
241
+ // ==========================================================================
242
+ // User/Profile Methods
243
+ // ==========================================================================
244
+ this.user = {
245
+ /**
246
+ * Update user profile
247
+ */
248
+ update: async (sessionToken, data) => {
249
+ return this.request("PATCH", "/v1/auth/profile", {
250
+ sessionToken,
251
+ body: data
252
+ });
253
+ },
254
+ /**
255
+ * Change password
256
+ */
257
+ changePassword: async (sessionToken, currentPassword, newPassword) => {
258
+ return this.request("POST", "/v1/auth/change-password", {
259
+ sessionToken,
260
+ body: { current_password: currentPassword, new_password: newPassword }
261
+ });
262
+ },
263
+ /**
264
+ * Change email
265
+ */
266
+ changeEmail: async (sessionToken, newEmail, password) => {
267
+ return this.request("POST", "/v1/auth/change-email", {
268
+ sessionToken,
269
+ body: { new_email: newEmail, password }
270
+ });
271
+ },
272
+ /**
273
+ * Delete account
274
+ */
275
+ deleteAccount: async (sessionToken, password) => {
276
+ return this.request("DELETE", "/v1/auth/me", {
277
+ sessionToken,
278
+ body: { password }
279
+ });
280
+ }
281
+ };
282
+ // ==========================================================================
283
+ // Storage/Content Methods
284
+ // ==========================================================================
285
+ // ==========================================================================
286
+ // Secrets Methods (Tenant Vault)
287
+ // ==========================================================================
288
+ this.secrets = {
289
+ /**
290
+ * Get a secret from the tenant vault
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const result = await scalemule.secrets.get('ANONYMOUS_USER_SALT')
295
+ * if (result.success) {
296
+ * console.log('Salt:', result.data.value)
297
+ * }
298
+ * ```
299
+ */
300
+ get: async (key) => {
301
+ return this.request("GET", `/v1/vault/secrets/${encodeURIComponent(key)}`);
302
+ },
303
+ /**
304
+ * Set a secret in the tenant vault
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * await scalemule.secrets.set('ANONYMOUS_USER_SALT', 'my-secret-salt')
309
+ * ```
310
+ */
311
+ set: async (key, value) => {
312
+ return this.request("PUT", `/v1/vault/secrets/${encodeURIComponent(key)}`, {
313
+ body: { value }
314
+ });
315
+ },
316
+ /**
317
+ * Delete a secret from the tenant vault
318
+ */
319
+ delete: async (key) => {
320
+ return this.request("DELETE", `/v1/vault/secrets/${encodeURIComponent(key)}`);
321
+ },
322
+ /**
323
+ * List all secrets in the tenant vault
324
+ */
325
+ list: async () => {
326
+ return this.request("GET", "/v1/vault/secrets");
327
+ },
328
+ /**
329
+ * Get secret version history
330
+ */
331
+ versions: async (key) => {
332
+ return this.request(
333
+ "GET",
334
+ `/v1/vault/versions/${encodeURIComponent(key)}`
335
+ );
336
+ },
337
+ /**
338
+ * Rollback to a specific version
339
+ */
340
+ rollback: async (key, version) => {
341
+ return this.request(
342
+ "POST",
343
+ `/v1/vault/actions/rollback/${encodeURIComponent(key)}`,
344
+ { body: { version } }
345
+ );
346
+ },
347
+ /**
348
+ * Rotate a secret (copy current version as new version)
349
+ */
350
+ rotate: async (key, newValue) => {
351
+ return this.request(
352
+ "POST",
353
+ `/v1/vault/actions/rotate/${encodeURIComponent(key)}`,
354
+ { body: { value: newValue } }
355
+ );
356
+ }
357
+ };
358
+ // ==========================================================================
359
+ // Bundle Methods (Structured Secrets with Inheritance)
360
+ // ==========================================================================
361
+ this.bundles = {
362
+ /**
363
+ * Get a bundle (structured secret like database credentials)
364
+ *
365
+ * @param key - Bundle key (e.g., 'database/prod')
366
+ * @param resolve - Whether to resolve inheritance (default: true)
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * const result = await scalemule.bundles.get('database/prod')
371
+ * if (result.success) {
372
+ * console.log('DB Host:', result.data.data.host)
373
+ * }
374
+ * ```
375
+ */
376
+ get: async (key, resolve = true) => {
377
+ const params = new URLSearchParams({ resolve: resolve.toString() });
378
+ return this.request(
379
+ "GET",
380
+ `/v1/vault/bundles/${encodeURIComponent(key)}?${params}`
381
+ );
382
+ },
383
+ /**
384
+ * Set a bundle (structured secret)
385
+ *
386
+ * @param key - Bundle key
387
+ * @param type - Bundle type: 'mysql', 'postgres', 'redis', 's3', 'oauth', 'smtp', 'generic'
388
+ * @param data - Bundle data (structure depends on type)
389
+ * @param inheritsFrom - Optional parent bundle key for inheritance
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * // Create a MySQL bundle
394
+ * await scalemule.bundles.set('database/prod', 'mysql', {
395
+ * host: 'db.example.com',
396
+ * port: 3306,
397
+ * username: 'app',
398
+ * password: 'secret',
399
+ * database: 'myapp'
400
+ * })
401
+ *
402
+ * // Create a bundle that inherits from another
403
+ * await scalemule.bundles.set('database/staging', 'mysql', {
404
+ * host: 'staging-db.example.com', // Override just the host
405
+ * }, 'database/prod')
406
+ * ```
407
+ */
408
+ set: async (key, type, data, inheritsFrom) => {
409
+ return this.request(
410
+ "PUT",
411
+ `/v1/vault/bundles/${encodeURIComponent(key)}`,
412
+ {
413
+ body: {
414
+ type,
415
+ value: data,
416
+ inherits_from: inheritsFrom
417
+ }
418
+ }
419
+ );
420
+ },
421
+ /**
422
+ * Delete a bundle
423
+ */
424
+ delete: async (key) => {
425
+ return this.request("DELETE", `/v1/vault/bundles/${encodeURIComponent(key)}`);
426
+ },
427
+ /**
428
+ * List all bundles
429
+ */
430
+ list: async () => {
431
+ return this.request(
432
+ "GET",
433
+ "/v1/vault/bundles"
434
+ );
435
+ },
436
+ /**
437
+ * Get connection URL for a database bundle
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * const result = await scalemule.bundles.connectionUrl('database/prod')
442
+ * if (result.success) {
443
+ * const client = mysql.createConnection(result.data.url)
444
+ * }
445
+ * ```
446
+ */
447
+ connectionUrl: async (key) => {
448
+ return this.request(
449
+ "GET",
450
+ `/v1/vault/bundles/${encodeURIComponent(key)}?connection_url=true`
451
+ );
452
+ }
453
+ };
454
+ // ==========================================================================
455
+ // Vault Audit Methods
456
+ // ==========================================================================
457
+ this.vaultAudit = {
458
+ /**
459
+ * Query audit logs for your tenant's vault operations
460
+ *
461
+ * @example
462
+ * ```typescript
463
+ * const result = await scalemule.vaultAudit.query({
464
+ * action: 'read',
465
+ * path: 'database/*',
466
+ * since: '2026-01-01'
467
+ * })
468
+ * ```
469
+ */
470
+ query: async (options) => {
471
+ const params = new URLSearchParams();
472
+ if (options?.action) params.set("action", options.action);
473
+ if (options?.path) params.set("path", options.path);
474
+ if (options?.since) params.set("since", options.since);
475
+ if (options?.until) params.set("until", options.until);
476
+ if (options?.limit) params.set("limit", options.limit.toString());
477
+ const queryStr = params.toString();
478
+ return this.request("GET", `/v1/vault/audit${queryStr ? `?${queryStr}` : ""}`);
479
+ }
480
+ };
481
+ this.storage = {
482
+ /**
483
+ * List user's files
484
+ */
485
+ list: async (userId, params) => {
486
+ const query = new URLSearchParams();
487
+ if (params?.content_type) query.set("content_type", params.content_type);
488
+ if (params?.search) query.set("search", params.search);
489
+ if (params?.limit) query.set("limit", params.limit.toString());
490
+ if (params?.offset) query.set("offset", params.offset.toString());
491
+ const queryStr = query.toString();
492
+ const path = `/v1/storage/my-files${queryStr ? `?${queryStr}` : ""}`;
493
+ return this.request("GET", path, { userId });
494
+ },
495
+ /**
496
+ * Get file info
497
+ */
498
+ get: async (fileId) => {
499
+ return this.request("GET", `/v1/storage/files/${fileId}/info`);
500
+ },
501
+ /**
502
+ * Delete file
503
+ */
504
+ delete: async (userId, fileId) => {
505
+ return this.request("DELETE", `/v1/storage/files/${fileId}`, { userId });
506
+ },
507
+ /**
508
+ * Upload file (from server - use FormData)
509
+ *
510
+ * @param userId - The user ID who owns this file
511
+ * @param file - File data to upload
512
+ * @param options - Upload options
513
+ * @param options.clientContext - End user context to forward (IP, user agent, etc.)
514
+ *
515
+ * @example
516
+ * ```typescript
517
+ * // Forward end user context for proper attribution
518
+ * const result = await scalemule.storage.upload(
519
+ * userId,
520
+ * { buffer, filename, contentType },
521
+ * { clientContext: extractClientContext(request) }
522
+ * )
523
+ * ```
524
+ */
525
+ upload: async (userId, file, options) => {
526
+ const formData = new FormData();
527
+ const blob = new Blob([file.buffer], { type: file.contentType });
528
+ formData.append("file", blob, file.filename);
529
+ formData.append("sm_user_id", userId);
530
+ const url = `${this.gatewayUrl}/v1/storage/upload`;
531
+ const headers = {
532
+ "x-api-key": this.apiKey,
533
+ "x-user-id": userId,
534
+ ...buildClientContextHeaders(options?.clientContext)
535
+ };
536
+ if (this.debug && options?.clientContext) {
537
+ console.log(`[ScaleMule Server] Upload with client context: IP=${options.clientContext.ip}`);
538
+ }
539
+ try {
540
+ const response = await fetch(url, {
541
+ method: "POST",
542
+ headers,
543
+ body: formData
544
+ });
545
+ const data = await response.json();
546
+ if (!response.ok) {
547
+ return {
548
+ success: false,
549
+ error: data.error || { code: "UPLOAD_FAILED", message: "Upload failed" }
550
+ };
551
+ }
552
+ return data;
553
+ } catch (err) {
554
+ return {
555
+ success: false,
556
+ error: {
557
+ code: "UPLOAD_ERROR",
558
+ message: err instanceof Error ? err.message : "Upload failed"
559
+ }
560
+ };
561
+ }
562
+ }
563
+ };
564
+ // ==========================================================================
565
+ // Analytics Methods
566
+ // ==========================================================================
567
+ // ==========================================================================
568
+ // Webhooks Methods
569
+ // ==========================================================================
570
+ this.webhooks = {
571
+ /**
572
+ * Create a new webhook subscription
573
+ *
574
+ * @example
575
+ * ```typescript
576
+ * const result = await scalemule.webhooks.create({
577
+ * webhook_name: 'Video Status Webhook',
578
+ * url: 'https://myapp.com/api/webhooks/scalemule',
579
+ * events: ['video.ready', 'video.failed']
580
+ * })
581
+ *
582
+ * // Store the secret for signature verification
583
+ * console.log('Webhook secret:', result.data.secret)
584
+ * ```
585
+ */
586
+ create: async (data) => {
587
+ return this.request(
588
+ "POST",
589
+ "/v1/webhooks",
590
+ { body: data }
591
+ );
592
+ },
593
+ /**
594
+ * List all webhook subscriptions
595
+ */
596
+ list: async () => {
597
+ return this.request("GET", "/v1/webhooks");
598
+ },
599
+ /**
600
+ * Delete a webhook subscription
601
+ */
602
+ delete: async (id) => {
603
+ return this.request("DELETE", `/v1/webhooks/${id}`);
604
+ },
605
+ /**
606
+ * Update a webhook subscription
607
+ */
608
+ update: async (id, data) => {
609
+ return this.request(
610
+ "PATCH",
611
+ `/v1/webhooks/${id}`,
612
+ { body: data }
613
+ );
614
+ },
615
+ /**
616
+ * Get available webhook event types
617
+ */
618
+ eventTypes: async () => {
619
+ return this.request("GET", "/v1/webhooks/events");
620
+ }
621
+ };
622
+ // ==========================================================================
623
+ // Analytics Methods
624
+ // ==========================================================================
625
+ this.analytics = {
626
+ /**
627
+ * Track an analytics event
628
+ *
629
+ * IMPORTANT: When calling from server-side code (API routes), always pass
630
+ * clientContext to ensure the real end user's IP is recorded, not the server's IP.
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * // In an API route
635
+ * import { extractClientContext, createServerClient } from '@scalemule/nextjs/server'
636
+ *
637
+ * export async function POST(request: NextRequest) {
638
+ * const clientContext = extractClientContext(request)
639
+ * const scalemule = createServerClient()
640
+ *
641
+ * await scalemule.analytics.trackEvent({
642
+ * event_name: 'button_clicked',
643
+ * properties: { button_id: 'signup' }
644
+ * }, { clientContext })
645
+ * }
646
+ * ```
647
+ */
648
+ trackEvent: async (event, options) => {
649
+ return this.request("POST", "/v1/analytics/v2/events", {
650
+ body: event,
651
+ clientContext: options?.clientContext
652
+ });
653
+ },
654
+ /**
655
+ * Track a page view
656
+ *
657
+ * @example
658
+ * ```typescript
659
+ * await scalemule.analytics.trackPageView({
660
+ * page_url: 'https://example.com/products',
661
+ * page_title: 'Products',
662
+ * referrer: 'https://google.com'
663
+ * }, { clientContext })
664
+ * ```
665
+ */
666
+ trackPageView: async (data, options) => {
667
+ return this.request("POST", "/v1/analytics/v2/events", {
668
+ body: {
669
+ event_name: "page_viewed",
670
+ event_category: "navigation",
671
+ page_url: data.page_url,
672
+ properties: {
673
+ page_title: data.page_title,
674
+ referrer: data.referrer
675
+ },
676
+ session_id: data.session_id,
677
+ user_id: data.user_id
678
+ },
679
+ clientContext: options?.clientContext
680
+ });
681
+ },
682
+ /**
683
+ * Track multiple events in a batch (max 100)
684
+ *
685
+ * @example
686
+ * ```typescript
687
+ * await scalemule.analytics.trackBatch([
688
+ * { event_name: 'item_viewed', properties: { item_id: '123' } },
689
+ * { event_name: 'item_added_to_cart', properties: { item_id: '123' } }
690
+ * ], { clientContext })
691
+ * ```
692
+ */
693
+ trackBatch: async (events, options) => {
694
+ return this.request("POST", "/v1/analytics/v2/events/batch", {
695
+ body: { events },
696
+ clientContext: options?.clientContext
697
+ });
698
+ }
699
+ };
700
+ this.apiKey = config.apiKey;
701
+ this.gatewayUrl = resolveGatewayUrl(config);
702
+ this.debug = config.debug || false;
703
+ }
704
+ /**
705
+ * Make a request to the ScaleMule API
706
+ *
707
+ * @param method - HTTP method
708
+ * @param path - API path (e.g., /v1/auth/login)
709
+ * @param options - Request options
710
+ * @param options.body - Request body (will be JSON stringified)
711
+ * @param options.userId - User ID (passed through for storage operations)
712
+ * @param options.sessionToken - Session token sent as Authorization: Bearer header
713
+ * @param options.clientContext - End user context to forward (IP, user agent, etc.)
714
+ */
715
+ async request(method, path, options = {}) {
716
+ const url = `${this.gatewayUrl}${path}`;
717
+ const headers = {
718
+ "x-api-key": this.apiKey,
719
+ "Content-Type": "application/json",
720
+ // Forward client context headers if provided
721
+ ...buildClientContextHeaders(options.clientContext)
722
+ };
723
+ if (options.sessionToken) {
724
+ headers["Authorization"] = `Bearer ${options.sessionToken}`;
725
+ }
726
+ if (this.debug) {
727
+ console.log(`[ScaleMule Server] ${method} ${path}`);
728
+ if (options.clientContext) {
729
+ console.log(`[ScaleMule Server] Client context: IP=${options.clientContext.ip}, UA=${options.clientContext.userAgent?.substring(0, 50)}...`);
730
+ }
731
+ }
732
+ try {
733
+ const response = await fetch(url, {
734
+ method,
735
+ headers,
736
+ body: options.body ? JSON.stringify(options.body) : void 0
737
+ });
738
+ const data = await response.json();
739
+ if (!response.ok) {
740
+ const error = data.error || {
741
+ code: `HTTP_${response.status}`,
742
+ message: data.message || response.statusText
743
+ };
744
+ return { success: false, error };
745
+ }
746
+ return data;
747
+ } catch (err) {
748
+ return {
749
+ success: false,
750
+ error: {
751
+ code: "SERVER_ERROR",
752
+ message: err instanceof Error ? err.message : "Request failed"
753
+ }
754
+ };
755
+ }
756
+ }
757
+ };
758
+ function createServerClient(config) {
759
+ const apiKey = config?.apiKey || process.env.SCALEMULE_API_KEY;
760
+ if (!apiKey) {
761
+ throw new Error(
762
+ "ScaleMule API key is required. Set SCALEMULE_API_KEY environment variable or pass apiKey in config."
763
+ );
764
+ }
765
+ const environment = config?.environment || process.env.SCALEMULE_ENV || "prod";
766
+ return new ScaleMuleServer({
767
+ apiKey,
768
+ environment,
769
+ gatewayUrl: config?.gatewayUrl,
770
+ debug: config?.debug || process.env.SCALEMULE_DEBUG === "true"
771
+ });
772
+ }
773
+ var SESSION_COOKIE_NAME = "sm_session";
774
+ var USER_ID_COOKIE_NAME = "sm_user_id";
775
+ ({
776
+ secure: process.env.NODE_ENV === "production"});
777
+ function createCookieHeader(name, value, options = {}) {
778
+ const maxAge = options.maxAge ?? 7 * 24 * 60 * 60;
779
+ const secure = options.secure ?? process.env.NODE_ENV === "production";
780
+ const sameSite = options.sameSite ?? "lax";
781
+ const path = options.path ?? "/";
782
+ let cookie = `${name}=${encodeURIComponent(value)}; Path=${path}; Max-Age=${maxAge}; HttpOnly; SameSite=${sameSite}`;
783
+ if (secure) {
784
+ cookie += "; Secure";
785
+ }
786
+ if (options.domain) {
787
+ cookie += `; Domain=${options.domain}`;
788
+ }
789
+ return cookie;
790
+ }
791
+ function createClearCookieHeader(name, options = {}) {
792
+ const path = options.path ?? "/";
793
+ let cookie = `${name}=; Path=${path}; Max-Age=0; HttpOnly`;
794
+ if (options.domain) {
795
+ cookie += `; Domain=${options.domain}`;
796
+ }
797
+ return cookie;
798
+ }
799
+ function withSession(loginResponse, responseBody, options = {}) {
800
+ const headers = new Headers();
801
+ headers.set("Content-Type", "application/json");
802
+ headers.append(
803
+ "Set-Cookie",
804
+ createCookieHeader(SESSION_COOKIE_NAME, loginResponse.session_token, options)
805
+ );
806
+ headers.append(
807
+ "Set-Cookie",
808
+ createCookieHeader(USER_ID_COOKIE_NAME, loginResponse.user.id, options)
809
+ );
810
+ return new Response(JSON.stringify({ success: true, data: responseBody }), {
811
+ status: 200,
812
+ headers
813
+ });
814
+ }
815
+ function withRefreshedSession(sessionToken, userId, responseBody, options = {}) {
816
+ const headers = new Headers();
817
+ headers.set("Content-Type", "application/json");
818
+ headers.append(
819
+ "Set-Cookie",
820
+ createCookieHeader(SESSION_COOKIE_NAME, sessionToken, options)
821
+ );
822
+ headers.append(
823
+ "Set-Cookie",
824
+ createCookieHeader(USER_ID_COOKIE_NAME, userId, options)
825
+ );
826
+ return new Response(JSON.stringify({ success: true, data: responseBody }), {
827
+ status: 200,
828
+ headers
829
+ });
830
+ }
831
+ function clearSession(responseBody, options = {}, status = 200) {
832
+ const headers = new Headers();
833
+ headers.set("Content-Type", "application/json");
834
+ headers.append("Set-Cookie", createClearCookieHeader(SESSION_COOKIE_NAME, options));
835
+ headers.append("Set-Cookie", createClearCookieHeader(USER_ID_COOKIE_NAME, options));
836
+ return new Response(JSON.stringify({ success: status < 300, data: responseBody }), {
837
+ status,
838
+ headers
839
+ });
840
+ }
841
+ async function getSession() {
842
+ const cookieStore = await cookies();
843
+ const sessionCookie = cookieStore.get(SESSION_COOKIE_NAME);
844
+ const userIdCookie = cookieStore.get(USER_ID_COOKIE_NAME);
845
+ if (!sessionCookie?.value || !userIdCookie?.value) {
846
+ return null;
847
+ }
848
+ return {
849
+ sessionToken: sessionCookie.value,
850
+ userId: userIdCookie.value,
851
+ expiresAt: /* @__PURE__ */ new Date()
852
+ // Note: actual expiry is managed by ScaleMule backend
853
+ };
854
+ }
855
+ function getSessionFromRequest(request) {
856
+ const cookieHeader = request.headers.get("cookie");
857
+ if (!cookieHeader) return null;
858
+ const cookies4 = Object.fromEntries(
859
+ cookieHeader.split(";").map((c) => {
860
+ const [key, ...rest] = c.trim().split("=");
861
+ return [key, decodeURIComponent(rest.join("="))];
862
+ })
863
+ );
864
+ const sessionToken = cookies4[SESSION_COOKIE_NAME];
865
+ const userId = cookies4[USER_ID_COOKIE_NAME];
866
+ if (!sessionToken || !userId) {
867
+ return null;
868
+ }
869
+ return {
870
+ sessionToken,
871
+ userId,
872
+ expiresAt: /* @__PURE__ */ new Date()
873
+ };
874
+ }
875
+ async function requireSession() {
876
+ const session = await getSession();
877
+ if (!session) {
878
+ throw new Response(
879
+ JSON.stringify({
880
+ success: false,
881
+ error: { code: "UNAUTHORIZED", message: "Authentication required" }
882
+ }),
883
+ {
884
+ status: 401,
885
+ headers: { "Content-Type": "application/json" }
886
+ }
887
+ );
888
+ }
889
+ return session;
890
+ }
891
+
892
+ // src/server/timing.ts
893
+ function constantTimeEqual(a, b) {
894
+ const maxLength = Math.max(a.length, b.length);
895
+ let mismatch = a.length ^ b.length;
896
+ for (let i = 0; i < maxLength; i++) {
897
+ const aCode = i < a.length ? a.charCodeAt(i) : 0;
898
+ const bCode = i < b.length ? b.charCodeAt(i) : 0;
899
+ mismatch |= aCode ^ bCode;
900
+ }
901
+ return mismatch === 0;
902
+ }
903
+
904
+ // src/server/csrf.ts
905
+ var CSRF_COOKIE_NAME = "sm_csrf";
906
+ var CSRF_HEADER_NAME = "x-csrf-token";
907
+ function generateCSRFToken() {
908
+ const array = new Uint8Array(32);
909
+ crypto.getRandomValues(array);
910
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
911
+ }
912
+ function withCSRFToken(response, token) {
913
+ const csrfToken = token || generateCSRFToken();
914
+ response.cookies.set(CSRF_COOKIE_NAME, csrfToken, {
915
+ httpOnly: false,
916
+ // Must be readable by JavaScript to include in requests
917
+ secure: process.env.NODE_ENV === "production",
918
+ sameSite: "strict",
919
+ path: "/",
920
+ maxAge: 60 * 60 * 24
921
+ // 24 hours
922
+ });
923
+ return response;
924
+ }
925
+ function validateCSRFToken(request) {
926
+ const cookieToken = request.cookies.get(CSRF_COOKIE_NAME)?.value;
927
+ if (!cookieToken) {
928
+ return "Missing CSRF cookie";
929
+ }
930
+ const headerToken = request.headers.get(CSRF_HEADER_NAME);
931
+ if (!headerToken) {
932
+ return "Missing CSRF token header";
933
+ }
934
+ if (!constantTimeEqual(cookieToken, headerToken)) {
935
+ return "CSRF token mismatch";
936
+ }
937
+ return void 0;
938
+ }
939
+ async function validateCSRFTokenAsync(request, body) {
940
+ const cookieToken = request.cookies.get(CSRF_COOKIE_NAME)?.value;
941
+ if (!cookieToken) {
942
+ return "Missing CSRF cookie";
943
+ }
944
+ let requestToken = request.headers.get(CSRF_HEADER_NAME);
945
+ if (!requestToken && body) {
946
+ requestToken = body.csrf_token ?? body._csrf ?? null;
947
+ }
948
+ if (!requestToken) {
949
+ return "Missing CSRF token";
950
+ }
951
+ if (!constantTimeEqual(cookieToken, requestToken)) {
952
+ return "CSRF token mismatch";
953
+ }
954
+ return void 0;
955
+ }
956
+ function withCSRFProtection(handler) {
957
+ return async (request) => {
958
+ if (["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) {
959
+ const error = validateCSRFToken(request);
960
+ if (error) {
961
+ return NextResponse.json(
962
+ { error: "CSRF validation failed", message: error },
963
+ { status: 403 }
964
+ );
965
+ }
966
+ }
967
+ return handler(request);
968
+ };
969
+ }
970
+ async function getCSRFToken() {
971
+ const cookieStore = await cookies();
972
+ let token = cookieStore.get(CSRF_COOKIE_NAME)?.value;
973
+ if (!token) {
974
+ token = generateCSRFToken();
975
+ }
976
+ return token;
977
+ }
978
+
979
+ // src/server/routes.ts
980
+ function errorResponse(code, message, status) {
981
+ return new Response(
982
+ JSON.stringify({ success: false, error: { code, message } }),
983
+ { status, headers: { "Content-Type": "application/json" } }
984
+ );
985
+ }
986
+ function successResponse(data, status = 200) {
987
+ return new Response(
988
+ JSON.stringify({ success: true, data }),
989
+ { status, headers: { "Content-Type": "application/json" } }
990
+ );
991
+ }
992
+ function createAuthRoutes(config = {}) {
993
+ const sm = createServerClient(config.client);
994
+ const cookieOptions = config.cookies || {};
995
+ const POST = async (request, context) => {
996
+ if (config.csrf) {
997
+ const csrfError = validateCSRFToken(request);
998
+ if (csrfError) {
999
+ return errorResponse("CSRF_ERROR", "CSRF validation failed", 403);
1000
+ }
1001
+ }
1002
+ const params = await context?.params;
1003
+ const path = params?.scalemule?.join("/") || "";
1004
+ try {
1005
+ const body = await request.json().catch(() => ({}));
1006
+ switch (path) {
1007
+ // ==================== Register ====================
1008
+ case "register": {
1009
+ const { email, password, full_name, username, phone } = body;
1010
+ if (!email || !password) {
1011
+ return errorResponse("VALIDATION_ERROR", "Email and password required", 400);
1012
+ }
1013
+ const result = await sm.auth.register({ email, password, full_name, username, phone });
1014
+ if (!result.success) {
1015
+ return errorResponse(
1016
+ result.error?.code || "REGISTER_FAILED",
1017
+ result.error?.message || "Registration failed",
1018
+ 400
1019
+ );
1020
+ }
1021
+ if (config.onRegister && result.data) {
1022
+ await config.onRegister({ id: result.data.id, email: result.data.email });
1023
+ }
1024
+ return successResponse({ user: result.data, message: "Registration successful" }, 201);
1025
+ }
1026
+ // ==================== Login ====================
1027
+ case "login": {
1028
+ const { email, password, remember_me } = body;
1029
+ if (!email || !password) {
1030
+ return errorResponse("VALIDATION_ERROR", "Email and password required", 400);
1031
+ }
1032
+ const result = await sm.auth.login({ email, password, remember_me });
1033
+ if (!result.success || !result.data) {
1034
+ const errorCode = result.error?.code || "LOGIN_FAILED";
1035
+ let status = 400;
1036
+ if (errorCode === "INVALID_CREDENTIALS" || errorCode === "UNAUTHORIZED") status = 401;
1037
+ if (["EMAIL_NOT_VERIFIED", "PHONE_NOT_VERIFIED", "ACCOUNT_LOCKED", "ACCOUNT_DISABLED", "MFA_REQUIRED"].includes(errorCode)) {
1038
+ status = 403;
1039
+ }
1040
+ return errorResponse(
1041
+ errorCode,
1042
+ result.error?.message || "Login failed",
1043
+ status
1044
+ );
1045
+ }
1046
+ if (config.onLogin) {
1047
+ await config.onLogin({
1048
+ id: result.data.user.id,
1049
+ email: result.data.user.email
1050
+ });
1051
+ }
1052
+ return withSession(result.data, { user: result.data.user }, cookieOptions);
1053
+ }
1054
+ // ==================== Logout ====================
1055
+ case "logout": {
1056
+ const session = await getSession();
1057
+ if (session) {
1058
+ await sm.auth.logout(session.sessionToken);
1059
+ }
1060
+ if (config.onLogout) {
1061
+ await config.onLogout();
1062
+ }
1063
+ return clearSession({ message: "Logged out successfully" }, cookieOptions);
1064
+ }
1065
+ // ==================== Forgot Password ====================
1066
+ case "forgot-password": {
1067
+ const { email } = body;
1068
+ if (!email) {
1069
+ return errorResponse("VALIDATION_ERROR", "Email required", 400);
1070
+ }
1071
+ const result = await sm.auth.forgotPassword(email);
1072
+ return successResponse({ message: "If an account exists, a reset email has been sent" });
1073
+ }
1074
+ // ==================== Reset Password ====================
1075
+ case "reset-password": {
1076
+ const { token, new_password } = body;
1077
+ if (!token || !new_password) {
1078
+ return errorResponse("VALIDATION_ERROR", "Token and new password required", 400);
1079
+ }
1080
+ const result = await sm.auth.resetPassword(token, new_password);
1081
+ if (!result.success) {
1082
+ return errorResponse(
1083
+ result.error?.code || "RESET_FAILED",
1084
+ result.error?.message || "Password reset failed",
1085
+ 400
1086
+ );
1087
+ }
1088
+ return successResponse({ message: "Password reset successful" });
1089
+ }
1090
+ // ==================== Verify Email ====================
1091
+ case "verify-email": {
1092
+ const { token } = body;
1093
+ if (!token) {
1094
+ return errorResponse("VALIDATION_ERROR", "Token required", 400);
1095
+ }
1096
+ const result = await sm.auth.verifyEmail(token);
1097
+ if (!result.success) {
1098
+ return errorResponse(
1099
+ result.error?.code || "VERIFY_FAILED",
1100
+ result.error?.message || "Email verification failed",
1101
+ 400
1102
+ );
1103
+ }
1104
+ return successResponse({ message: "Email verified successfully" });
1105
+ }
1106
+ // ==================== Resend Verification ====================
1107
+ // Supports both authenticated (session-based) and unauthenticated (email-based) resend
1108
+ case "resend-verification": {
1109
+ const { email } = body;
1110
+ const session = await getSession();
1111
+ if (email) {
1112
+ const result2 = await sm.auth.resendVerification(email);
1113
+ if (!result2.success) {
1114
+ return errorResponse(
1115
+ result2.error?.code || "RESEND_FAILED",
1116
+ result2.error?.message || "Failed to resend verification",
1117
+ result2.error?.code === "RATE_LIMITED" ? 429 : 400
1118
+ );
1119
+ }
1120
+ return successResponse({ message: "Verification email sent" });
1121
+ }
1122
+ if (!session) {
1123
+ return errorResponse("UNAUTHORIZED", "Email or session required", 401);
1124
+ }
1125
+ const result = await sm.auth.resendVerification(session.sessionToken);
1126
+ if (!result.success) {
1127
+ return errorResponse(
1128
+ result.error?.code || "RESEND_FAILED",
1129
+ result.error?.message || "Failed to resend verification",
1130
+ 400
1131
+ );
1132
+ }
1133
+ return successResponse({ message: "Verification email sent" });
1134
+ }
1135
+ // ==================== Refresh Session ====================
1136
+ case "refresh": {
1137
+ const session = await getSession();
1138
+ if (!session) {
1139
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1140
+ }
1141
+ const result = await sm.auth.refresh(session.sessionToken);
1142
+ if (!result.success || !result.data) {
1143
+ return clearSession(
1144
+ { message: "Session expired" },
1145
+ cookieOptions
1146
+ );
1147
+ }
1148
+ return withRefreshedSession(
1149
+ result.data.session_token,
1150
+ session.userId,
1151
+ { message: "Session refreshed" },
1152
+ cookieOptions
1153
+ );
1154
+ }
1155
+ // ==================== Change Password ====================
1156
+ case "change-password": {
1157
+ const session = await getSession();
1158
+ if (!session) {
1159
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1160
+ }
1161
+ const { current_password, new_password } = body;
1162
+ if (!current_password || !new_password) {
1163
+ return errorResponse("VALIDATION_ERROR", "Current and new password required", 400);
1164
+ }
1165
+ const result = await sm.user.changePassword(
1166
+ session.sessionToken,
1167
+ current_password,
1168
+ new_password
1169
+ );
1170
+ if (!result.success) {
1171
+ return errorResponse(
1172
+ result.error?.code || "CHANGE_FAILED",
1173
+ result.error?.message || "Failed to change password",
1174
+ 400
1175
+ );
1176
+ }
1177
+ return successResponse({ message: "Password changed successfully" });
1178
+ }
1179
+ default:
1180
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1181
+ }
1182
+ } catch (err) {
1183
+ console.error("[ScaleMule Auth] Error:", err);
1184
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1185
+ }
1186
+ };
1187
+ const GET = async (request, context) => {
1188
+ const params = await context?.params;
1189
+ const path = params?.scalemule?.join("/") || "";
1190
+ try {
1191
+ switch (path) {
1192
+ // ==================== Get Current User ====================
1193
+ case "me": {
1194
+ const session = await getSession();
1195
+ if (!session) {
1196
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1197
+ }
1198
+ const result = await sm.auth.me(session.sessionToken);
1199
+ if (!result.success || !result.data) {
1200
+ return clearSession(
1201
+ { error: { code: "SESSION_EXPIRED", message: "Session expired" } },
1202
+ cookieOptions
1203
+ );
1204
+ }
1205
+ return successResponse({ user: result.data });
1206
+ }
1207
+ // ==================== Get Session Status ====================
1208
+ case "session": {
1209
+ const session = await getSession();
1210
+ return successResponse({
1211
+ authenticated: !!session,
1212
+ userId: session?.userId || null
1213
+ });
1214
+ }
1215
+ default:
1216
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1217
+ }
1218
+ } catch (err) {
1219
+ console.error("[ScaleMule Auth] Error:", err);
1220
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1221
+ }
1222
+ };
1223
+ const DELETE = async (request, context) => {
1224
+ const params = await context?.params;
1225
+ const path = params?.scalemule?.join("/") || "";
1226
+ try {
1227
+ switch (path) {
1228
+ // ==================== Delete Account ====================
1229
+ case "me":
1230
+ case "account": {
1231
+ const session = await getSession();
1232
+ if (!session) {
1233
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1234
+ }
1235
+ const body = await request.json().catch(() => ({}));
1236
+ const { password } = body;
1237
+ if (!password) {
1238
+ return errorResponse("VALIDATION_ERROR", "Password required", 400);
1239
+ }
1240
+ const result = await sm.user.deleteAccount(session.sessionToken, password);
1241
+ if (!result.success) {
1242
+ return errorResponse(
1243
+ result.error?.code || "DELETE_FAILED",
1244
+ result.error?.message || "Failed to delete account",
1245
+ 400
1246
+ );
1247
+ }
1248
+ return clearSession({ message: "Account deleted successfully" }, cookieOptions);
1249
+ }
1250
+ default:
1251
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1252
+ }
1253
+ } catch (err) {
1254
+ console.error("[ScaleMule Auth] Error:", err);
1255
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1256
+ }
1257
+ };
1258
+ const PATCH = async (request, context) => {
1259
+ const params = await context?.params;
1260
+ const path = params?.scalemule?.join("/") || "";
1261
+ try {
1262
+ switch (path) {
1263
+ // ==================== Update Profile ====================
1264
+ case "me":
1265
+ case "profile": {
1266
+ const session = await getSession();
1267
+ if (!session) {
1268
+ return errorResponse("UNAUTHORIZED", "Authentication required", 401);
1269
+ }
1270
+ const body = await request.json().catch(() => ({}));
1271
+ const { full_name, avatar_url } = body;
1272
+ const result = await sm.user.update(session.sessionToken, { full_name, avatar_url });
1273
+ if (!result.success || !result.data) {
1274
+ return errorResponse(
1275
+ result.error?.code || "UPDATE_FAILED",
1276
+ result.error?.message || "Failed to update profile",
1277
+ 400
1278
+ );
1279
+ }
1280
+ return successResponse({ user: result.data });
1281
+ }
1282
+ default:
1283
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1284
+ }
1285
+ } catch (err) {
1286
+ console.error("[ScaleMule Auth] Error:", err);
1287
+ return errorResponse("SERVER_ERROR", "Internal server error", 500);
1288
+ }
1289
+ };
1290
+ return { GET, POST, DELETE, PATCH };
1291
+ }
1292
+ function createAnalyticsRoutes(config = {}) {
1293
+ const sm = createServerClient(config.client);
1294
+ const handleTrackEvent = async (body, clientContext) => {
1295
+ const {
1296
+ event_name,
1297
+ event_category,
1298
+ properties,
1299
+ user_id,
1300
+ session_id,
1301
+ anonymous_id,
1302
+ session_duration_seconds,
1303
+ page_url,
1304
+ page_title,
1305
+ referrer,
1306
+ landing_page,
1307
+ device_type,
1308
+ device_brand,
1309
+ device_model,
1310
+ browser,
1311
+ browser_version,
1312
+ os,
1313
+ os_version,
1314
+ screen_resolution,
1315
+ viewport_size,
1316
+ utm_source,
1317
+ utm_medium,
1318
+ utm_campaign,
1319
+ utm_term,
1320
+ utm_content,
1321
+ client_timestamp,
1322
+ timestamp
1323
+ // Legacy field
1324
+ } = body;
1325
+ if (!event_name) {
1326
+ return errorResponse("VALIDATION_ERROR", "event_name is required", 400);
1327
+ }
1328
+ const result = await sm.analytics.trackEvent(
1329
+ {
1330
+ event_name,
1331
+ event_category,
1332
+ properties,
1333
+ user_id,
1334
+ session_id,
1335
+ anonymous_id,
1336
+ session_duration_seconds,
1337
+ page_url,
1338
+ page_title,
1339
+ referrer,
1340
+ landing_page,
1341
+ device_type,
1342
+ device_brand,
1343
+ device_model,
1344
+ browser,
1345
+ browser_version,
1346
+ os,
1347
+ os_version,
1348
+ screen_resolution,
1349
+ viewport_size,
1350
+ utm_source,
1351
+ utm_medium,
1352
+ utm_campaign,
1353
+ utm_term,
1354
+ utm_content,
1355
+ client_timestamp: client_timestamp || timestamp
1356
+ },
1357
+ { clientContext }
1358
+ );
1359
+ if (!result.success) {
1360
+ return errorResponse(
1361
+ result.error?.code || "TRACK_FAILED",
1362
+ result.error?.message || "Failed to track event",
1363
+ 400
1364
+ );
1365
+ }
1366
+ if (config.onEvent) {
1367
+ await config.onEvent({ event_name, session_id: result.data?.session_id });
1368
+ }
1369
+ return successResponse({ tracked: result.data?.tracked || 1, session_id: result.data?.session_id });
1370
+ };
1371
+ const POST = async (request, context) => {
1372
+ try {
1373
+ const body = await request.json().catch(() => ({}));
1374
+ const clientContext = extractClientContext(request);
1375
+ if (config.simpleProxy) {
1376
+ return handleTrackEvent(body, clientContext);
1377
+ }
1378
+ const params = await context?.params;
1379
+ const path = params?.scalemule?.join("/") || "";
1380
+ switch (path) {
1381
+ // ==================== Track Single Event ====================
1382
+ case "event":
1383
+ case "events":
1384
+ case "": {
1385
+ return handleTrackEvent(body, clientContext);
1386
+ }
1387
+ // ==================== Track Batch Events ====================
1388
+ case "batch": {
1389
+ const { events } = body;
1390
+ if (!Array.isArray(events) || events.length === 0) {
1391
+ return errorResponse("VALIDATION_ERROR", "events array is required", 400);
1392
+ }
1393
+ if (events.length > 100) {
1394
+ return errorResponse("VALIDATION_ERROR", "Maximum 100 events per batch", 400);
1395
+ }
1396
+ const result = await sm.analytics.trackBatch(events, { clientContext });
1397
+ if (!result.success) {
1398
+ return errorResponse(
1399
+ result.error?.code || "BATCH_FAILED",
1400
+ result.error?.message || "Failed to track events",
1401
+ 400
1402
+ );
1403
+ }
1404
+ return successResponse({ tracked: result.data?.tracked || events.length });
1405
+ }
1406
+ // ==================== Track Page View ====================
1407
+ case "page-view":
1408
+ case "pageview": {
1409
+ const { page_url, page_title, referrer, session_id, user_id } = body;
1410
+ if (!page_url) {
1411
+ return errorResponse("VALIDATION_ERROR", "page_url is required", 400);
1412
+ }
1413
+ const result = await sm.analytics.trackPageView(
1414
+ { page_url, page_title, referrer, session_id, user_id },
1415
+ { clientContext }
1416
+ );
1417
+ if (!result.success) {
1418
+ return errorResponse(
1419
+ result.error?.code || "TRACK_FAILED",
1420
+ result.error?.message || "Failed to track page view",
1421
+ 400
1422
+ );
1423
+ }
1424
+ if (config.onEvent) {
1425
+ await config.onEvent({ event_name: "page_viewed", session_id: result.data?.session_id });
1426
+ }
1427
+ return successResponse({ tracked: result.data?.tracked || 1, session_id: result.data?.session_id });
1428
+ }
1429
+ default:
1430
+ return errorResponse("NOT_FOUND", `Unknown endpoint: ${path}`, 404);
1431
+ }
1432
+ } catch (err) {
1433
+ console.error("[ScaleMule Analytics] Error:", err);
1434
+ return successResponse({ tracked: 0 });
1435
+ }
1436
+ };
1437
+ return { POST };
1438
+ }
1439
+
1440
+ // src/server/errors.ts
1441
+ var ScaleMuleError = class extends Error {
1442
+ constructor(code, message, status = 400, details) {
1443
+ super(message);
1444
+ this.code = code;
1445
+ this.status = status;
1446
+ this.details = details;
1447
+ this.name = "ScaleMuleError";
1448
+ }
1449
+ };
1450
+ var CODE_TO_STATUS = {
1451
+ // Auth (401)
1452
+ unauthorized: 401,
1453
+ invalid_credentials: 401,
1454
+ session_expired: 401,
1455
+ token_expired: 401,
1456
+ token_invalid: 401,
1457
+ // Forbidden (403)
1458
+ forbidden: 403,
1459
+ email_not_verified: 403,
1460
+ phone_not_verified: 403,
1461
+ account_locked: 403,
1462
+ account_disabled: 403,
1463
+ mfa_required: 403,
1464
+ csrf_error: 403,
1465
+ origin_not_allowed: 403,
1466
+ // Not found (404)
1467
+ not_found: 404,
1468
+ // Conflict (409)
1469
+ conflict: 409,
1470
+ email_taken: 409,
1471
+ // Rate limiting (429)
1472
+ rate_limited: 429,
1473
+ quota_exceeded: 429,
1474
+ // Validation (400)
1475
+ validation_error: 400,
1476
+ weak_password: 400,
1477
+ invalid_email: 400,
1478
+ invalid_otp: 400,
1479
+ otp_expired: 400,
1480
+ // Server (500)
1481
+ internal_error: 500,
1482
+ // Network — SDK-generated (502/504)
1483
+ network_error: 502,
1484
+ timeout: 504
1485
+ };
1486
+ function errorCodeToStatus(code) {
1487
+ return CODE_TO_STATUS[code.toLowerCase()] || 400;
1488
+ }
1489
+ function unwrap(result) {
1490
+ if (result.error || result.success === false) {
1491
+ const err = result.error;
1492
+ const code = err?.code || "UNKNOWN_ERROR";
1493
+ const status = err?.status || errorCodeToStatus(code);
1494
+ throw new ScaleMuleError(
1495
+ code,
1496
+ err?.message || "An error occurred",
1497
+ status,
1498
+ err?.details
1499
+ );
1500
+ }
1501
+ return result.data;
1502
+ }
1503
+
1504
+ // src/server/handler.ts
1505
+ function apiHandler(handler, options) {
1506
+ return async (request, routeContext) => {
1507
+ try {
1508
+ if (options?.csrf) {
1509
+ const csrfError = validateCSRFToken(request);
1510
+ if (csrfError) {
1511
+ throw new ScaleMuleError("CSRF_ERROR", csrfError, 403);
1512
+ }
1513
+ }
1514
+ let session;
1515
+ if (options?.auth) {
1516
+ session = await requireSession();
1517
+ }
1518
+ const rawParams = routeContext?.params ? await routeContext.params : {};
1519
+ const params = {};
1520
+ for (const [key, val] of Object.entries(rawParams)) {
1521
+ params[key] = Array.isArray(val) ? val.join("/") : val;
1522
+ }
1523
+ const context = {
1524
+ params,
1525
+ searchParams: request.nextUrl.searchParams,
1526
+ session
1527
+ };
1528
+ const result = await handler(request, context);
1529
+ if (result instanceof Response) return result;
1530
+ if (result !== void 0) {
1531
+ return Response.json({ success: true, data: result }, { status: 200 });
1532
+ }
1533
+ return new Response(null, { status: 204 });
1534
+ } catch (error) {
1535
+ if (error instanceof ScaleMuleError) {
1536
+ if (options?.onError) {
1537
+ const custom = options.onError(error);
1538
+ if (custom) return custom;
1539
+ }
1540
+ return Response.json(
1541
+ { success: false, error: { code: error.code, message: error.message } },
1542
+ { status: error.status }
1543
+ );
1544
+ }
1545
+ if (error instanceof Response) return error;
1546
+ console.error("Unhandled API error:", error);
1547
+ return Response.json(
1548
+ { success: false, error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" } },
1549
+ { status: 500 }
1550
+ );
1551
+ }
1552
+ };
1553
+ }
1554
+ function verifyWebhookSignature(payload, signature, secret) {
1555
+ if (!signature.startsWith("sha256=")) {
1556
+ return false;
1557
+ }
1558
+ const providedSig = signature.slice(7);
1559
+ const expectedSig = createHmac("sha256", secret).update(payload).digest("hex");
1560
+ try {
1561
+ return timingSafeEqual(
1562
+ Buffer.from(providedSig, "hex"),
1563
+ Buffer.from(expectedSig, "hex")
1564
+ );
1565
+ } catch {
1566
+ return false;
1567
+ }
1568
+ }
1569
+ function parseWebhookEvent(payload) {
1570
+ return JSON.parse(payload);
1571
+ }
1572
+ async function registerVideoWebhook(url, options) {
1573
+ const sm = createServerClient(options?.clientConfig);
1574
+ const result = await sm.webhooks.create({
1575
+ webhook_name: options?.name || "Video Status Webhook",
1576
+ url,
1577
+ events: options?.events || ["video.ready", "video.failed"]
1578
+ });
1579
+ if (!result.success || !result.data) {
1580
+ throw new Error(result.error?.message || "Failed to register webhook");
1581
+ }
1582
+ return {
1583
+ id: result.data.id,
1584
+ secret: result.data.secret
1585
+ };
1586
+ }
1587
+ function createWebhookRoutes(config = {}) {
1588
+ const POST = async (request) => {
1589
+ const signature = request.headers.get("x-webhook-signature");
1590
+ const body = await request.text();
1591
+ if (config.secret) {
1592
+ if (!signature || !verifyWebhookSignature(body, signature, config.secret)) {
1593
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
1594
+ status: 401,
1595
+ headers: { "Content-Type": "application/json" }
1596
+ });
1597
+ }
1598
+ }
1599
+ try {
1600
+ const event = parseWebhookEvent(body);
1601
+ switch (event.event) {
1602
+ case "video.ready":
1603
+ if (config.onVideoReady) {
1604
+ await config.onVideoReady(event.data);
1605
+ }
1606
+ break;
1607
+ case "video.failed":
1608
+ if (config.onVideoFailed) {
1609
+ await config.onVideoFailed(event.data);
1610
+ }
1611
+ break;
1612
+ case "video.uploaded":
1613
+ if (config.onVideoUploaded) {
1614
+ await config.onVideoUploaded(event.data);
1615
+ }
1616
+ break;
1617
+ case "video.transcoded":
1618
+ if (config.onVideoTranscoded) {
1619
+ await config.onVideoTranscoded(event.data);
1620
+ }
1621
+ break;
1622
+ }
1623
+ if (config.onEvent) {
1624
+ await config.onEvent(event);
1625
+ }
1626
+ return new Response(JSON.stringify({ received: true }), {
1627
+ status: 200,
1628
+ headers: { "Content-Type": "application/json" }
1629
+ });
1630
+ } catch (error) {
1631
+ console.error("Webhook handler error:", error);
1632
+ return new Response(JSON.stringify({ error: "Handler failed" }), {
1633
+ status: 500,
1634
+ headers: { "Content-Type": "application/json" }
1635
+ });
1636
+ }
1637
+ };
1638
+ return { POST };
1639
+ }
1640
+
1641
+ // src/server/webhook-handler.ts
1642
+ function createWebhookHandler(config = {}) {
1643
+ return async (request) => {
1644
+ const signature = request.headers.get("x-webhook-signature");
1645
+ const body = await request.text();
1646
+ if (config.secret) {
1647
+ if (!signature || !verifyWebhookSignature(body, signature, config.secret)) {
1648
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
1649
+ status: 401,
1650
+ headers: { "Content-Type": "application/json" }
1651
+ });
1652
+ }
1653
+ }
1654
+ try {
1655
+ const event = parseWebhookEvent(body);
1656
+ if (config.onEvent && event.event && config.onEvent[event.event]) {
1657
+ await config.onEvent[event.event](event);
1658
+ }
1659
+ return new Response(JSON.stringify({ received: true }), {
1660
+ status: 200,
1661
+ headers: { "Content-Type": "application/json" }
1662
+ });
1663
+ } catch (error) {
1664
+ return new Response(JSON.stringify({ error: "Webhook processing failed" }), {
1665
+ status: 500,
1666
+ headers: { "Content-Type": "application/json" }
1667
+ });
1668
+ }
1669
+ };
1670
+ }
1671
+ function globToRegex(pattern) {
1672
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1673
+ return new RegExp(`^${escaped}$`);
1674
+ }
1675
+ function matchesPattern(pathname, patterns) {
1676
+ return patterns.some((pattern) => {
1677
+ if (pattern === pathname) return true;
1678
+ if (pattern.includes("*") || pattern.includes("?")) {
1679
+ return globToRegex(pattern).test(pathname);
1680
+ }
1681
+ if (pathname.startsWith(pattern + "/")) return true;
1682
+ return false;
1683
+ });
1684
+ }
1685
+ function createAuthMiddleware(config = {}) {
1686
+ const {
1687
+ protectedRoutes = [],
1688
+ publicRoutes = [],
1689
+ authOnlyPublic = [],
1690
+ redirectTo = "/login",
1691
+ redirectAuthenticated,
1692
+ skipValidation = false,
1693
+ onUnauthorized
1694
+ } = config;
1695
+ return async function middleware(request) {
1696
+ const { pathname } = request.nextUrl;
1697
+ if (pathname.startsWith("/api/auth")) {
1698
+ return NextResponse.next();
1699
+ }
1700
+ if (publicRoutes.length > 0 && matchesPattern(pathname, publicRoutes)) {
1701
+ if (redirectAuthenticated && authOnlyPublic.length > 0 && matchesPattern(pathname, authOnlyPublic)) {
1702
+ const session2 = getSessionFromRequest(request);
1703
+ if (session2) {
1704
+ return NextResponse.redirect(new URL(redirectAuthenticated, request.url));
1705
+ }
1706
+ }
1707
+ return NextResponse.next();
1708
+ }
1709
+ const requiresAuth = protectedRoutes.length === 0 || matchesPattern(pathname, protectedRoutes);
1710
+ if (!requiresAuth) {
1711
+ return NextResponse.next();
1712
+ }
1713
+ const session = getSessionFromRequest(request);
1714
+ if (!session) {
1715
+ if (onUnauthorized) {
1716
+ return onUnauthorized(request);
1717
+ }
1718
+ const redirectUrl = new URL(redirectTo, request.url);
1719
+ redirectUrl.searchParams.set("callbackUrl", pathname);
1720
+ return NextResponse.redirect(redirectUrl);
1721
+ }
1722
+ if (!skipValidation) {
1723
+ try {
1724
+ const sm = createServerClient();
1725
+ const result = await sm.auth.me(session.sessionToken);
1726
+ if (!result.success) {
1727
+ const response = NextResponse.redirect(new URL(redirectTo, request.url));
1728
+ response.cookies.delete(SESSION_COOKIE_NAME);
1729
+ response.cookies.delete(USER_ID_COOKIE_NAME);
1730
+ return response;
1731
+ }
1732
+ } catch (error) {
1733
+ console.error("[ScaleMule Middleware] Session validation failed, blocking request:", error);
1734
+ const response = NextResponse.redirect(new URL(redirectTo, request.url));
1735
+ response.cookies.delete(SESSION_COOKIE_NAME);
1736
+ response.cookies.delete(USER_ID_COOKIE_NAME);
1737
+ return response;
1738
+ }
1739
+ }
1740
+ return NextResponse.next();
1741
+ };
1742
+ }
1743
+ function withAuth(config = {}) {
1744
+ const { redirectTo = "/login", onUnauthorized } = config;
1745
+ return function middleware(request) {
1746
+ const session = getSessionFromRequest(request);
1747
+ if (!session) {
1748
+ if (onUnauthorized) {
1749
+ return onUnauthorized(request);
1750
+ }
1751
+ const redirectUrl = new URL(redirectTo, request.url);
1752
+ redirectUrl.searchParams.set("callbackUrl", request.nextUrl.pathname);
1753
+ return NextResponse.redirect(redirectUrl);
1754
+ }
1755
+ return NextResponse.next();
1756
+ };
1757
+ }
1758
+ var OAUTH_STATE_COOKIE_NAME = "sm_oauth_state";
1759
+ function setOAuthState(response, state) {
1760
+ response.cookies.set(OAUTH_STATE_COOKIE_NAME, state, {
1761
+ httpOnly: true,
1762
+ secure: process.env.NODE_ENV === "production",
1763
+ sameSite: "lax",
1764
+ // Lax allows the cookie to be sent on OAuth redirects
1765
+ path: "/",
1766
+ maxAge: 60 * 10
1767
+ // 10 minutes - OAuth flows should complete quickly
1768
+ });
1769
+ return response;
1770
+ }
1771
+ function validateOAuthState(request, callbackState) {
1772
+ const cookieState = request.cookies.get(OAUTH_STATE_COOKIE_NAME)?.value;
1773
+ if (!cookieState) {
1774
+ return "Missing OAuth state cookie - session may have expired";
1775
+ }
1776
+ if (!callbackState) {
1777
+ return "Missing OAuth state in callback";
1778
+ }
1779
+ if (!constantTimeEqual(cookieState, callbackState)) {
1780
+ return "OAuth state mismatch - possible CSRF attack";
1781
+ }
1782
+ return void 0;
1783
+ }
1784
+ async function validateOAuthStateAsync(callbackState) {
1785
+ const cookieStore = await cookies();
1786
+ const cookieState = cookieStore.get(OAUTH_STATE_COOKIE_NAME)?.value;
1787
+ if (!cookieState) {
1788
+ return "Missing OAuth state cookie - session may have expired";
1789
+ }
1790
+ if (!callbackState) {
1791
+ return "Missing OAuth state in callback";
1792
+ }
1793
+ if (!constantTimeEqual(cookieState, callbackState)) {
1794
+ return "OAuth state mismatch - possible CSRF attack";
1795
+ }
1796
+ return void 0;
1797
+ }
1798
+ function clearOAuthState(response) {
1799
+ response.cookies.delete(OAUTH_STATE_COOKIE_NAME);
1800
+ return response;
1801
+ }
1802
+
1803
+ // src/server/secrets.ts
1804
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
1805
+ var secretsCache = {};
1806
+ var globalConfig = {};
1807
+ function configureSecrets(config) {
1808
+ globalConfig = { ...globalConfig, ...config };
1809
+ }
1810
+ async function getAppSecret(key) {
1811
+ const cacheTtl = globalConfig.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1812
+ const noCache = globalConfig.noCache ?? false;
1813
+ if (!noCache) {
1814
+ const cached = secretsCache[key];
1815
+ if (cached && Date.now() - cached.cachedAt < cacheTtl) {
1816
+ return cached.value;
1817
+ }
1818
+ }
1819
+ try {
1820
+ const client = createServerClient();
1821
+ const result = await client.secrets.get(key);
1822
+ if (!result.success) {
1823
+ if (result.error?.code === "SECRET_NOT_FOUND") {
1824
+ return void 0;
1825
+ }
1826
+ console.error(`[ScaleMule Secrets] Failed to fetch ${key}:`, result.error);
1827
+ return void 0;
1828
+ }
1829
+ if (!noCache && result.data) {
1830
+ secretsCache[key] = {
1831
+ value: result.data.value,
1832
+ version: result.data.version,
1833
+ cachedAt: Date.now()
1834
+ };
1835
+ }
1836
+ return result.data?.value;
1837
+ } catch (error) {
1838
+ console.error(`[ScaleMule Secrets] Error fetching ${key}:`, error);
1839
+ return void 0;
1840
+ }
1841
+ }
1842
+ async function requireAppSecret(key) {
1843
+ const value = await getAppSecret(key);
1844
+ if (value === void 0) {
1845
+ throw new Error(
1846
+ `Required secret '${key}' not found in ScaleMule vault. Configure it in the ScaleMule dashboard or use the SDK: scalemule.secrets.set('${key}', value)`
1847
+ );
1848
+ }
1849
+ return value;
1850
+ }
1851
+ async function getAppSecretOrDefault(key, fallback) {
1852
+ const value = await getAppSecret(key);
1853
+ return value ?? fallback;
1854
+ }
1855
+ function invalidateSecretCache(key) {
1856
+ if (key) {
1857
+ delete secretsCache[key];
1858
+ } else {
1859
+ Object.keys(secretsCache).forEach((k) => delete secretsCache[k]);
1860
+ }
1861
+ }
1862
+ async function prefetchSecrets(keys) {
1863
+ await Promise.all(keys.map((key) => getAppSecret(key)));
1864
+ }
1865
+
1866
+ // src/server/bundles.ts
1867
+ var DEFAULT_CACHE_TTL_MS2 = 5 * 60 * 1e3;
1868
+ var bundlesCache = {};
1869
+ var globalConfig2 = {};
1870
+ function configureBundles(config) {
1871
+ globalConfig2 = { ...globalConfig2, ...config };
1872
+ }
1873
+ async function getBundle(key, resolve = true) {
1874
+ const cacheTtl = globalConfig2.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS2;
1875
+ const noCache = globalConfig2.noCache ?? false;
1876
+ if (!noCache) {
1877
+ const cached = bundlesCache[key];
1878
+ if (cached && Date.now() - cached.cachedAt < cacheTtl) {
1879
+ return cached.data;
1880
+ }
1881
+ }
1882
+ try {
1883
+ const client = createServerClient();
1884
+ const result = await client.bundles.get(key, resolve);
1885
+ if (!result.success) {
1886
+ if (result.error?.code === "BUNDLE_NOT_FOUND") {
1887
+ return void 0;
1888
+ }
1889
+ console.error(`[ScaleMule Bundles] Failed to fetch ${key}:`, result.error);
1890
+ return void 0;
1891
+ }
1892
+ if (!noCache && result.data) {
1893
+ bundlesCache[key] = {
1894
+ type: result.data.type,
1895
+ data: result.data.data,
1896
+ version: result.data.version,
1897
+ inheritsFrom: result.data.inherits_from,
1898
+ cachedAt: Date.now()
1899
+ };
1900
+ }
1901
+ return result.data?.data;
1902
+ } catch (error) {
1903
+ console.error(`[ScaleMule Bundles] Error fetching ${key}:`, error);
1904
+ return void 0;
1905
+ }
1906
+ }
1907
+ async function requireBundle(key, resolve = true) {
1908
+ const value = await getBundle(key, resolve);
1909
+ if (value === void 0) {
1910
+ throw new Error(
1911
+ `Required bundle '${key}' not found in ScaleMule vault. Configure it in the ScaleMule dashboard`
1912
+ );
1913
+ }
1914
+ return value;
1915
+ }
1916
+ async function getMySqlBundle(key) {
1917
+ const bundle = await getBundle(key);
1918
+ if (!bundle) return void 0;
1919
+ const { host, port, username, password, database, ssl_mode } = bundle;
1920
+ const encodedPassword = encodeURIComponent(password);
1921
+ let connectionUrl = `mysql://${username}:${encodedPassword}@${host}:${port}/${database}`;
1922
+ if (ssl_mode) {
1923
+ connectionUrl += `?ssl_mode=${ssl_mode}`;
1924
+ }
1925
+ return { ...bundle, connectionUrl };
1926
+ }
1927
+ async function getPostgresBundle(key) {
1928
+ const bundle = await getBundle(key);
1929
+ if (!bundle) return void 0;
1930
+ const { host, port, username, password, database, ssl_mode } = bundle;
1931
+ const encodedPassword = encodeURIComponent(password);
1932
+ let connectionUrl = `postgresql://${username}:${encodedPassword}@${host}:${port}/${database}`;
1933
+ if (ssl_mode) {
1934
+ connectionUrl += `?sslmode=${ssl_mode}`;
1935
+ }
1936
+ return { ...bundle, connectionUrl };
1937
+ }
1938
+ async function getRedisBundle(key) {
1939
+ const bundle = await getBundle(key);
1940
+ if (!bundle) return void 0;
1941
+ const { host, port, password, database, ssl } = bundle;
1942
+ let connectionUrl = ssl ? "rediss://" : "redis://";
1943
+ if (password) {
1944
+ connectionUrl += `:${encodeURIComponent(password)}@`;
1945
+ }
1946
+ connectionUrl += `${host}:${port}`;
1947
+ if (database !== void 0) {
1948
+ connectionUrl += `/${database}`;
1949
+ }
1950
+ return { ...bundle, connectionUrl };
1951
+ }
1952
+ async function getS3Bundle(key) {
1953
+ return getBundle(key);
1954
+ }
1955
+ async function getOAuthBundle(key) {
1956
+ return getBundle(key);
1957
+ }
1958
+ async function getSmtpBundle(key) {
1959
+ return getBundle(key);
1960
+ }
1961
+ function invalidateBundleCache(key) {
1962
+ if (key) {
1963
+ delete bundlesCache[key];
1964
+ } else {
1965
+ Object.keys(bundlesCache).forEach((k) => delete bundlesCache[k]);
1966
+ }
1967
+ }
1968
+ async function prefetchBundles(keys) {
1969
+ await Promise.all(keys.map((key) => getBundle(key)));
1970
+ }
1971
+
1972
+ export { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, OAUTH_STATE_COOKIE_NAME, SESSION_COOKIE_NAME, ScaleMuleError, ScaleMuleServer, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createServerClient, createWebhookHandler, createWebhookRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, parseWebhookEvent, prefetchBundles, prefetchSecrets, registerVideoWebhook, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, verifyWebhookSignature, withAuth, withCSRFProtection, withCSRFToken, withSession };