@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
|
@@ -127,15 +127,46 @@ var init_HttpClient = __esm({
|
|
|
127
127
|
credentials;
|
|
128
128
|
timeout;
|
|
129
129
|
debug;
|
|
130
|
+
retryConfig;
|
|
130
131
|
logger;
|
|
131
132
|
constructor(config) {
|
|
132
133
|
this.baseUrl = config.baseUrl;
|
|
133
134
|
this.auth = config.auth;
|
|
134
135
|
this.defaultHeaders = config.defaultHeaders || {};
|
|
135
136
|
this.credentials = config.credentials || "same-origin";
|
|
136
|
-
|
|
137
|
+
const MAX_TIMEOUT = 12e4;
|
|
138
|
+
this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
|
|
137
139
|
this.debug = config.debug ?? false;
|
|
138
140
|
this.logger = config.logger ?? console;
|
|
141
|
+
this.retryConfig = {
|
|
142
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
143
|
+
initialDelay: config.retry?.initialDelay ?? 1e3,
|
|
144
|
+
maxDelay: config.retry?.maxDelay ?? 1e4,
|
|
145
|
+
retryableStatuses: config.retry?.retryableStatuses ?? [408, 429, 500, 502, 503, 504]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate path parameter to prevent path traversal attacks
|
|
150
|
+
*/
|
|
151
|
+
validatePathParam(param, paramName = "parameter") {
|
|
152
|
+
if (typeof param === "number") {
|
|
153
|
+
return String(param);
|
|
154
|
+
}
|
|
155
|
+
if (!param || typeof param !== "string") {
|
|
156
|
+
throw new MarvinValidationError(`Invalid ${paramName}: must be a non-empty string or number`);
|
|
157
|
+
}
|
|
158
|
+
if (param.includes("..") || param.includes("/") || param.includes("\\")) {
|
|
159
|
+
throw new MarvinValidationError(
|
|
160
|
+
`Invalid ${paramName}: cannot contain path traversal characters (.. / \\)`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (param.includes("\0")) {
|
|
164
|
+
throw new MarvinValidationError(`Invalid ${paramName}: cannot contain null bytes`);
|
|
165
|
+
}
|
|
166
|
+
if (param.length > 255) {
|
|
167
|
+
throw new MarvinValidationError(`Invalid ${paramName}: exceeds maximum length of 255 characters`);
|
|
168
|
+
}
|
|
169
|
+
return param;
|
|
139
170
|
}
|
|
140
171
|
/**
|
|
141
172
|
* Build full URL from endpoint
|
|
@@ -169,9 +200,69 @@ var init_HttpClient = __esm({
|
|
|
169
200
|
return filtered.length > 0 ? `?${filtered.join("&")}` : "";
|
|
170
201
|
}
|
|
171
202
|
/**
|
|
172
|
-
*
|
|
203
|
+
* Sanitize data for logging by redacting sensitive fields
|
|
204
|
+
*/
|
|
205
|
+
sanitizeForLogging(data) {
|
|
206
|
+
if (!data || typeof data !== "object") {
|
|
207
|
+
return data;
|
|
208
|
+
}
|
|
209
|
+
const sensitiveFields = [
|
|
210
|
+
"token",
|
|
211
|
+
"access_token",
|
|
212
|
+
"refresh_token",
|
|
213
|
+
"password",
|
|
214
|
+
"secret",
|
|
215
|
+
"apiKey",
|
|
216
|
+
"api_key",
|
|
217
|
+
"bearer",
|
|
218
|
+
"authorization",
|
|
219
|
+
"csrf_token",
|
|
220
|
+
"session",
|
|
221
|
+
"cookie"
|
|
222
|
+
];
|
|
223
|
+
const sanitize = (obj) => {
|
|
224
|
+
const sanitized = {};
|
|
225
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
226
|
+
const keyLower = key.toLowerCase();
|
|
227
|
+
const isSensitive = sensitiveFields.some((field) => keyLower.includes(field));
|
|
228
|
+
if (isSensitive) {
|
|
229
|
+
sanitized[key] = "[REDACTED]";
|
|
230
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
231
|
+
sanitized[key] = sanitize(value);
|
|
232
|
+
} else if (Array.isArray(value)) {
|
|
233
|
+
sanitized[key] = value.map(
|
|
234
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
sanitized[key] = value;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return sanitized;
|
|
241
|
+
};
|
|
242
|
+
if (Array.isArray(data)) {
|
|
243
|
+
return data.map(
|
|
244
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return sanitize(data);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Calculate delay for retry with exponential backoff
|
|
251
|
+
*/
|
|
252
|
+
calculateRetryDelay(attempt) {
|
|
253
|
+
const delay = this.retryConfig.initialDelay * Math.pow(2, attempt);
|
|
254
|
+
return Math.min(delay, this.retryConfig.maxDelay);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Sleep for specified milliseconds
|
|
173
258
|
*/
|
|
174
|
-
|
|
259
|
+
sleep(ms) {
|
|
260
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Perform HTTP request with automatic retry on transient failures
|
|
264
|
+
*/
|
|
265
|
+
async request(method, endpoint, options, retryAttempt = 0) {
|
|
175
266
|
const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
|
|
176
267
|
const isFormData = options?.body instanceof FormData;
|
|
177
268
|
const headers = this.buildHeaders(options?.headers, isFormData);
|
|
@@ -207,6 +298,18 @@ var init_HttpClient = __esm({
|
|
|
207
298
|
errorText ? `Body: ${errorText}` : ""
|
|
208
299
|
);
|
|
209
300
|
}
|
|
301
|
+
const isRetryable = this.retryConfig.retryableStatuses.includes(response.status);
|
|
302
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
303
|
+
if (isRetryable && canRetry) {
|
|
304
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
305
|
+
if (this.debug) {
|
|
306
|
+
this.logger.warn(
|
|
307
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (status: ${response.status})`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
await this.sleep(delay);
|
|
311
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
312
|
+
}
|
|
210
313
|
if (response.status === 404) {
|
|
211
314
|
throw new MarvinNotFoundError(
|
|
212
315
|
`Resource not found: ${endpoint}`,
|
|
@@ -239,7 +342,8 @@ var init_HttpClient = __esm({
|
|
|
239
342
|
}
|
|
240
343
|
const data = await response.json();
|
|
241
344
|
if (this.debug) {
|
|
242
|
-
|
|
345
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
346
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
243
347
|
}
|
|
244
348
|
return data;
|
|
245
349
|
} catch (error) {
|
|
@@ -248,6 +352,17 @@ var init_HttpClient = __esm({
|
|
|
248
352
|
throw error;
|
|
249
353
|
}
|
|
250
354
|
if (error instanceof Error) {
|
|
355
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
356
|
+
if (canRetry && error.name !== "AbortError") {
|
|
357
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
358
|
+
if (this.debug) {
|
|
359
|
+
this.logger.warn(
|
|
360
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (network error)`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
await this.sleep(delay);
|
|
364
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
365
|
+
}
|
|
251
366
|
if (error.name === "AbortError") {
|
|
252
367
|
throw new MarvinNetworkError(
|
|
253
368
|
`Request timeout after ${this.timeout}ms`,
|
|
@@ -328,10 +443,27 @@ var init_AuthStrategy = __esm({
|
|
|
328
443
|
}
|
|
329
444
|
sessionToken;
|
|
330
445
|
onUnauthorized;
|
|
446
|
+
csrfToken;
|
|
447
|
+
/**
|
|
448
|
+
* Set CSRF token for cross-site request forgery protection
|
|
449
|
+
* The token should be obtained from the server after login
|
|
450
|
+
*/
|
|
451
|
+
setCsrfToken(token) {
|
|
452
|
+
this.csrfToken = token;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Get current CSRF token
|
|
456
|
+
*/
|
|
457
|
+
getCsrfToken() {
|
|
458
|
+
return this.csrfToken;
|
|
459
|
+
}
|
|
331
460
|
injectAuth(headers) {
|
|
332
461
|
if (this.sessionToken) {
|
|
333
462
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
334
463
|
}
|
|
464
|
+
if (this.csrfToken) {
|
|
465
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
466
|
+
}
|
|
335
467
|
}
|
|
336
468
|
async handleUnauthorized() {
|
|
337
469
|
if (this.onUnauthorized) {
|
|
@@ -339,7 +471,9 @@ var init_AuthStrategy = __esm({
|
|
|
339
471
|
}
|
|
340
472
|
}
|
|
341
473
|
getDescription() {
|
|
342
|
-
|
|
474
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
475
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
476
|
+
return auth + csrf;
|
|
343
477
|
}
|
|
344
478
|
};
|
|
345
479
|
NoAuth = class {
|
|
@@ -399,11 +533,17 @@ var init_cache = __esm({
|
|
|
399
533
|
"src/core/cache.ts"() {
|
|
400
534
|
"use strict";
|
|
401
535
|
MarvinCache = class {
|
|
402
|
-
constructor(duration = 5 * 60 * 1e3) {
|
|
536
|
+
constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
|
|
403
537
|
this.duration = duration;
|
|
538
|
+
this.maxEntries = maxEntries;
|
|
404
539
|
}
|
|
405
540
|
duration;
|
|
541
|
+
maxEntries;
|
|
406
542
|
cache = /* @__PURE__ */ new Map();
|
|
543
|
+
/**
|
|
544
|
+
* Get cached data by key
|
|
545
|
+
* Returns undefined if not found or expired
|
|
546
|
+
*/
|
|
407
547
|
get(key) {
|
|
408
548
|
const entry = this.cache.get(key);
|
|
409
549
|
if (!entry) return void 0;
|
|
@@ -414,25 +554,149 @@ var init_cache = __esm({
|
|
|
414
554
|
}
|
|
415
555
|
return entry.data;
|
|
416
556
|
}
|
|
417
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Set cached data with optional tags for invalidation
|
|
559
|
+
*/
|
|
560
|
+
set(key, data, tags) {
|
|
561
|
+
if (this.cache.size >= this.maxEntries) {
|
|
562
|
+
const oldestKey = this.cache.keys().next().value;
|
|
563
|
+
if (oldestKey) {
|
|
564
|
+
this.cache.delete(oldestKey);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
418
567
|
this.cache.set(key, {
|
|
419
568
|
data,
|
|
420
|
-
timestamp: Date.now()
|
|
569
|
+
timestamp: Date.now(),
|
|
570
|
+
tags
|
|
421
571
|
});
|
|
422
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if key exists and is not expired
|
|
575
|
+
*/
|
|
423
576
|
has(key) {
|
|
424
577
|
return this.get(key) !== void 0;
|
|
425
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Clear all cached entries
|
|
581
|
+
*/
|
|
426
582
|
clear() {
|
|
427
583
|
this.cache.clear();
|
|
428
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Delete a specific cache entry
|
|
587
|
+
*/
|
|
429
588
|
delete(key) {
|
|
430
589
|
this.cache.delete(key);
|
|
431
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Invalidate all cache entries with a specific tag
|
|
593
|
+
* Useful for invalidating related data after mutations
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```typescript
|
|
597
|
+
* // Cache entries with tags
|
|
598
|
+
* cache.set('entry:1', data, ['entries', 'entry:1']);
|
|
599
|
+
* cache.set('entries:list', data, ['entries']);
|
|
600
|
+
*
|
|
601
|
+
* // Invalidate all entries
|
|
602
|
+
* cache.invalidateByTag('entries');
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
invalidateByTag(tag) {
|
|
606
|
+
let count = 0;
|
|
607
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
608
|
+
if (entry.tags?.includes(tag)) {
|
|
609
|
+
this.cache.delete(key);
|
|
610
|
+
count++;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return count;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Get cache statistics
|
|
617
|
+
*/
|
|
618
|
+
getStats() {
|
|
619
|
+
let oldest = null;
|
|
620
|
+
let newest = null;
|
|
621
|
+
for (const entry of this.cache.values()) {
|
|
622
|
+
if (oldest === null || entry.timestamp < oldest) {
|
|
623
|
+
oldest = entry.timestamp;
|
|
624
|
+
}
|
|
625
|
+
if (newest === null || entry.timestamp > newest) {
|
|
626
|
+
newest = entry.timestamp;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
size: this.cache.size,
|
|
631
|
+
maxEntries: this.maxEntries,
|
|
632
|
+
oldestEntry: oldest,
|
|
633
|
+
newestEntry: newest
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Remove expired entries (garbage collection)
|
|
638
|
+
* Returns number of entries removed
|
|
639
|
+
*/
|
|
640
|
+
prune() {
|
|
641
|
+
let count = 0;
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
644
|
+
const age = now - entry.timestamp;
|
|
645
|
+
if (age > this.duration) {
|
|
646
|
+
this.cache.delete(key);
|
|
647
|
+
count++;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return count;
|
|
651
|
+
}
|
|
432
652
|
};
|
|
433
653
|
}
|
|
434
654
|
});
|
|
435
655
|
|
|
656
|
+
// src/core/validation.ts
|
|
657
|
+
function validateEmail(email, fieldName = "Email") {
|
|
658
|
+
if (!email || typeof email !== "string") {
|
|
659
|
+
throw new MarvinValidationError(`${fieldName} is required and must be a string`);
|
|
660
|
+
}
|
|
661
|
+
const trimmed = email.trim();
|
|
662
|
+
if (trimmed.length === 0) {
|
|
663
|
+
throw new MarvinValidationError(`${fieldName} cannot be empty`);
|
|
664
|
+
}
|
|
665
|
+
if (trimmed.length < 5) {
|
|
666
|
+
throw new MarvinValidationError(`${fieldName} is too short`);
|
|
667
|
+
}
|
|
668
|
+
if (trimmed.length > 254) {
|
|
669
|
+
throw new MarvinValidationError(`${fieldName} exceeds maximum length of 254 characters`);
|
|
670
|
+
}
|
|
671
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
672
|
+
throw new MarvinValidationError(
|
|
673
|
+
`${fieldName} format is invalid. Expected format: user@example.com`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const [localPart, domain] = trimmed.split("@");
|
|
677
|
+
if (localPart.length > 64) {
|
|
678
|
+
throw new MarvinValidationError(`${fieldName} local part exceeds 64 characters`);
|
|
679
|
+
}
|
|
680
|
+
if (domain.length > 253) {
|
|
681
|
+
throw new MarvinValidationError(`${fieldName} domain exceeds 253 characters`);
|
|
682
|
+
}
|
|
683
|
+
if (trimmed.includes("..")) {
|
|
684
|
+
throw new MarvinValidationError(`${fieldName} cannot contain consecutive dots`);
|
|
685
|
+
}
|
|
686
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
687
|
+
throw new MarvinValidationError(`${fieldName} local part cannot start or end with a dot`);
|
|
688
|
+
}
|
|
689
|
+
return trimmed;
|
|
690
|
+
}
|
|
691
|
+
var EMAIL_REGEX;
|
|
692
|
+
var init_validation = __esm({
|
|
693
|
+
"src/core/validation.ts"() {
|
|
694
|
+
"use strict";
|
|
695
|
+
init_errors();
|
|
696
|
+
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])?)*$/;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
436
700
|
// src/core/index.ts
|
|
437
701
|
var init_core = __esm({
|
|
438
702
|
"src/core/index.ts"() {
|
|
@@ -442,6 +706,7 @@ var init_core = __esm({
|
|
|
442
706
|
init_errors();
|
|
443
707
|
init_pagination();
|
|
444
708
|
init_cache();
|
|
709
|
+
init_validation();
|
|
445
710
|
}
|
|
446
711
|
});
|
|
447
712
|
|
|
@@ -468,5 +733,6 @@ export {
|
|
|
468
733
|
pageToOffset,
|
|
469
734
|
offsetToPage,
|
|
470
735
|
MarvinCache,
|
|
736
|
+
validateEmail,
|
|
471
737
|
init_core
|
|
472
738
|
};
|