@inneropen/marvin-sdk 2.0.0-next.13 → 2.0.0-next.15
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/README.md +59 -10
- package/dist/{HttpClient-BX5Gqd0w.d.cts → HttpClient-CdF3IHM1.d.cts} +41 -2
- package/dist/{HttpClient-BX5Gqd0w.d.ts → HttpClient-CdF3IHM1.d.ts} +41 -2
- package/dist/chunk-GJNOYXUQ.js +980 -0
- package/dist/{chunk-DYC2BEA4.js → chunk-WJC4X7TB.js} +274 -9
- package/dist/index.cjs +356 -12
- package/dist/index.d.cts +20 -610
- package/dist/index.d.ts +20 -610
- package/dist/index.js +21 -880
- package/dist/marvin-Bx5k6tsF.d.cts +654 -0
- package/dist/marvin-g7dayc83.d.ts +654 -0
- package/dist/platform.cjs +396 -43
- package/dist/platform.d.cts +124 -15
- package/dist/platform.d.ts +124 -15
- package/dist/platform.js +251 -37
- package/dist/publish.cjs +1487 -0
- package/dist/publish.d.cts +49 -0
- package/dist/publish.d.ts +49 -0
- package/dist/publish.js +75 -0
- package/dist/types/index.d.cts +9 -1
- package/dist/types/index.d.ts +9 -1
- package/package.json +11 -6
package/dist/index.cjs
CHANGED
|
@@ -30,19 +30,97 @@ function validateConfig(config) {
|
|
|
30
30
|
const receivedKeys = Object.keys(config).join(", ");
|
|
31
31
|
if (!config.apiUrl) {
|
|
32
32
|
throw new Error(
|
|
33
|
-
`Missing required config
|
|
33
|
+
`Missing required config: 'apiUrl'
|
|
34
|
+
|
|
35
|
+
The API URL is the base URL of your Marvin server.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
- Production: https://marvin.example.com
|
|
39
|
+
- Development: http://localhost:8080
|
|
40
|
+
|
|
41
|
+
Solutions:
|
|
42
|
+
1. Set environment variable: MARVIN_API_URL=https://marvin.example.com
|
|
43
|
+
2. Pass directly: createMarvinClient({ apiUrl: 'https://marvin.example.com' })
|
|
44
|
+
|
|
45
|
+
Received config keys: ${receivedKeys || "(none)"}`
|
|
34
46
|
);
|
|
35
47
|
}
|
|
36
48
|
if (!config.siteClientToken) {
|
|
37
49
|
throw new Error(
|
|
38
|
-
`Missing required config
|
|
50
|
+
`Missing required config: 'siteClientToken'
|
|
51
|
+
|
|
52
|
+
The site client token provides read-only access to published content.
|
|
53
|
+
|
|
54
|
+
How to get it:
|
|
55
|
+
1. Log into Marvin admin panel
|
|
56
|
+
2. Navigate to: Settings \u2192 API Clients
|
|
57
|
+
3. Copy the token for your workspace
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
- site_client_abc123xyz
|
|
61
|
+
- sc_1234567890abcdef
|
|
62
|
+
|
|
63
|
+
Solutions:
|
|
64
|
+
1. Set environment variable: MARVIN_SITE_CLIENT_TOKEN=site_client_abc123
|
|
65
|
+
2. Pass directly: createMarvinClient({ siteClientToken: 'site_client_abc123' })
|
|
66
|
+
|
|
67
|
+
Received config keys: ${receivedKeys}`
|
|
39
68
|
);
|
|
40
69
|
}
|
|
41
70
|
if (!config.workspaceSlug) {
|
|
42
71
|
throw new Error(
|
|
43
|
-
`Missing required config
|
|
72
|
+
`Missing required config: 'workspaceSlug'
|
|
73
|
+
|
|
74
|
+
The workspace slug identifies which workspace to access.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
- my-blog
|
|
78
|
+
- company-website
|
|
79
|
+
- acme-corp
|
|
80
|
+
|
|
81
|
+
How to find it:
|
|
82
|
+
1. Check your workspace URL: https://marvin.example.com/workspace/[slug]
|
|
83
|
+
2. Or find it in: Settings \u2192 General \u2192 Workspace Slug
|
|
84
|
+
|
|
85
|
+
Solutions:
|
|
86
|
+
1. Set environment variable: MARVIN_WORKSPACE_SLUG=my-blog
|
|
87
|
+
2. Pass directly: createMarvinClient({ workspaceSlug: 'my-blog' })
|
|
88
|
+
|
|
89
|
+
Received config keys: ${receivedKeys}`
|
|
44
90
|
);
|
|
45
91
|
}
|
|
92
|
+
if (config.apiUrl) {
|
|
93
|
+
try {
|
|
94
|
+
new URL(config.apiUrl);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid 'apiUrl' format: "${config.apiUrl}"
|
|
98
|
+
|
|
99
|
+
The API URL must be a valid HTTP or HTTPS URL.
|
|
100
|
+
|
|
101
|
+
Valid examples:
|
|
102
|
+
\u2713 https://marvin.example.com
|
|
103
|
+
\u2713 http://localhost:8080
|
|
104
|
+
\u2713 https://api.marvin.io
|
|
105
|
+
|
|
106
|
+
Invalid examples:
|
|
107
|
+
\u2717 marvin.example.com (missing protocol)
|
|
108
|
+
\u2717 ftp://marvin.example.com (wrong protocol)
|
|
109
|
+
\u2717 /api (not a full URL)`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const url = new URL(config.apiUrl);
|
|
113
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Invalid 'apiUrl' protocol: "${url.protocol}"
|
|
116
|
+
|
|
117
|
+
Only HTTP and HTTPS protocols are supported.
|
|
118
|
+
|
|
119
|
+
Your URL: ${config.apiUrl}
|
|
120
|
+
Change to: ${config.apiUrl.replace(url.protocol, "https:")}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
46
124
|
}
|
|
47
125
|
function createConfigFromEnv(overrides) {
|
|
48
126
|
return {
|
|
@@ -170,15 +248,46 @@ var init_HttpClient = __esm({
|
|
|
170
248
|
credentials;
|
|
171
249
|
timeout;
|
|
172
250
|
debug;
|
|
251
|
+
retryConfig;
|
|
173
252
|
logger;
|
|
174
253
|
constructor(config) {
|
|
175
254
|
this.baseUrl = config.baseUrl;
|
|
176
255
|
this.auth = config.auth;
|
|
177
256
|
this.defaultHeaders = config.defaultHeaders || {};
|
|
178
257
|
this.credentials = config.credentials || "same-origin";
|
|
179
|
-
|
|
258
|
+
const MAX_TIMEOUT = 12e4;
|
|
259
|
+
this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
|
|
180
260
|
this.debug = config.debug ?? false;
|
|
181
261
|
this.logger = config.logger ?? console;
|
|
262
|
+
this.retryConfig = {
|
|
263
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
264
|
+
initialDelay: config.retry?.initialDelay ?? 1e3,
|
|
265
|
+
maxDelay: config.retry?.maxDelay ?? 1e4,
|
|
266
|
+
retryableStatuses: config.retry?.retryableStatuses ?? [408, 429, 500, 502, 503, 504]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Validate path parameter to prevent path traversal attacks
|
|
271
|
+
*/
|
|
272
|
+
validatePathParam(param, paramName = "parameter") {
|
|
273
|
+
if (typeof param === "number") {
|
|
274
|
+
return String(param);
|
|
275
|
+
}
|
|
276
|
+
if (!param || typeof param !== "string") {
|
|
277
|
+
throw new MarvinValidationError(`Invalid ${paramName}: must be a non-empty string or number`);
|
|
278
|
+
}
|
|
279
|
+
if (param.includes("..") || param.includes("/") || param.includes("\\")) {
|
|
280
|
+
throw new MarvinValidationError(
|
|
281
|
+
`Invalid ${paramName}: cannot contain path traversal characters (.. / \\)`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (param.includes("\0")) {
|
|
285
|
+
throw new MarvinValidationError(`Invalid ${paramName}: cannot contain null bytes`);
|
|
286
|
+
}
|
|
287
|
+
if (param.length > 255) {
|
|
288
|
+
throw new MarvinValidationError(`Invalid ${paramName}: exceeds maximum length of 255 characters`);
|
|
289
|
+
}
|
|
290
|
+
return param;
|
|
182
291
|
}
|
|
183
292
|
/**
|
|
184
293
|
* Build full URL from endpoint
|
|
@@ -212,9 +321,69 @@ var init_HttpClient = __esm({
|
|
|
212
321
|
return filtered.length > 0 ? `?${filtered.join("&")}` : "";
|
|
213
322
|
}
|
|
214
323
|
/**
|
|
215
|
-
*
|
|
324
|
+
* Sanitize data for logging by redacting sensitive fields
|
|
325
|
+
*/
|
|
326
|
+
sanitizeForLogging(data) {
|
|
327
|
+
if (!data || typeof data !== "object") {
|
|
328
|
+
return data;
|
|
329
|
+
}
|
|
330
|
+
const sensitiveFields = [
|
|
331
|
+
"token",
|
|
332
|
+
"access_token",
|
|
333
|
+
"refresh_token",
|
|
334
|
+
"password",
|
|
335
|
+
"secret",
|
|
336
|
+
"apiKey",
|
|
337
|
+
"api_key",
|
|
338
|
+
"bearer",
|
|
339
|
+
"authorization",
|
|
340
|
+
"csrf_token",
|
|
341
|
+
"session",
|
|
342
|
+
"cookie"
|
|
343
|
+
];
|
|
344
|
+
const sanitize = (obj) => {
|
|
345
|
+
const sanitized = {};
|
|
346
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
347
|
+
const keyLower = key.toLowerCase();
|
|
348
|
+
const isSensitive = sensitiveFields.some((field) => keyLower.includes(field));
|
|
349
|
+
if (isSensitive) {
|
|
350
|
+
sanitized[key] = "[REDACTED]";
|
|
351
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
352
|
+
sanitized[key] = sanitize(value);
|
|
353
|
+
} else if (Array.isArray(value)) {
|
|
354
|
+
sanitized[key] = value.map(
|
|
355
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
sanitized[key] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return sanitized;
|
|
362
|
+
};
|
|
363
|
+
if (Array.isArray(data)) {
|
|
364
|
+
return data.map(
|
|
365
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return sanitize(data);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Calculate delay for retry with exponential backoff
|
|
216
372
|
*/
|
|
217
|
-
|
|
373
|
+
calculateRetryDelay(attempt) {
|
|
374
|
+
const delay = this.retryConfig.initialDelay * Math.pow(2, attempt);
|
|
375
|
+
return Math.min(delay, this.retryConfig.maxDelay);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Sleep for specified milliseconds
|
|
379
|
+
*/
|
|
380
|
+
sleep(ms) {
|
|
381
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Perform HTTP request with automatic retry on transient failures
|
|
385
|
+
*/
|
|
386
|
+
async request(method, endpoint, options, retryAttempt = 0) {
|
|
218
387
|
const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
|
|
219
388
|
const isFormData = options?.body instanceof FormData;
|
|
220
389
|
const headers = this.buildHeaders(options?.headers, isFormData);
|
|
@@ -250,6 +419,18 @@ var init_HttpClient = __esm({
|
|
|
250
419
|
errorText ? `Body: ${errorText}` : ""
|
|
251
420
|
);
|
|
252
421
|
}
|
|
422
|
+
const isRetryable = this.retryConfig.retryableStatuses.includes(response.status);
|
|
423
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
424
|
+
if (isRetryable && canRetry) {
|
|
425
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
426
|
+
if (this.debug) {
|
|
427
|
+
this.logger.warn(
|
|
428
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (status: ${response.status})`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
await this.sleep(delay);
|
|
432
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
433
|
+
}
|
|
253
434
|
if (response.status === 404) {
|
|
254
435
|
throw new MarvinNotFoundError(
|
|
255
436
|
`Resource not found: ${endpoint}`,
|
|
@@ -282,8 +463,8 @@ var init_HttpClient = __esm({
|
|
|
282
463
|
}
|
|
283
464
|
const data = await response.json();
|
|
284
465
|
if (this.debug) {
|
|
285
|
-
const
|
|
286
|
-
this.logger.log(
|
|
466
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
467
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
287
468
|
}
|
|
288
469
|
return data;
|
|
289
470
|
} catch (error) {
|
|
@@ -292,6 +473,17 @@ var init_HttpClient = __esm({
|
|
|
292
473
|
throw error;
|
|
293
474
|
}
|
|
294
475
|
if (error instanceof Error) {
|
|
476
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
477
|
+
if (canRetry && error.name !== "AbortError") {
|
|
478
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
479
|
+
if (this.debug) {
|
|
480
|
+
this.logger.warn(
|
|
481
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (network error)`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
await this.sleep(delay);
|
|
485
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
486
|
+
}
|
|
295
487
|
if (error.name === "AbortError") {
|
|
296
488
|
throw new MarvinNetworkError(
|
|
297
489
|
`Request timeout after ${this.timeout}ms`,
|
|
@@ -372,10 +564,27 @@ var init_AuthStrategy = __esm({
|
|
|
372
564
|
}
|
|
373
565
|
sessionToken;
|
|
374
566
|
onUnauthorized;
|
|
567
|
+
csrfToken;
|
|
568
|
+
/**
|
|
569
|
+
* Set CSRF token for cross-site request forgery protection
|
|
570
|
+
* The token should be obtained from the server after login
|
|
571
|
+
*/
|
|
572
|
+
setCsrfToken(token) {
|
|
573
|
+
this.csrfToken = token;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get current CSRF token
|
|
577
|
+
*/
|
|
578
|
+
getCsrfToken() {
|
|
579
|
+
return this.csrfToken;
|
|
580
|
+
}
|
|
375
581
|
injectAuth(headers) {
|
|
376
582
|
if (this.sessionToken) {
|
|
377
583
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
378
584
|
}
|
|
585
|
+
if (this.csrfToken) {
|
|
586
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
587
|
+
}
|
|
379
588
|
}
|
|
380
589
|
async handleUnauthorized() {
|
|
381
590
|
if (this.onUnauthorized) {
|
|
@@ -383,7 +592,9 @@ var init_AuthStrategy = __esm({
|
|
|
383
592
|
}
|
|
384
593
|
}
|
|
385
594
|
getDescription() {
|
|
386
|
-
|
|
595
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
596
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
597
|
+
return auth + csrf;
|
|
387
598
|
}
|
|
388
599
|
};
|
|
389
600
|
NoAuth = class {
|
|
@@ -443,11 +654,17 @@ var init_cache = __esm({
|
|
|
443
654
|
"src/core/cache.ts"() {
|
|
444
655
|
"use strict";
|
|
445
656
|
MarvinCache = class {
|
|
446
|
-
constructor(duration = 5 * 60 * 1e3) {
|
|
657
|
+
constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
|
|
447
658
|
this.duration = duration;
|
|
659
|
+
this.maxEntries = maxEntries;
|
|
448
660
|
}
|
|
449
661
|
duration;
|
|
662
|
+
maxEntries;
|
|
450
663
|
cache = /* @__PURE__ */ new Map();
|
|
664
|
+
/**
|
|
665
|
+
* Get cached data by key
|
|
666
|
+
* Returns undefined if not found or expired
|
|
667
|
+
*/
|
|
451
668
|
get(key) {
|
|
452
669
|
const entry = this.cache.get(key);
|
|
453
670
|
if (!entry) return void 0;
|
|
@@ -458,25 +675,149 @@ var init_cache = __esm({
|
|
|
458
675
|
}
|
|
459
676
|
return entry.data;
|
|
460
677
|
}
|
|
461
|
-
|
|
678
|
+
/**
|
|
679
|
+
* Set cached data with optional tags for invalidation
|
|
680
|
+
*/
|
|
681
|
+
set(key, data, tags) {
|
|
682
|
+
if (this.cache.size >= this.maxEntries) {
|
|
683
|
+
const oldestKey = this.cache.keys().next().value;
|
|
684
|
+
if (oldestKey) {
|
|
685
|
+
this.cache.delete(oldestKey);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
462
688
|
this.cache.set(key, {
|
|
463
689
|
data,
|
|
464
|
-
timestamp: Date.now()
|
|
690
|
+
timestamp: Date.now(),
|
|
691
|
+
tags
|
|
465
692
|
});
|
|
466
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Check if key exists and is not expired
|
|
696
|
+
*/
|
|
467
697
|
has(key) {
|
|
468
698
|
return this.get(key) !== void 0;
|
|
469
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Clear all cached entries
|
|
702
|
+
*/
|
|
470
703
|
clear() {
|
|
471
704
|
this.cache.clear();
|
|
472
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Delete a specific cache entry
|
|
708
|
+
*/
|
|
473
709
|
delete(key) {
|
|
474
710
|
this.cache.delete(key);
|
|
475
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Invalidate all cache entries with a specific tag
|
|
714
|
+
* Useful for invalidating related data after mutations
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```typescript
|
|
718
|
+
* // Cache entries with tags
|
|
719
|
+
* cache.set('entry:1', data, ['entries', 'entry:1']);
|
|
720
|
+
* cache.set('entries:list', data, ['entries']);
|
|
721
|
+
*
|
|
722
|
+
* // Invalidate all entries
|
|
723
|
+
* cache.invalidateByTag('entries');
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
invalidateByTag(tag) {
|
|
727
|
+
let count = 0;
|
|
728
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
729
|
+
if (entry.tags?.includes(tag)) {
|
|
730
|
+
this.cache.delete(key);
|
|
731
|
+
count++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return count;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Get cache statistics
|
|
738
|
+
*/
|
|
739
|
+
getStats() {
|
|
740
|
+
let oldest = null;
|
|
741
|
+
let newest = null;
|
|
742
|
+
for (const entry of this.cache.values()) {
|
|
743
|
+
if (oldest === null || entry.timestamp < oldest) {
|
|
744
|
+
oldest = entry.timestamp;
|
|
745
|
+
}
|
|
746
|
+
if (newest === null || entry.timestamp > newest) {
|
|
747
|
+
newest = entry.timestamp;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
size: this.cache.size,
|
|
752
|
+
maxEntries: this.maxEntries,
|
|
753
|
+
oldestEntry: oldest,
|
|
754
|
+
newestEntry: newest
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Remove expired entries (garbage collection)
|
|
759
|
+
* Returns number of entries removed
|
|
760
|
+
*/
|
|
761
|
+
prune() {
|
|
762
|
+
let count = 0;
|
|
763
|
+
const now = Date.now();
|
|
764
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
765
|
+
const age = now - entry.timestamp;
|
|
766
|
+
if (age > this.duration) {
|
|
767
|
+
this.cache.delete(key);
|
|
768
|
+
count++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return count;
|
|
772
|
+
}
|
|
476
773
|
};
|
|
477
774
|
}
|
|
478
775
|
});
|
|
479
776
|
|
|
777
|
+
// src/core/validation.ts
|
|
778
|
+
function validateEmail(email, fieldName = "Email") {
|
|
779
|
+
if (!email || typeof email !== "string") {
|
|
780
|
+
throw new MarvinValidationError(`${fieldName} is required and must be a string`);
|
|
781
|
+
}
|
|
782
|
+
const trimmed = email.trim();
|
|
783
|
+
if (trimmed.length === 0) {
|
|
784
|
+
throw new MarvinValidationError(`${fieldName} cannot be empty`);
|
|
785
|
+
}
|
|
786
|
+
if (trimmed.length < 5) {
|
|
787
|
+
throw new MarvinValidationError(`${fieldName} is too short`);
|
|
788
|
+
}
|
|
789
|
+
if (trimmed.length > 254) {
|
|
790
|
+
throw new MarvinValidationError(`${fieldName} exceeds maximum length of 254 characters`);
|
|
791
|
+
}
|
|
792
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
793
|
+
throw new MarvinValidationError(
|
|
794
|
+
`${fieldName} format is invalid. Expected format: user@example.com`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const [localPart, domain] = trimmed.split("@");
|
|
798
|
+
if (localPart.length > 64) {
|
|
799
|
+
throw new MarvinValidationError(`${fieldName} local part exceeds 64 characters`);
|
|
800
|
+
}
|
|
801
|
+
if (domain.length > 253) {
|
|
802
|
+
throw new MarvinValidationError(`${fieldName} domain exceeds 253 characters`);
|
|
803
|
+
}
|
|
804
|
+
if (trimmed.includes("..")) {
|
|
805
|
+
throw new MarvinValidationError(`${fieldName} cannot contain consecutive dots`);
|
|
806
|
+
}
|
|
807
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
808
|
+
throw new MarvinValidationError(`${fieldName} local part cannot start or end with a dot`);
|
|
809
|
+
}
|
|
810
|
+
return trimmed;
|
|
811
|
+
}
|
|
812
|
+
var EMAIL_REGEX;
|
|
813
|
+
var init_validation = __esm({
|
|
814
|
+
"src/core/validation.ts"() {
|
|
815
|
+
"use strict";
|
|
816
|
+
init_errors();
|
|
817
|
+
EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
480
821
|
// src/core/index.ts
|
|
481
822
|
var init_core = __esm({
|
|
482
823
|
"src/core/index.ts"() {
|
|
@@ -486,6 +827,7 @@ var init_core = __esm({
|
|
|
486
827
|
init_errors();
|
|
487
828
|
init_pagination();
|
|
488
829
|
init_cache();
|
|
830
|
+
init_validation();
|
|
489
831
|
}
|
|
490
832
|
});
|
|
491
833
|
|
|
@@ -1373,12 +1715,14 @@ var AuthClient = class extends HttpClient {
|
|
|
1373
1715
|
* Returns the created user object
|
|
1374
1716
|
*/
|
|
1375
1717
|
async register(data) {
|
|
1718
|
+
validateEmail(data.email, "Email");
|
|
1376
1719
|
return this.post("/api/users/register", data);
|
|
1377
1720
|
}
|
|
1378
1721
|
/**
|
|
1379
1722
|
* Request password reset (sends email with token)
|
|
1380
1723
|
*/
|
|
1381
1724
|
async forgotPassword(data) {
|
|
1725
|
+
validateEmail(data.email, "Email");
|
|
1382
1726
|
return this.post("/api/users/forgot-password", data);
|
|
1383
1727
|
}
|
|
1384
1728
|
/**
|