@inneropen/marvin-sdk 2.0.0-next.14 → 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-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/platform.cjs
CHANGED
|
@@ -43,7 +43,8 @@ __export(platform_exports, {
|
|
|
43
43
|
WebhooksModule: () => WebhooksModule,
|
|
44
44
|
WorkspaceMembersModule: () => WorkspaceMembersModule,
|
|
45
45
|
WorkspacesModule: () => WorkspacesModule,
|
|
46
|
-
createPlatformClient: () => createPlatformClient
|
|
46
|
+
createPlatformClient: () => createPlatformClient,
|
|
47
|
+
createPlatformConfigFromEnv: () => createPlatformConfigFromEnv
|
|
47
48
|
});
|
|
48
49
|
module.exports = __toCommonJS(platform_exports);
|
|
49
50
|
|
|
@@ -84,6 +85,15 @@ var MarvinAuthError = class _MarvinAuthError extends MarvinError {
|
|
|
84
85
|
Object.setPrototypeOf(this, _MarvinAuthError.prototype);
|
|
85
86
|
}
|
|
86
87
|
};
|
|
88
|
+
var MarvinValidationError = class _MarvinValidationError extends MarvinError {
|
|
89
|
+
constructor(message, field) {
|
|
90
|
+
super(message, "VALIDATION_ERROR");
|
|
91
|
+
this.field = field;
|
|
92
|
+
this.name = "MarvinValidationError";
|
|
93
|
+
Object.setPrototypeOf(this, _MarvinValidationError.prototype);
|
|
94
|
+
}
|
|
95
|
+
field;
|
|
96
|
+
};
|
|
87
97
|
var MarvinNotFoundError = class _MarvinNotFoundError extends MarvinError {
|
|
88
98
|
constructor(message, endpoint) {
|
|
89
99
|
super(message, "NOT_FOUND", 404);
|
|
@@ -120,15 +130,46 @@ var HttpClient = class {
|
|
|
120
130
|
credentials;
|
|
121
131
|
timeout;
|
|
122
132
|
debug;
|
|
133
|
+
retryConfig;
|
|
123
134
|
logger;
|
|
124
135
|
constructor(config) {
|
|
125
136
|
this.baseUrl = config.baseUrl;
|
|
126
137
|
this.auth = config.auth;
|
|
127
138
|
this.defaultHeaders = config.defaultHeaders || {};
|
|
128
139
|
this.credentials = config.credentials || "same-origin";
|
|
129
|
-
|
|
140
|
+
const MAX_TIMEOUT = 12e4;
|
|
141
|
+
this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
|
|
130
142
|
this.debug = config.debug ?? false;
|
|
131
143
|
this.logger = config.logger ?? console;
|
|
144
|
+
this.retryConfig = {
|
|
145
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
146
|
+
initialDelay: config.retry?.initialDelay ?? 1e3,
|
|
147
|
+
maxDelay: config.retry?.maxDelay ?? 1e4,
|
|
148
|
+
retryableStatuses: config.retry?.retryableStatuses ?? [408, 429, 500, 502, 503, 504]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Validate path parameter to prevent path traversal attacks
|
|
153
|
+
*/
|
|
154
|
+
validatePathParam(param, paramName = "parameter") {
|
|
155
|
+
if (typeof param === "number") {
|
|
156
|
+
return String(param);
|
|
157
|
+
}
|
|
158
|
+
if (!param || typeof param !== "string") {
|
|
159
|
+
throw new MarvinValidationError(`Invalid ${paramName}: must be a non-empty string or number`);
|
|
160
|
+
}
|
|
161
|
+
if (param.includes("..") || param.includes("/") || param.includes("\\")) {
|
|
162
|
+
throw new MarvinValidationError(
|
|
163
|
+
`Invalid ${paramName}: cannot contain path traversal characters (.. / \\)`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (param.includes("\0")) {
|
|
167
|
+
throw new MarvinValidationError(`Invalid ${paramName}: cannot contain null bytes`);
|
|
168
|
+
}
|
|
169
|
+
if (param.length > 255) {
|
|
170
|
+
throw new MarvinValidationError(`Invalid ${paramName}: exceeds maximum length of 255 characters`);
|
|
171
|
+
}
|
|
172
|
+
return param;
|
|
132
173
|
}
|
|
133
174
|
/**
|
|
134
175
|
* Build full URL from endpoint
|
|
@@ -162,9 +203,69 @@ var HttpClient = class {
|
|
|
162
203
|
return filtered.length > 0 ? `?${filtered.join("&")}` : "";
|
|
163
204
|
}
|
|
164
205
|
/**
|
|
165
|
-
*
|
|
206
|
+
* Sanitize data for logging by redacting sensitive fields
|
|
166
207
|
*/
|
|
167
|
-
|
|
208
|
+
sanitizeForLogging(data) {
|
|
209
|
+
if (!data || typeof data !== "object") {
|
|
210
|
+
return data;
|
|
211
|
+
}
|
|
212
|
+
const sensitiveFields = [
|
|
213
|
+
"token",
|
|
214
|
+
"access_token",
|
|
215
|
+
"refresh_token",
|
|
216
|
+
"password",
|
|
217
|
+
"secret",
|
|
218
|
+
"apiKey",
|
|
219
|
+
"api_key",
|
|
220
|
+
"bearer",
|
|
221
|
+
"authorization",
|
|
222
|
+
"csrf_token",
|
|
223
|
+
"session",
|
|
224
|
+
"cookie"
|
|
225
|
+
];
|
|
226
|
+
const sanitize = (obj) => {
|
|
227
|
+
const sanitized = {};
|
|
228
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
229
|
+
const keyLower = key.toLowerCase();
|
|
230
|
+
const isSensitive = sensitiveFields.some((field) => keyLower.includes(field));
|
|
231
|
+
if (isSensitive) {
|
|
232
|
+
sanitized[key] = "[REDACTED]";
|
|
233
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
234
|
+
sanitized[key] = sanitize(value);
|
|
235
|
+
} else if (Array.isArray(value)) {
|
|
236
|
+
sanitized[key] = value.map(
|
|
237
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
sanitized[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return sanitized;
|
|
244
|
+
};
|
|
245
|
+
if (Array.isArray(data)) {
|
|
246
|
+
return data.map(
|
|
247
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return sanitize(data);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Calculate delay for retry with exponential backoff
|
|
254
|
+
*/
|
|
255
|
+
calculateRetryDelay(attempt) {
|
|
256
|
+
const delay = this.retryConfig.initialDelay * Math.pow(2, attempt);
|
|
257
|
+
return Math.min(delay, this.retryConfig.maxDelay);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Sleep for specified milliseconds
|
|
261
|
+
*/
|
|
262
|
+
sleep(ms) {
|
|
263
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Perform HTTP request with automatic retry on transient failures
|
|
267
|
+
*/
|
|
268
|
+
async request(method, endpoint, options, retryAttempt = 0) {
|
|
168
269
|
const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
|
|
169
270
|
const isFormData = options?.body instanceof FormData;
|
|
170
271
|
const headers = this.buildHeaders(options?.headers, isFormData);
|
|
@@ -200,6 +301,18 @@ var HttpClient = class {
|
|
|
200
301
|
errorText ? `Body: ${errorText}` : ""
|
|
201
302
|
);
|
|
202
303
|
}
|
|
304
|
+
const isRetryable = this.retryConfig.retryableStatuses.includes(response.status);
|
|
305
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
306
|
+
if (isRetryable && canRetry) {
|
|
307
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
308
|
+
if (this.debug) {
|
|
309
|
+
this.logger.warn(
|
|
310
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (status: ${response.status})`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
await this.sleep(delay);
|
|
314
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
315
|
+
}
|
|
203
316
|
if (response.status === 404) {
|
|
204
317
|
throw new MarvinNotFoundError(
|
|
205
318
|
`Resource not found: ${endpoint}`,
|
|
@@ -232,7 +345,8 @@ var HttpClient = class {
|
|
|
232
345
|
}
|
|
233
346
|
const data = await response.json();
|
|
234
347
|
if (this.debug) {
|
|
235
|
-
|
|
348
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
349
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
236
350
|
}
|
|
237
351
|
return data;
|
|
238
352
|
} catch (error) {
|
|
@@ -241,6 +355,17 @@ var HttpClient = class {
|
|
|
241
355
|
throw error;
|
|
242
356
|
}
|
|
243
357
|
if (error instanceof Error) {
|
|
358
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
359
|
+
if (canRetry && error.name !== "AbortError") {
|
|
360
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
361
|
+
if (this.debug) {
|
|
362
|
+
this.logger.warn(
|
|
363
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (network error)`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
await this.sleep(delay);
|
|
367
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
368
|
+
}
|
|
244
369
|
if (error.name === "AbortError") {
|
|
245
370
|
throw new MarvinNetworkError(
|
|
246
371
|
`Request timeout after ${this.timeout}ms`,
|
|
@@ -295,10 +420,27 @@ var SessionAuth = class {
|
|
|
295
420
|
}
|
|
296
421
|
sessionToken;
|
|
297
422
|
onUnauthorized;
|
|
423
|
+
csrfToken;
|
|
424
|
+
/**
|
|
425
|
+
* Set CSRF token for cross-site request forgery protection
|
|
426
|
+
* The token should be obtained from the server after login
|
|
427
|
+
*/
|
|
428
|
+
setCsrfToken(token) {
|
|
429
|
+
this.csrfToken = token;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get current CSRF token
|
|
433
|
+
*/
|
|
434
|
+
getCsrfToken() {
|
|
435
|
+
return this.csrfToken;
|
|
436
|
+
}
|
|
298
437
|
injectAuth(headers) {
|
|
299
438
|
if (this.sessionToken) {
|
|
300
439
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
301
440
|
}
|
|
441
|
+
if (this.csrfToken) {
|
|
442
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
443
|
+
}
|
|
302
444
|
}
|
|
303
445
|
async handleUnauthorized() {
|
|
304
446
|
if (this.onUnauthorized) {
|
|
@@ -306,7 +448,9 @@ var SessionAuth = class {
|
|
|
306
448
|
}
|
|
307
449
|
}
|
|
308
450
|
getDescription() {
|
|
309
|
-
|
|
451
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
452
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
453
|
+
return auth + csrf;
|
|
310
454
|
}
|
|
311
455
|
};
|
|
312
456
|
|
|
@@ -326,7 +470,8 @@ var EntriesModule = class {
|
|
|
326
470
|
* Get a single entry by ID
|
|
327
471
|
*/
|
|
328
472
|
async get(id) {
|
|
329
|
-
|
|
473
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
474
|
+
return this.http.get(`/api/platform/entries/${validId}`);
|
|
330
475
|
}
|
|
331
476
|
/**
|
|
332
477
|
* Create a new entry
|
|
@@ -338,26 +483,31 @@ var EntriesModule = class {
|
|
|
338
483
|
* Update an entry
|
|
339
484
|
*/
|
|
340
485
|
async update(id, data) {
|
|
341
|
-
|
|
486
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
487
|
+
return this.http.patch(`/api/platform/entries/${validId}`, data);
|
|
342
488
|
}
|
|
343
489
|
/**
|
|
344
490
|
* Delete an entry
|
|
345
491
|
*/
|
|
346
492
|
async delete(id) {
|
|
347
|
-
|
|
493
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
494
|
+
return this.http.delete(`/api/platform/entries/${validId}`);
|
|
348
495
|
}
|
|
349
496
|
/**
|
|
350
497
|
* Get collections that this entry belongs to
|
|
351
498
|
*/
|
|
352
499
|
async listCollections(entryId) {
|
|
353
|
-
|
|
500
|
+
const validId = this.http.validatePathParam(entryId, "entry ID");
|
|
501
|
+
return this.http.get(`/api/platform/entries/${validId}/collections`);
|
|
354
502
|
}
|
|
355
503
|
/**
|
|
356
504
|
* Add entry to a collection
|
|
357
505
|
*/
|
|
358
506
|
async addToCollection(entryId, collectionId) {
|
|
507
|
+
const validEntryId = this.http.validatePathParam(entryId, "entry ID");
|
|
508
|
+
const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
|
|
359
509
|
return this.http.post(
|
|
360
|
-
`/api/platform/entries/${
|
|
510
|
+
`/api/platform/entries/${validEntryId}/collections/${validCollectionId}`,
|
|
361
511
|
{}
|
|
362
512
|
);
|
|
363
513
|
}
|
|
@@ -365,7 +515,9 @@ var EntriesModule = class {
|
|
|
365
515
|
* Remove entry from a collection
|
|
366
516
|
*/
|
|
367
517
|
async removeFromCollection(entryId, collectionId) {
|
|
368
|
-
|
|
518
|
+
const validEntryId = this.http.validatePathParam(entryId, "entry ID");
|
|
519
|
+
const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
|
|
520
|
+
return this.http.delete(`/api/platform/entries/${validEntryId}/collections/${validCollectionId}`);
|
|
369
521
|
}
|
|
370
522
|
};
|
|
371
523
|
|
|
@@ -385,7 +537,8 @@ var CollectionsModule = class {
|
|
|
385
537
|
* Get a single collection by ID
|
|
386
538
|
*/
|
|
387
539
|
async get(id) {
|
|
388
|
-
|
|
540
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
541
|
+
return this.http.get(`/api/platform/collections/${validId}`);
|
|
389
542
|
}
|
|
390
543
|
/**
|
|
391
544
|
* Create a new collection
|
|
@@ -397,25 +550,29 @@ var CollectionsModule = class {
|
|
|
397
550
|
* Update a collection
|
|
398
551
|
*/
|
|
399
552
|
async update(id, data) {
|
|
400
|
-
|
|
553
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
554
|
+
return this.http.patch(`/api/platform/collections/${validId}`, data);
|
|
401
555
|
}
|
|
402
556
|
/**
|
|
403
557
|
* Delete a collection
|
|
404
558
|
*/
|
|
405
559
|
async delete(id) {
|
|
406
|
-
|
|
560
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
561
|
+
return this.http.delete(`/api/platform/collections/${validId}`);
|
|
407
562
|
}
|
|
408
563
|
/**
|
|
409
564
|
* Get entries in a collection
|
|
410
565
|
*/
|
|
411
566
|
async getEntries(id) {
|
|
412
|
-
|
|
567
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
568
|
+
return this.http.get(`/api/platform/collections/${validId}/entries`);
|
|
413
569
|
}
|
|
414
570
|
/**
|
|
415
571
|
* Reorder entries in a collection
|
|
416
572
|
*/
|
|
417
573
|
async reorderEntries(id, entries) {
|
|
418
|
-
|
|
574
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
575
|
+
return this.http.patch(`/api/platform/collections/${validId}/entries/order`, { entries });
|
|
419
576
|
}
|
|
420
577
|
};
|
|
421
578
|
|
|
@@ -435,7 +592,8 @@ var ResourcesModule = class {
|
|
|
435
592
|
* Get a single resource by ID
|
|
436
593
|
*/
|
|
437
594
|
async get(id) {
|
|
438
|
-
|
|
595
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
596
|
+
return this.http.get(`/api/platform/resources/${validId}`);
|
|
439
597
|
}
|
|
440
598
|
/**
|
|
441
599
|
* Create a new resource
|
|
@@ -447,7 +605,8 @@ var ResourcesModule = class {
|
|
|
447
605
|
* Update a resource
|
|
448
606
|
*/
|
|
449
607
|
async update(id, data) {
|
|
450
|
-
|
|
608
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
609
|
+
return this.http.patch(`/api/platform/resources/${validId}`, data);
|
|
451
610
|
}
|
|
452
611
|
/**
|
|
453
612
|
* Delete a resource
|
|
@@ -469,6 +628,47 @@ var AssetsModule = class {
|
|
|
469
628
|
this.http = http;
|
|
470
629
|
}
|
|
471
630
|
http;
|
|
631
|
+
/**
|
|
632
|
+
* Validate file before upload
|
|
633
|
+
*/
|
|
634
|
+
validateFile(file, options) {
|
|
635
|
+
const maxSize = options?.maxFileSize || 100 * 1024 * 1024;
|
|
636
|
+
const allowedTypes = options?.allowedMimeTypes;
|
|
637
|
+
if (file.size === 0) {
|
|
638
|
+
throw new MarvinValidationError("File is empty");
|
|
639
|
+
}
|
|
640
|
+
if (file.size > maxSize) {
|
|
641
|
+
const maxMB = Math.round(maxSize / (1024 * 1024));
|
|
642
|
+
const fileMB = Math.round(file.size / (1024 * 1024));
|
|
643
|
+
throw new MarvinValidationError(
|
|
644
|
+
`File size (${fileMB}MB) exceeds maximum allowed size of ${maxMB}MB`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
if (allowedTypes && allowedTypes.length > 0) {
|
|
648
|
+
if (file instanceof File && file.type) {
|
|
649
|
+
const isAllowed = allowedTypes.some((allowed) => {
|
|
650
|
+
if (allowed.endsWith("/*")) {
|
|
651
|
+
const prefix = allowed.slice(0, -2);
|
|
652
|
+
return file.type.startsWith(prefix);
|
|
653
|
+
}
|
|
654
|
+
return file.type === allowed;
|
|
655
|
+
});
|
|
656
|
+
if (!isAllowed) {
|
|
657
|
+
throw new MarvinValidationError(
|
|
658
|
+
`File type "${file.type}" is not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (file instanceof File) {
|
|
664
|
+
if (!file.name || file.name.trim().length === 0) {
|
|
665
|
+
throw new MarvinValidationError("File must have a valid name");
|
|
666
|
+
}
|
|
667
|
+
if (file.name.includes("..") || file.name.includes("/") || file.name.includes("\\")) {
|
|
668
|
+
throw new MarvinValidationError("Invalid filename: cannot contain path separators");
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
472
672
|
/**
|
|
473
673
|
* List all assets
|
|
474
674
|
*/
|
|
@@ -479,16 +679,31 @@ var AssetsModule = class {
|
|
|
479
679
|
* Get a single asset by ID
|
|
480
680
|
*/
|
|
481
681
|
async get(id) {
|
|
482
|
-
|
|
682
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
683
|
+
return this.http.get(`/api/platform/assets/${validId}`);
|
|
483
684
|
}
|
|
484
685
|
/**
|
|
485
686
|
* Upload a new asset file
|
|
486
687
|
*
|
|
487
688
|
* @param file - The file to upload (File or Blob)
|
|
488
689
|
* @param metadata - Asset metadata (slug, name, altText, description, metadata)
|
|
690
|
+
* @param options - Upload options (file size limits, allowed types)
|
|
489
691
|
* @returns The created asset with extracted metadata
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* ```typescript
|
|
695
|
+
* // Upload with default limits (100MB, all types)
|
|
696
|
+
* const asset = await assets.upload(file, { slug: 'my-image', name: 'My Image' });
|
|
697
|
+
*
|
|
698
|
+
* // Upload with restrictions
|
|
699
|
+
* const asset = await assets.upload(file,
|
|
700
|
+
* { slug: 'avatar', name: 'Avatar' },
|
|
701
|
+
* { maxFileSize: 5 * 1024 * 1024, allowedMimeTypes: ['image/*'] }
|
|
702
|
+
* );
|
|
703
|
+
* ```
|
|
490
704
|
*/
|
|
491
|
-
async upload(file, metadata) {
|
|
705
|
+
async upload(file, metadata, options) {
|
|
706
|
+
this.validateFile(file, options);
|
|
492
707
|
const formData = new FormData();
|
|
493
708
|
formData.append("file", file);
|
|
494
709
|
formData.append("slug", metadata.slug);
|
|
@@ -508,13 +723,24 @@ var AssetsModule = class {
|
|
|
508
723
|
* Update an asset (editable fields only: slug, name, altText, description, metadata)
|
|
509
724
|
*/
|
|
510
725
|
async update(id, data) {
|
|
511
|
-
|
|
726
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
727
|
+
return this.http.patch(`/api/platform/assets/${validId}`, data);
|
|
512
728
|
}
|
|
513
729
|
/**
|
|
514
730
|
* Delete an asset (removes file from storage and database)
|
|
515
731
|
*/
|
|
516
732
|
async delete(id) {
|
|
517
|
-
|
|
733
|
+
const validId = this.http.validatePathParam(id, "asset ID");
|
|
734
|
+
return this.http.delete(`/api/platform/assets/${validId}`);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Get download URL for an asset file
|
|
738
|
+
* Use this to create download links or fetch the file directly
|
|
739
|
+
*/
|
|
740
|
+
getDownloadUrl(id, baseUrl) {
|
|
741
|
+
const validId = this.http.validatePathParam(id, "asset ID");
|
|
742
|
+
const base = baseUrl || (typeof window !== "undefined" && window.location ? window.location.origin : "");
|
|
743
|
+
return `${base}/api/platform/assets/${validId}/file`;
|
|
518
744
|
}
|
|
519
745
|
};
|
|
520
746
|
|
|
@@ -534,7 +760,8 @@ var APIClientsModule = class {
|
|
|
534
760
|
* Get a single API client by ID
|
|
535
761
|
*/
|
|
536
762
|
async get(id) {
|
|
537
|
-
|
|
763
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
764
|
+
return this.http.get(`/api/platform/api-clients/${validId}`);
|
|
538
765
|
}
|
|
539
766
|
/**
|
|
540
767
|
* Create a new API client (returns client with token)
|
|
@@ -546,7 +773,8 @@ var APIClientsModule = class {
|
|
|
546
773
|
* Update an API client
|
|
547
774
|
*/
|
|
548
775
|
async update(id, data) {
|
|
549
|
-
|
|
776
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
777
|
+
return this.http.patch(`/api/platform/api-clients/${validId}`, data);
|
|
550
778
|
}
|
|
551
779
|
/**
|
|
552
780
|
* Delete an API client
|
|
@@ -757,11 +985,23 @@ var InvitesModule = class {
|
|
|
757
985
|
}
|
|
758
986
|
/**
|
|
759
987
|
* Generate invitation URL from a token
|
|
760
|
-
*
|
|
988
|
+
*
|
|
989
|
+
* @param token - Invitation token
|
|
990
|
+
* @param baseUrl - Base URL for the frontend (required in browser environments)
|
|
991
|
+
* @returns Full invitation URL
|
|
992
|
+
*
|
|
993
|
+
* @example
|
|
994
|
+
* ```typescript
|
|
995
|
+
* // Node.js (can use env vars)
|
|
996
|
+
* const url = invites.getInvitationUrl(token);
|
|
997
|
+
*
|
|
998
|
+
* // Browser (must provide baseUrl)
|
|
999
|
+
* const url = invites.getInvitationUrl(token, 'https://app.example.com');
|
|
1000
|
+
* ```
|
|
761
1001
|
*/
|
|
762
1002
|
getInvitationUrl(token, baseUrl) {
|
|
763
1003
|
let base = baseUrl;
|
|
764
|
-
if (!base) {
|
|
1004
|
+
if (!base && typeof process !== "undefined" && process.env) {
|
|
765
1005
|
base = process.env.MARVIN_FRONTEND_URL;
|
|
766
1006
|
if (!base && process.env.MARVIN_API_URL) {
|
|
767
1007
|
const apiUrl = process.env.MARVIN_API_URL;
|
|
@@ -773,11 +1013,17 @@ var InvitesModule = class {
|
|
|
773
1013
|
base = apiUrl;
|
|
774
1014
|
}
|
|
775
1015
|
}
|
|
776
|
-
|
|
777
|
-
|
|
1016
|
+
}
|
|
1017
|
+
if (!base) {
|
|
1018
|
+
if (typeof window !== "undefined" && window.location) {
|
|
1019
|
+
base = window.location.origin;
|
|
1020
|
+
} else {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
'baseUrl is required when not in Node.js environment. Pass the frontend URL as second argument: getInvitationUrl(token, "https://app.example.com")'
|
|
1023
|
+
);
|
|
778
1024
|
}
|
|
779
1025
|
}
|
|
780
|
-
return `${base}/register?token=${token}`;
|
|
1026
|
+
return `${base}/register?token=${encodeURIComponent(token)}`;
|
|
781
1027
|
}
|
|
782
1028
|
};
|
|
783
1029
|
|
|
@@ -831,6 +1077,57 @@ var WebhooksModule = class {
|
|
|
831
1077
|
this.http = http;
|
|
832
1078
|
}
|
|
833
1079
|
http;
|
|
1080
|
+
/**
|
|
1081
|
+
* Validate webhook URL to prevent SSRF attacks
|
|
1082
|
+
*/
|
|
1083
|
+
validateWebhookUrl(url) {
|
|
1084
|
+
if (!url || typeof url !== "string") {
|
|
1085
|
+
throw new MarvinValidationError("Webhook URL is required and must be a string");
|
|
1086
|
+
}
|
|
1087
|
+
let parsed;
|
|
1088
|
+
try {
|
|
1089
|
+
parsed = new URL(url);
|
|
1090
|
+
} catch {
|
|
1091
|
+
throw new MarvinValidationError("Invalid webhook URL format. Expected: https://example.com/webhook");
|
|
1092
|
+
}
|
|
1093
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
1094
|
+
throw new MarvinValidationError(
|
|
1095
|
+
`Webhook URL must use HTTP or HTTPS protocol. Got: ${parsed.protocol}`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1099
|
+
const blockedHosts = [
|
|
1100
|
+
"localhost",
|
|
1101
|
+
"127.0.0.1",
|
|
1102
|
+
"0.0.0.0",
|
|
1103
|
+
"::1",
|
|
1104
|
+
"169.254.169.254",
|
|
1105
|
+
// AWS metadata service
|
|
1106
|
+
"[::]"
|
|
1107
|
+
];
|
|
1108
|
+
if (blockedHosts.some((blocked) => hostname === blocked || hostname.includes(blocked))) {
|
|
1109
|
+
throw new MarvinValidationError(
|
|
1110
|
+
"Webhook URL cannot target localhost or internal network addresses"
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1114
|
+
const match = hostname.match(ipv4Regex);
|
|
1115
|
+
if (match) {
|
|
1116
|
+
const octets = match.slice(1).map(Number);
|
|
1117
|
+
if (octets[0] === 10) {
|
|
1118
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (10.x.x.x)");
|
|
1119
|
+
}
|
|
1120
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
1121
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (172.16-31.x.x)");
|
|
1122
|
+
}
|
|
1123
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
1124
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (192.168.x.x)");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (url.length > 2048) {
|
|
1128
|
+
throw new MarvinValidationError("Webhook URL exceeds maximum length of 2048 characters");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
834
1131
|
/**
|
|
835
1132
|
* List all webhooks for the current workspace
|
|
836
1133
|
*/
|
|
@@ -842,31 +1139,43 @@ var WebhooksModule = class {
|
|
|
842
1139
|
* Get a webhook by ID
|
|
843
1140
|
*/
|
|
844
1141
|
async get(id) {
|
|
845
|
-
|
|
1142
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1143
|
+
return this.http.get(`/api/groups/webhooks/${validId}`);
|
|
846
1144
|
}
|
|
847
1145
|
/**
|
|
848
1146
|
* Create a new webhook
|
|
1147
|
+
* Validates URL to prevent SSRF attacks
|
|
849
1148
|
*/
|
|
850
1149
|
async create(data) {
|
|
1150
|
+
if (data.url) {
|
|
1151
|
+
this.validateWebhookUrl(data.url);
|
|
1152
|
+
}
|
|
851
1153
|
return this.http.post("/api/groups/webhooks", data);
|
|
852
1154
|
}
|
|
853
1155
|
/**
|
|
854
1156
|
* Update a webhook
|
|
1157
|
+
* Validates URL if provided to prevent SSRF attacks
|
|
855
1158
|
*/
|
|
856
1159
|
async update(id, data) {
|
|
857
|
-
|
|
1160
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1161
|
+
if (data.url) {
|
|
1162
|
+
this.validateWebhookUrl(data.url);
|
|
1163
|
+
}
|
|
1164
|
+
return this.http.put(`/api/groups/webhooks/${validId}`, data);
|
|
858
1165
|
}
|
|
859
1166
|
/**
|
|
860
1167
|
* Delete a webhook
|
|
861
1168
|
*/
|
|
862
1169
|
async delete(id) {
|
|
863
|
-
|
|
1170
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1171
|
+
await this.http.delete(`/api/groups/webhooks/${validId}`);
|
|
864
1172
|
}
|
|
865
1173
|
/**
|
|
866
1174
|
* Test a webhook
|
|
867
1175
|
*/
|
|
868
1176
|
async test(id) {
|
|
869
|
-
|
|
1177
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1178
|
+
return this.http.post(`/api/groups/webhooks/${validId}/test`, {});
|
|
870
1179
|
}
|
|
871
1180
|
/**
|
|
872
1181
|
* Rerun failed webhooks
|
|
@@ -958,6 +1267,7 @@ var AdminSystemModule = class {
|
|
|
958
1267
|
}
|
|
959
1268
|
/**
|
|
960
1269
|
* Update email settings
|
|
1270
|
+
* Note: Password field is write-only and will not be returned in response
|
|
961
1271
|
*/
|
|
962
1272
|
async updateEmailSettings(data) {
|
|
963
1273
|
return this.http.put("/api/admin/email", data);
|
|
@@ -1308,7 +1618,8 @@ var ScheduledTasksModule = class {
|
|
|
1308
1618
|
* Get a scheduled task by ID or slug
|
|
1309
1619
|
*/
|
|
1310
1620
|
async get(idOrSlug) {
|
|
1311
|
-
|
|
1621
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1622
|
+
return this.http.get(`/api/platform/scheduled-tasks/${validIdOrSlug}`);
|
|
1312
1623
|
}
|
|
1313
1624
|
/**
|
|
1314
1625
|
* Create a new scheduled task
|
|
@@ -1320,7 +1631,8 @@ var ScheduledTasksModule = class {
|
|
|
1320
1631
|
* Update a scheduled task
|
|
1321
1632
|
*/
|
|
1322
1633
|
async update(idOrSlug, data) {
|
|
1323
|
-
|
|
1634
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1635
|
+
return this.http.patch(`/api/platform/scheduled-tasks/${validIdOrSlug}`, data);
|
|
1324
1636
|
}
|
|
1325
1637
|
/**
|
|
1326
1638
|
* Delete a scheduled task
|
|
@@ -1366,6 +1678,34 @@ var FormsModule = class {
|
|
|
1366
1678
|
this.http = http;
|
|
1367
1679
|
}
|
|
1368
1680
|
http;
|
|
1681
|
+
/**
|
|
1682
|
+
* Validate form submission data
|
|
1683
|
+
* - Prevents XSS by checking for script tags
|
|
1684
|
+
* - Validates data is a proper object
|
|
1685
|
+
* - Checks for reasonable size limits
|
|
1686
|
+
*/
|
|
1687
|
+
validateFormData(data) {
|
|
1688
|
+
if (!data || typeof data !== "object") {
|
|
1689
|
+
throw new MarvinValidationError("Form data must be a valid object");
|
|
1690
|
+
}
|
|
1691
|
+
const jsonString = JSON.stringify(data);
|
|
1692
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
1693
|
+
if (sizeInBytes > 1024 * 1024) {
|
|
1694
|
+
throw new MarvinValidationError("Form data exceeds maximum size of 1MB");
|
|
1695
|
+
}
|
|
1696
|
+
const checkForScripts = (value) => {
|
|
1697
|
+
if (typeof value === "string") {
|
|
1698
|
+
if (/<script[\s\S]*?>[\s\S]*?<\/script>/gi.test(value)) {
|
|
1699
|
+
throw new MarvinValidationError("Form data contains potentially malicious content");
|
|
1700
|
+
}
|
|
1701
|
+
} else if (Array.isArray(value)) {
|
|
1702
|
+
value.forEach(checkForScripts);
|
|
1703
|
+
} else if (value && typeof value === "object") {
|
|
1704
|
+
Object.values(value).forEach(checkForScripts);
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
Object.values(data).forEach(checkForScripts);
|
|
1708
|
+
}
|
|
1369
1709
|
/**
|
|
1370
1710
|
* List all forms
|
|
1371
1711
|
*/
|
|
@@ -1376,7 +1716,8 @@ var FormsModule = class {
|
|
|
1376
1716
|
* Get a single form by ID
|
|
1377
1717
|
*/
|
|
1378
1718
|
async get(id) {
|
|
1379
|
-
|
|
1719
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1720
|
+
return this.http.get(`/api/platform/forms/${validId}`);
|
|
1380
1721
|
}
|
|
1381
1722
|
/**
|
|
1382
1723
|
* Create a new form
|
|
@@ -1388,7 +1729,8 @@ var FormsModule = class {
|
|
|
1388
1729
|
* Update a form
|
|
1389
1730
|
*/
|
|
1390
1731
|
async update(id, data) {
|
|
1391
|
-
|
|
1732
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1733
|
+
return this.http.patch(`/api/platform/forms/${validId}`, data);
|
|
1392
1734
|
}
|
|
1393
1735
|
/**
|
|
1394
1736
|
* Delete a form
|
|
@@ -1410,15 +1752,25 @@ var FormsModule = class {
|
|
|
1410
1752
|
}
|
|
1411
1753
|
/**
|
|
1412
1754
|
* Submit data to a published form (Publishing API - public access)
|
|
1755
|
+
* Validates data before submission to prevent XSS and malicious content
|
|
1413
1756
|
*/
|
|
1414
1757
|
async submitForm(slug, data) {
|
|
1415
|
-
|
|
1758
|
+
const validSlug = this.http.validatePathParam(slug, "form slug");
|
|
1759
|
+
this.validateFormData(data);
|
|
1760
|
+
return this.http.post(`/api/publish/forms/${validSlug}/submit`, {
|
|
1416
1761
|
dataJson: data
|
|
1417
1762
|
});
|
|
1418
1763
|
}
|
|
1419
1764
|
};
|
|
1420
1765
|
|
|
1421
1766
|
// src/platform/client.ts
|
|
1767
|
+
function createPlatformConfigFromEnv(overrides) {
|
|
1768
|
+
return {
|
|
1769
|
+
apiUrl: overrides?.apiUrl || process.env.MARVIN_API_URL || "http://localhost:8080",
|
|
1770
|
+
userToken: overrides?.userToken || process.env.MARVIN_USER_TOKEN,
|
|
1771
|
+
credentials: overrides?.credentials || "include"
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1422
1774
|
var PlatformClient = class extends HttpClient {
|
|
1423
1775
|
entries;
|
|
1424
1776
|
collections;
|
|
@@ -1445,7 +1797,7 @@ var PlatformClient = class extends HttpClient {
|
|
|
1445
1797
|
adminMaintenance;
|
|
1446
1798
|
constructor(config) {
|
|
1447
1799
|
const httpConfig = {
|
|
1448
|
-
baseUrl: config.apiUrl,
|
|
1800
|
+
baseUrl: config.apiUrl || "http://localhost:8080",
|
|
1449
1801
|
auth: new SessionAuth(config.userToken),
|
|
1450
1802
|
credentials: config.credentials || "include"
|
|
1451
1803
|
};
|
|
@@ -1475,7 +1827,8 @@ var PlatformClient = class extends HttpClient {
|
|
|
1475
1827
|
}
|
|
1476
1828
|
};
|
|
1477
1829
|
function createPlatformClient(config) {
|
|
1478
|
-
|
|
1830
|
+
const finalConfig = createPlatformConfigFromEnv(config);
|
|
1831
|
+
return new PlatformClient(finalConfig);
|
|
1479
1832
|
}
|
|
1480
1833
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1481
1834
|
0 && (module.exports = {
|
|
@@ -1502,5 +1855,6 @@ function createPlatformClient(config) {
|
|
|
1502
1855
|
WebhooksModule,
|
|
1503
1856
|
WorkspaceMembersModule,
|
|
1504
1857
|
WorkspacesModule,
|
|
1505
|
-
createPlatformClient
|
|
1858
|
+
createPlatformClient,
|
|
1859
|
+
createPlatformConfigFromEnv
|
|
1506
1860
|
});
|