@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
|
@@ -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,8 +342,8 @@ var init_HttpClient = __esm({
|
|
|
239
342
|
}
|
|
240
343
|
const data = await response.json();
|
|
241
344
|
if (this.debug) {
|
|
242
|
-
const
|
|
243
|
-
this.logger.log(
|
|
345
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
346
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
244
347
|
}
|
|
245
348
|
return data;
|
|
246
349
|
} catch (error) {
|
|
@@ -249,6 +352,17 @@ var init_HttpClient = __esm({
|
|
|
249
352
|
throw error;
|
|
250
353
|
}
|
|
251
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
|
+
}
|
|
252
366
|
if (error.name === "AbortError") {
|
|
253
367
|
throw new MarvinNetworkError(
|
|
254
368
|
`Request timeout after ${this.timeout}ms`,
|
|
@@ -329,10 +443,27 @@ var init_AuthStrategy = __esm({
|
|
|
329
443
|
}
|
|
330
444
|
sessionToken;
|
|
331
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
|
+
}
|
|
332
460
|
injectAuth(headers) {
|
|
333
461
|
if (this.sessionToken) {
|
|
334
462
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
335
463
|
}
|
|
464
|
+
if (this.csrfToken) {
|
|
465
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
466
|
+
}
|
|
336
467
|
}
|
|
337
468
|
async handleUnauthorized() {
|
|
338
469
|
if (this.onUnauthorized) {
|
|
@@ -340,7 +471,9 @@ var init_AuthStrategy = __esm({
|
|
|
340
471
|
}
|
|
341
472
|
}
|
|
342
473
|
getDescription() {
|
|
343
|
-
|
|
474
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
475
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
476
|
+
return auth + csrf;
|
|
344
477
|
}
|
|
345
478
|
};
|
|
346
479
|
NoAuth = class {
|
|
@@ -400,11 +533,17 @@ var init_cache = __esm({
|
|
|
400
533
|
"src/core/cache.ts"() {
|
|
401
534
|
"use strict";
|
|
402
535
|
MarvinCache = class {
|
|
403
|
-
constructor(duration = 5 * 60 * 1e3) {
|
|
536
|
+
constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
|
|
404
537
|
this.duration = duration;
|
|
538
|
+
this.maxEntries = maxEntries;
|
|
405
539
|
}
|
|
406
540
|
duration;
|
|
541
|
+
maxEntries;
|
|
407
542
|
cache = /* @__PURE__ */ new Map();
|
|
543
|
+
/**
|
|
544
|
+
* Get cached data by key
|
|
545
|
+
* Returns undefined if not found or expired
|
|
546
|
+
*/
|
|
408
547
|
get(key) {
|
|
409
548
|
const entry = this.cache.get(key);
|
|
410
549
|
if (!entry) return void 0;
|
|
@@ -415,25 +554,149 @@ var init_cache = __esm({
|
|
|
415
554
|
}
|
|
416
555
|
return entry.data;
|
|
417
556
|
}
|
|
418
|
-
|
|
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
|
+
}
|
|
419
567
|
this.cache.set(key, {
|
|
420
568
|
data,
|
|
421
|
-
timestamp: Date.now()
|
|
569
|
+
timestamp: Date.now(),
|
|
570
|
+
tags
|
|
422
571
|
});
|
|
423
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if key exists and is not expired
|
|
575
|
+
*/
|
|
424
576
|
has(key) {
|
|
425
577
|
return this.get(key) !== void 0;
|
|
426
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Clear all cached entries
|
|
581
|
+
*/
|
|
427
582
|
clear() {
|
|
428
583
|
this.cache.clear();
|
|
429
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Delete a specific cache entry
|
|
587
|
+
*/
|
|
430
588
|
delete(key) {
|
|
431
589
|
this.cache.delete(key);
|
|
432
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
|
+
}
|
|
433
652
|
};
|
|
434
653
|
}
|
|
435
654
|
});
|
|
436
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
|
+
|
|
437
700
|
// src/core/index.ts
|
|
438
701
|
var init_core = __esm({
|
|
439
702
|
"src/core/index.ts"() {
|
|
@@ -443,6 +706,7 @@ var init_core = __esm({
|
|
|
443
706
|
init_errors();
|
|
444
707
|
init_pagination();
|
|
445
708
|
init_cache();
|
|
709
|
+
init_validation();
|
|
446
710
|
}
|
|
447
711
|
});
|
|
448
712
|
|
|
@@ -469,5 +733,6 @@ export {
|
|
|
469
733
|
pageToOffset,
|
|
470
734
|
offsetToPage,
|
|
471
735
|
MarvinCache,
|
|
736
|
+
validateEmail,
|
|
472
737
|
init_core
|
|
473
738
|
};
|