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