@inneropen/marvin-sdk 2.0.0-next.14 → 2.0.0-next.16
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 +136 -11
- 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-BVHNN2PW.js → chunk-WJC4X7TB.js} +274 -8
- package/dist/index.cjs +356 -11
- 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 -42
- 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,7 +463,8 @@ var init_HttpClient = __esm({
|
|
|
282
463
|
}
|
|
283
464
|
const data = await response.json();
|
|
284
465
|
if (this.debug) {
|
|
285
|
-
|
|
466
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
467
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
286
468
|
}
|
|
287
469
|
return data;
|
|
288
470
|
} catch (error) {
|
|
@@ -291,6 +473,17 @@ var init_HttpClient = __esm({
|
|
|
291
473
|
throw error;
|
|
292
474
|
}
|
|
293
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
|
+
}
|
|
294
487
|
if (error.name === "AbortError") {
|
|
295
488
|
throw new MarvinNetworkError(
|
|
296
489
|
`Request timeout after ${this.timeout}ms`,
|
|
@@ -371,10 +564,27 @@ var init_AuthStrategy = __esm({
|
|
|
371
564
|
}
|
|
372
565
|
sessionToken;
|
|
373
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
|
+
}
|
|
374
581
|
injectAuth(headers) {
|
|
375
582
|
if (this.sessionToken) {
|
|
376
583
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
377
584
|
}
|
|
585
|
+
if (this.csrfToken) {
|
|
586
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
587
|
+
}
|
|
378
588
|
}
|
|
379
589
|
async handleUnauthorized() {
|
|
380
590
|
if (this.onUnauthorized) {
|
|
@@ -382,7 +592,9 @@ var init_AuthStrategy = __esm({
|
|
|
382
592
|
}
|
|
383
593
|
}
|
|
384
594
|
getDescription() {
|
|
385
|
-
|
|
595
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
596
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
597
|
+
return auth + csrf;
|
|
386
598
|
}
|
|
387
599
|
};
|
|
388
600
|
NoAuth = class {
|
|
@@ -442,11 +654,17 @@ var init_cache = __esm({
|
|
|
442
654
|
"src/core/cache.ts"() {
|
|
443
655
|
"use strict";
|
|
444
656
|
MarvinCache = class {
|
|
445
|
-
constructor(duration = 5 * 60 * 1e3) {
|
|
657
|
+
constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
|
|
446
658
|
this.duration = duration;
|
|
659
|
+
this.maxEntries = maxEntries;
|
|
447
660
|
}
|
|
448
661
|
duration;
|
|
662
|
+
maxEntries;
|
|
449
663
|
cache = /* @__PURE__ */ new Map();
|
|
664
|
+
/**
|
|
665
|
+
* Get cached data by key
|
|
666
|
+
* Returns undefined if not found or expired
|
|
667
|
+
*/
|
|
450
668
|
get(key) {
|
|
451
669
|
const entry = this.cache.get(key);
|
|
452
670
|
if (!entry) return void 0;
|
|
@@ -457,25 +675,149 @@ var init_cache = __esm({
|
|
|
457
675
|
}
|
|
458
676
|
return entry.data;
|
|
459
677
|
}
|
|
460
|
-
|
|
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
|
+
}
|
|
461
688
|
this.cache.set(key, {
|
|
462
689
|
data,
|
|
463
|
-
timestamp: Date.now()
|
|
690
|
+
timestamp: Date.now(),
|
|
691
|
+
tags
|
|
464
692
|
});
|
|
465
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Check if key exists and is not expired
|
|
696
|
+
*/
|
|
466
697
|
has(key) {
|
|
467
698
|
return this.get(key) !== void 0;
|
|
468
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Clear all cached entries
|
|
702
|
+
*/
|
|
469
703
|
clear() {
|
|
470
704
|
this.cache.clear();
|
|
471
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Delete a specific cache entry
|
|
708
|
+
*/
|
|
472
709
|
delete(key) {
|
|
473
710
|
this.cache.delete(key);
|
|
474
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
|
+
}
|
|
475
773
|
};
|
|
476
774
|
}
|
|
477
775
|
});
|
|
478
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
|
+
|
|
479
821
|
// src/core/index.ts
|
|
480
822
|
var init_core = __esm({
|
|
481
823
|
"src/core/index.ts"() {
|
|
@@ -485,6 +827,7 @@ var init_core = __esm({
|
|
|
485
827
|
init_errors();
|
|
486
828
|
init_pagination();
|
|
487
829
|
init_cache();
|
|
830
|
+
init_validation();
|
|
488
831
|
}
|
|
489
832
|
});
|
|
490
833
|
|
|
@@ -1372,12 +1715,14 @@ var AuthClient = class extends HttpClient {
|
|
|
1372
1715
|
* Returns the created user object
|
|
1373
1716
|
*/
|
|
1374
1717
|
async register(data) {
|
|
1718
|
+
validateEmail(data.email, "Email");
|
|
1375
1719
|
return this.post("/api/users/register", data);
|
|
1376
1720
|
}
|
|
1377
1721
|
/**
|
|
1378
1722
|
* Request password reset (sends email with token)
|
|
1379
1723
|
*/
|
|
1380
1724
|
async forgotPassword(data) {
|
|
1725
|
+
validateEmail(data.email, "Email");
|
|
1381
1726
|
return this.post("/api/users/forgot-password", data);
|
|
1382
1727
|
}
|
|
1383
1728
|
/**
|