@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/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,8 +345,8 @@ var HttpClient = class {
|
|
|
232
345
|
}
|
|
233
346
|
const data = await response.json();
|
|
234
347
|
if (this.debug) {
|
|
235
|
-
const
|
|
236
|
-
this.logger.log(
|
|
348
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
349
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
237
350
|
}
|
|
238
351
|
return data;
|
|
239
352
|
} catch (error) {
|
|
@@ -242,6 +355,17 @@ var HttpClient = class {
|
|
|
242
355
|
throw error;
|
|
243
356
|
}
|
|
244
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
|
+
}
|
|
245
369
|
if (error.name === "AbortError") {
|
|
246
370
|
throw new MarvinNetworkError(
|
|
247
371
|
`Request timeout after ${this.timeout}ms`,
|
|
@@ -296,10 +420,27 @@ var SessionAuth = class {
|
|
|
296
420
|
}
|
|
297
421
|
sessionToken;
|
|
298
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
|
+
}
|
|
299
437
|
injectAuth(headers) {
|
|
300
438
|
if (this.sessionToken) {
|
|
301
439
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
302
440
|
}
|
|
441
|
+
if (this.csrfToken) {
|
|
442
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
443
|
+
}
|
|
303
444
|
}
|
|
304
445
|
async handleUnauthorized() {
|
|
305
446
|
if (this.onUnauthorized) {
|
|
@@ -307,7 +448,9 @@ var SessionAuth = class {
|
|
|
307
448
|
}
|
|
308
449
|
}
|
|
309
450
|
getDescription() {
|
|
310
|
-
|
|
451
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
452
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
453
|
+
return auth + csrf;
|
|
311
454
|
}
|
|
312
455
|
};
|
|
313
456
|
|
|
@@ -327,7 +470,8 @@ var EntriesModule = class {
|
|
|
327
470
|
* Get a single entry by ID
|
|
328
471
|
*/
|
|
329
472
|
async get(id) {
|
|
330
|
-
|
|
473
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
474
|
+
return this.http.get(`/api/platform/entries/${validId}`);
|
|
331
475
|
}
|
|
332
476
|
/**
|
|
333
477
|
* Create a new entry
|
|
@@ -339,26 +483,31 @@ var EntriesModule = class {
|
|
|
339
483
|
* Update an entry
|
|
340
484
|
*/
|
|
341
485
|
async update(id, data) {
|
|
342
|
-
|
|
486
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
487
|
+
return this.http.patch(`/api/platform/entries/${validId}`, data);
|
|
343
488
|
}
|
|
344
489
|
/**
|
|
345
490
|
* Delete an entry
|
|
346
491
|
*/
|
|
347
492
|
async delete(id) {
|
|
348
|
-
|
|
493
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
494
|
+
return this.http.delete(`/api/platform/entries/${validId}`);
|
|
349
495
|
}
|
|
350
496
|
/**
|
|
351
497
|
* Get collections that this entry belongs to
|
|
352
498
|
*/
|
|
353
499
|
async listCollections(entryId) {
|
|
354
|
-
|
|
500
|
+
const validId = this.http.validatePathParam(entryId, "entry ID");
|
|
501
|
+
return this.http.get(`/api/platform/entries/${validId}/collections`);
|
|
355
502
|
}
|
|
356
503
|
/**
|
|
357
504
|
* Add entry to a collection
|
|
358
505
|
*/
|
|
359
506
|
async addToCollection(entryId, collectionId) {
|
|
507
|
+
const validEntryId = this.http.validatePathParam(entryId, "entry ID");
|
|
508
|
+
const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
|
|
360
509
|
return this.http.post(
|
|
361
|
-
`/api/platform/entries/${
|
|
510
|
+
`/api/platform/entries/${validEntryId}/collections/${validCollectionId}`,
|
|
362
511
|
{}
|
|
363
512
|
);
|
|
364
513
|
}
|
|
@@ -366,7 +515,9 @@ var EntriesModule = class {
|
|
|
366
515
|
* Remove entry from a collection
|
|
367
516
|
*/
|
|
368
517
|
async removeFromCollection(entryId, collectionId) {
|
|
369
|
-
|
|
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}`);
|
|
370
521
|
}
|
|
371
522
|
};
|
|
372
523
|
|
|
@@ -386,7 +537,8 @@ var CollectionsModule = class {
|
|
|
386
537
|
* Get a single collection by ID
|
|
387
538
|
*/
|
|
388
539
|
async get(id) {
|
|
389
|
-
|
|
540
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
541
|
+
return this.http.get(`/api/platform/collections/${validId}`);
|
|
390
542
|
}
|
|
391
543
|
/**
|
|
392
544
|
* Create a new collection
|
|
@@ -398,25 +550,29 @@ var CollectionsModule = class {
|
|
|
398
550
|
* Update a collection
|
|
399
551
|
*/
|
|
400
552
|
async update(id, data) {
|
|
401
|
-
|
|
553
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
554
|
+
return this.http.patch(`/api/platform/collections/${validId}`, data);
|
|
402
555
|
}
|
|
403
556
|
/**
|
|
404
557
|
* Delete a collection
|
|
405
558
|
*/
|
|
406
559
|
async delete(id) {
|
|
407
|
-
|
|
560
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
561
|
+
return this.http.delete(`/api/platform/collections/${validId}`);
|
|
408
562
|
}
|
|
409
563
|
/**
|
|
410
564
|
* Get entries in a collection
|
|
411
565
|
*/
|
|
412
566
|
async getEntries(id) {
|
|
413
|
-
|
|
567
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
568
|
+
return this.http.get(`/api/platform/collections/${validId}/entries`);
|
|
414
569
|
}
|
|
415
570
|
/**
|
|
416
571
|
* Reorder entries in a collection
|
|
417
572
|
*/
|
|
418
573
|
async reorderEntries(id, entries) {
|
|
419
|
-
|
|
574
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
575
|
+
return this.http.patch(`/api/platform/collections/${validId}/entries/order`, { entries });
|
|
420
576
|
}
|
|
421
577
|
};
|
|
422
578
|
|
|
@@ -436,7 +592,8 @@ var ResourcesModule = class {
|
|
|
436
592
|
* Get a single resource by ID
|
|
437
593
|
*/
|
|
438
594
|
async get(id) {
|
|
439
|
-
|
|
595
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
596
|
+
return this.http.get(`/api/platform/resources/${validId}`);
|
|
440
597
|
}
|
|
441
598
|
/**
|
|
442
599
|
* Create a new resource
|
|
@@ -448,7 +605,8 @@ var ResourcesModule = class {
|
|
|
448
605
|
* Update a resource
|
|
449
606
|
*/
|
|
450
607
|
async update(id, data) {
|
|
451
|
-
|
|
608
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
609
|
+
return this.http.patch(`/api/platform/resources/${validId}`, data);
|
|
452
610
|
}
|
|
453
611
|
/**
|
|
454
612
|
* Delete a resource
|
|
@@ -470,6 +628,47 @@ var AssetsModule = class {
|
|
|
470
628
|
this.http = http;
|
|
471
629
|
}
|
|
472
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
|
+
}
|
|
473
672
|
/**
|
|
474
673
|
* List all assets
|
|
475
674
|
*/
|
|
@@ -480,16 +679,31 @@ var AssetsModule = class {
|
|
|
480
679
|
* Get a single asset by ID
|
|
481
680
|
*/
|
|
482
681
|
async get(id) {
|
|
483
|
-
|
|
682
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
683
|
+
return this.http.get(`/api/platform/assets/${validId}`);
|
|
484
684
|
}
|
|
485
685
|
/**
|
|
486
686
|
* Upload a new asset file
|
|
487
687
|
*
|
|
488
688
|
* @param file - The file to upload (File or Blob)
|
|
489
689
|
* @param metadata - Asset metadata (slug, name, altText, description, metadata)
|
|
690
|
+
* @param options - Upload options (file size limits, allowed types)
|
|
490
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
|
+
* ```
|
|
491
704
|
*/
|
|
492
|
-
async upload(file, metadata) {
|
|
705
|
+
async upload(file, metadata, options) {
|
|
706
|
+
this.validateFile(file, options);
|
|
493
707
|
const formData = new FormData();
|
|
494
708
|
formData.append("file", file);
|
|
495
709
|
formData.append("slug", metadata.slug);
|
|
@@ -509,13 +723,24 @@ var AssetsModule = class {
|
|
|
509
723
|
* Update an asset (editable fields only: slug, name, altText, description, metadata)
|
|
510
724
|
*/
|
|
511
725
|
async update(id, data) {
|
|
512
|
-
|
|
726
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
727
|
+
return this.http.patch(`/api/platform/assets/${validId}`, data);
|
|
513
728
|
}
|
|
514
729
|
/**
|
|
515
730
|
* Delete an asset (removes file from storage and database)
|
|
516
731
|
*/
|
|
517
732
|
async delete(id) {
|
|
518
|
-
|
|
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`;
|
|
519
744
|
}
|
|
520
745
|
};
|
|
521
746
|
|
|
@@ -535,7 +760,8 @@ var APIClientsModule = class {
|
|
|
535
760
|
* Get a single API client by ID
|
|
536
761
|
*/
|
|
537
762
|
async get(id) {
|
|
538
|
-
|
|
763
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
764
|
+
return this.http.get(`/api/platform/api-clients/${validId}`);
|
|
539
765
|
}
|
|
540
766
|
/**
|
|
541
767
|
* Create a new API client (returns client with token)
|
|
@@ -547,7 +773,8 @@ var APIClientsModule = class {
|
|
|
547
773
|
* Update an API client
|
|
548
774
|
*/
|
|
549
775
|
async update(id, data) {
|
|
550
|
-
|
|
776
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
777
|
+
return this.http.patch(`/api/platform/api-clients/${validId}`, data);
|
|
551
778
|
}
|
|
552
779
|
/**
|
|
553
780
|
* Delete an API client
|
|
@@ -758,11 +985,23 @@ var InvitesModule = class {
|
|
|
758
985
|
}
|
|
759
986
|
/**
|
|
760
987
|
* Generate invitation URL from a token
|
|
761
|
-
*
|
|
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
|
+
* ```
|
|
762
1001
|
*/
|
|
763
1002
|
getInvitationUrl(token, baseUrl) {
|
|
764
1003
|
let base = baseUrl;
|
|
765
|
-
if (!base) {
|
|
1004
|
+
if (!base && typeof process !== "undefined" && process.env) {
|
|
766
1005
|
base = process.env.MARVIN_FRONTEND_URL;
|
|
767
1006
|
if (!base && process.env.MARVIN_API_URL) {
|
|
768
1007
|
const apiUrl = process.env.MARVIN_API_URL;
|
|
@@ -774,11 +1013,17 @@ var InvitesModule = class {
|
|
|
774
1013
|
base = apiUrl;
|
|
775
1014
|
}
|
|
776
1015
|
}
|
|
777
|
-
|
|
778
|
-
|
|
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
|
+
);
|
|
779
1024
|
}
|
|
780
1025
|
}
|
|
781
|
-
return `${base}/register?token=${token}`;
|
|
1026
|
+
return `${base}/register?token=${encodeURIComponent(token)}`;
|
|
782
1027
|
}
|
|
783
1028
|
};
|
|
784
1029
|
|
|
@@ -832,6 +1077,57 @@ var WebhooksModule = class {
|
|
|
832
1077
|
this.http = http;
|
|
833
1078
|
}
|
|
834
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
|
+
}
|
|
835
1131
|
/**
|
|
836
1132
|
* List all webhooks for the current workspace
|
|
837
1133
|
*/
|
|
@@ -843,31 +1139,43 @@ var WebhooksModule = class {
|
|
|
843
1139
|
* Get a webhook by ID
|
|
844
1140
|
*/
|
|
845
1141
|
async get(id) {
|
|
846
|
-
|
|
1142
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1143
|
+
return this.http.get(`/api/groups/webhooks/${validId}`);
|
|
847
1144
|
}
|
|
848
1145
|
/**
|
|
849
1146
|
* Create a new webhook
|
|
1147
|
+
* Validates URL to prevent SSRF attacks
|
|
850
1148
|
*/
|
|
851
1149
|
async create(data) {
|
|
1150
|
+
if (data.url) {
|
|
1151
|
+
this.validateWebhookUrl(data.url);
|
|
1152
|
+
}
|
|
852
1153
|
return this.http.post("/api/groups/webhooks", data);
|
|
853
1154
|
}
|
|
854
1155
|
/**
|
|
855
1156
|
* Update a webhook
|
|
1157
|
+
* Validates URL if provided to prevent SSRF attacks
|
|
856
1158
|
*/
|
|
857
1159
|
async update(id, data) {
|
|
858
|
-
|
|
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);
|
|
859
1165
|
}
|
|
860
1166
|
/**
|
|
861
1167
|
* Delete a webhook
|
|
862
1168
|
*/
|
|
863
1169
|
async delete(id) {
|
|
864
|
-
|
|
1170
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1171
|
+
await this.http.delete(`/api/groups/webhooks/${validId}`);
|
|
865
1172
|
}
|
|
866
1173
|
/**
|
|
867
1174
|
* Test a webhook
|
|
868
1175
|
*/
|
|
869
1176
|
async test(id) {
|
|
870
|
-
|
|
1177
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1178
|
+
return this.http.post(`/api/groups/webhooks/${validId}/test`, {});
|
|
871
1179
|
}
|
|
872
1180
|
/**
|
|
873
1181
|
* Rerun failed webhooks
|
|
@@ -959,6 +1267,7 @@ var AdminSystemModule = class {
|
|
|
959
1267
|
}
|
|
960
1268
|
/**
|
|
961
1269
|
* Update email settings
|
|
1270
|
+
* Note: Password field is write-only and will not be returned in response
|
|
962
1271
|
*/
|
|
963
1272
|
async updateEmailSettings(data) {
|
|
964
1273
|
return this.http.put("/api/admin/email", data);
|
|
@@ -1309,7 +1618,8 @@ var ScheduledTasksModule = class {
|
|
|
1309
1618
|
* Get a scheduled task by ID or slug
|
|
1310
1619
|
*/
|
|
1311
1620
|
async get(idOrSlug) {
|
|
1312
|
-
|
|
1621
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1622
|
+
return this.http.get(`/api/platform/scheduled-tasks/${validIdOrSlug}`);
|
|
1313
1623
|
}
|
|
1314
1624
|
/**
|
|
1315
1625
|
* Create a new scheduled task
|
|
@@ -1321,7 +1631,8 @@ var ScheduledTasksModule = class {
|
|
|
1321
1631
|
* Update a scheduled task
|
|
1322
1632
|
*/
|
|
1323
1633
|
async update(idOrSlug, data) {
|
|
1324
|
-
|
|
1634
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1635
|
+
return this.http.patch(`/api/platform/scheduled-tasks/${validIdOrSlug}`, data);
|
|
1325
1636
|
}
|
|
1326
1637
|
/**
|
|
1327
1638
|
* Delete a scheduled task
|
|
@@ -1367,6 +1678,34 @@ var FormsModule = class {
|
|
|
1367
1678
|
this.http = http;
|
|
1368
1679
|
}
|
|
1369
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
|
+
}
|
|
1370
1709
|
/**
|
|
1371
1710
|
* List all forms
|
|
1372
1711
|
*/
|
|
@@ -1377,7 +1716,8 @@ var FormsModule = class {
|
|
|
1377
1716
|
* Get a single form by ID
|
|
1378
1717
|
*/
|
|
1379
1718
|
async get(id) {
|
|
1380
|
-
|
|
1719
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1720
|
+
return this.http.get(`/api/platform/forms/${validId}`);
|
|
1381
1721
|
}
|
|
1382
1722
|
/**
|
|
1383
1723
|
* Create a new form
|
|
@@ -1389,7 +1729,8 @@ var FormsModule = class {
|
|
|
1389
1729
|
* Update a form
|
|
1390
1730
|
*/
|
|
1391
1731
|
async update(id, data) {
|
|
1392
|
-
|
|
1732
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1733
|
+
return this.http.patch(`/api/platform/forms/${validId}`, data);
|
|
1393
1734
|
}
|
|
1394
1735
|
/**
|
|
1395
1736
|
* Delete a form
|
|
@@ -1411,15 +1752,25 @@ var FormsModule = class {
|
|
|
1411
1752
|
}
|
|
1412
1753
|
/**
|
|
1413
1754
|
* Submit data to a published form (Publishing API - public access)
|
|
1755
|
+
* Validates data before submission to prevent XSS and malicious content
|
|
1414
1756
|
*/
|
|
1415
1757
|
async submitForm(slug, data) {
|
|
1416
|
-
|
|
1758
|
+
const validSlug = this.http.validatePathParam(slug, "form slug");
|
|
1759
|
+
this.validateFormData(data);
|
|
1760
|
+
return this.http.post(`/api/publish/forms/${validSlug}/submit`, {
|
|
1417
1761
|
dataJson: data
|
|
1418
1762
|
});
|
|
1419
1763
|
}
|
|
1420
1764
|
};
|
|
1421
1765
|
|
|
1422
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
|
+
}
|
|
1423
1774
|
var PlatformClient = class extends HttpClient {
|
|
1424
1775
|
entries;
|
|
1425
1776
|
collections;
|
|
@@ -1446,7 +1797,7 @@ var PlatformClient = class extends HttpClient {
|
|
|
1446
1797
|
adminMaintenance;
|
|
1447
1798
|
constructor(config) {
|
|
1448
1799
|
const httpConfig = {
|
|
1449
|
-
baseUrl: config.apiUrl,
|
|
1800
|
+
baseUrl: config.apiUrl || "http://localhost:8080",
|
|
1450
1801
|
auth: new SessionAuth(config.userToken),
|
|
1451
1802
|
credentials: config.credentials || "include"
|
|
1452
1803
|
};
|
|
@@ -1476,7 +1827,8 @@ var PlatformClient = class extends HttpClient {
|
|
|
1476
1827
|
}
|
|
1477
1828
|
};
|
|
1478
1829
|
function createPlatformClient(config) {
|
|
1479
|
-
|
|
1830
|
+
const finalConfig = createPlatformConfigFromEnv(config);
|
|
1831
|
+
return new PlatformClient(finalConfig);
|
|
1480
1832
|
}
|
|
1481
1833
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1482
1834
|
0 && (module.exports = {
|
|
@@ -1503,5 +1855,6 @@ function createPlatformClient(config) {
|
|
|
1503
1855
|
WebhooksModule,
|
|
1504
1856
|
WorkspaceMembersModule,
|
|
1505
1857
|
WorkspacesModule,
|
|
1506
|
-
createPlatformClient
|
|
1858
|
+
createPlatformClient,
|
|
1859
|
+
createPlatformConfigFromEnv
|
|
1507
1860
|
});
|