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