@inneropen/marvin-sdk 2.0.0-next.2 → 2.0.0-next.20
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-CLtgQGXH.d.cts → HttpClient-CdF3IHM1.d.cts} +49 -2
- package/dist/{HttpClient-CLtgQGXH.d.ts → HttpClient-CdF3IHM1.d.ts} +49 -2
- package/dist/chunk-TZ2TZFWP.js +1009 -0
- package/dist/chunk-WJC4X7TB.js +738 -0
- package/dist/index.cjs +565 -25
- package/dist/index.d.cts +20 -544
- package/dist/index.d.ts +20 -544
- package/dist/index.js +27 -791
- package/dist/marvin-BAslemIz.d.cts +666 -0
- package/dist/marvin-BmbMUJXh.d.ts +666 -0
- package/dist/platform.cjs +681 -42
- package/dist/platform.d.cts +968 -94
- package/dist/platform.d.ts +968 -94
- package/dist/platform.js +449 -32
- package/dist/publish.cjs +1509 -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 +46 -1
- package/dist/types/index.d.ts +46 -1
- package/package.json +18 -7
- package/dist/chunk-25F3IF6S.js +0 -396
package/dist/platform.cjs
CHANGED
|
@@ -27,20 +27,24 @@ __export(platform_exports, {
|
|
|
27
27
|
AssetsModule: () => AssetsModule,
|
|
28
28
|
AuthModule: () => AuthModule,
|
|
29
29
|
CollectionsModule: () => CollectionsModule,
|
|
30
|
+
EmailTemplatesClient: () => EmailTemplatesClient,
|
|
30
31
|
EntriesModule: () => EntriesModule,
|
|
31
32
|
EntryTypesModule: () => EntryTypesModule,
|
|
32
33
|
EventLogModule: () => EventLogModule,
|
|
33
34
|
EventsModule: () => EventsModule,
|
|
35
|
+
FormsModule: () => FormsModule,
|
|
34
36
|
InvitesModule: () => InvitesModule,
|
|
35
37
|
NotificationsModule: () => NotificationsModule,
|
|
36
38
|
PlatformClient: () => PlatformClient,
|
|
37
39
|
ResourcesModule: () => ResourcesModule,
|
|
40
|
+
ScheduledTasksModule: () => ScheduledTasksModule,
|
|
38
41
|
ThemeModule: () => ThemeModule,
|
|
39
42
|
UserModule: () => UserModule,
|
|
40
43
|
WebhooksModule: () => WebhooksModule,
|
|
41
44
|
WorkspaceMembersModule: () => WorkspaceMembersModule,
|
|
42
45
|
WorkspacesModule: () => WorkspacesModule,
|
|
43
|
-
createPlatformClient: () => createPlatformClient
|
|
46
|
+
createPlatformClient: () => createPlatformClient,
|
|
47
|
+
createPlatformConfigFromEnv: () => createPlatformConfigFromEnv
|
|
44
48
|
});
|
|
45
49
|
module.exports = __toCommonJS(platform_exports);
|
|
46
50
|
|
|
@@ -74,6 +78,49 @@ ${body}` : ""}`;
|
|
|
74
78
|
return new _MarvinApiError(message, status, endpoint, body);
|
|
75
79
|
}
|
|
76
80
|
};
|
|
81
|
+
var MarvinAuthError = class _MarvinAuthError extends MarvinError {
|
|
82
|
+
constructor(message, statusCode) {
|
|
83
|
+
super(message, "AUTH_ERROR", statusCode);
|
|
84
|
+
this.name = "MarvinAuthError";
|
|
85
|
+
Object.setPrototypeOf(this, _MarvinAuthError.prototype);
|
|
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
|
+
};
|
|
97
|
+
var MarvinNotFoundError = class _MarvinNotFoundError extends MarvinError {
|
|
98
|
+
constructor(message, endpoint) {
|
|
99
|
+
super(message, "NOT_FOUND", 404);
|
|
100
|
+
this.endpoint = endpoint;
|
|
101
|
+
this.name = "MarvinNotFoundError";
|
|
102
|
+
Object.setPrototypeOf(this, _MarvinNotFoundError.prototype);
|
|
103
|
+
}
|
|
104
|
+
endpoint;
|
|
105
|
+
};
|
|
106
|
+
var MarvinNetworkError = class _MarvinNetworkError extends MarvinError {
|
|
107
|
+
constructor(message, originalError) {
|
|
108
|
+
super(message, "NETWORK_ERROR");
|
|
109
|
+
this.originalError = originalError;
|
|
110
|
+
this.name = "MarvinNetworkError";
|
|
111
|
+
Object.setPrototypeOf(this, _MarvinNetworkError.prototype);
|
|
112
|
+
}
|
|
113
|
+
originalError;
|
|
114
|
+
};
|
|
115
|
+
var MarvinServerError = class _MarvinServerError extends MarvinError {
|
|
116
|
+
constructor(message, statusCode, endpoint) {
|
|
117
|
+
super(message, "SERVER_ERROR", statusCode);
|
|
118
|
+
this.endpoint = endpoint;
|
|
119
|
+
this.name = "MarvinServerError";
|
|
120
|
+
Object.setPrototypeOf(this, _MarvinServerError.prototype);
|
|
121
|
+
}
|
|
122
|
+
endpoint;
|
|
123
|
+
};
|
|
77
124
|
|
|
78
125
|
// src/core/http/HttpClient.ts
|
|
79
126
|
var HttpClient = class {
|
|
@@ -82,12 +129,47 @@ var HttpClient = class {
|
|
|
82
129
|
defaultHeaders;
|
|
83
130
|
credentials;
|
|
84
131
|
timeout;
|
|
132
|
+
debug;
|
|
133
|
+
retryConfig;
|
|
134
|
+
logger;
|
|
85
135
|
constructor(config) {
|
|
86
136
|
this.baseUrl = config.baseUrl;
|
|
87
137
|
this.auth = config.auth;
|
|
88
138
|
this.defaultHeaders = config.defaultHeaders || {};
|
|
89
139
|
this.credentials = config.credentials || "same-origin";
|
|
90
|
-
|
|
140
|
+
const MAX_TIMEOUT = 12e4;
|
|
141
|
+
this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
|
|
142
|
+
this.debug = config.debug ?? false;
|
|
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;
|
|
91
173
|
}
|
|
92
174
|
/**
|
|
93
175
|
* Build full URL from endpoint
|
|
@@ -121,14 +203,78 @@ var HttpClient = class {
|
|
|
121
203
|
return filtered.length > 0 ? `?${filtered.join("&")}` : "";
|
|
122
204
|
}
|
|
123
205
|
/**
|
|
124
|
-
*
|
|
206
|
+
* Sanitize data for logging by redacting sensitive fields
|
|
207
|
+
*/
|
|
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
|
|
125
261
|
*/
|
|
126
|
-
|
|
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) {
|
|
127
269
|
const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
|
|
128
270
|
const isFormData = options?.body instanceof FormData;
|
|
129
271
|
const headers = this.buildHeaders(options?.headers, isFormData);
|
|
130
272
|
const controller = new AbortController();
|
|
131
273
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
274
|
+
const startTime = Date.now();
|
|
275
|
+
if (this.debug) {
|
|
276
|
+
this.logger.log(`[Marvin] ${method} ${url}`);
|
|
277
|
+
}
|
|
132
278
|
try {
|
|
133
279
|
const response = await fetch(url, {
|
|
134
280
|
method,
|
|
@@ -138,11 +284,54 @@ var HttpClient = class {
|
|
|
138
284
|
signal: controller.signal
|
|
139
285
|
});
|
|
140
286
|
clearTimeout(timeoutId);
|
|
287
|
+
const duration = Date.now() - startTime;
|
|
288
|
+
if (this.debug) {
|
|
289
|
+
this.logger.log(
|
|
290
|
+
`[Marvin] ${response.status} ${response.statusText} (${duration}ms)`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
141
293
|
if (response.status === 401 && this.auth.handleUnauthorized) {
|
|
142
294
|
await this.auth.handleUnauthorized();
|
|
143
295
|
}
|
|
144
296
|
if (!response.ok) {
|
|
145
297
|
const errorText = await response.text().catch(() => "");
|
|
298
|
+
if (this.debug) {
|
|
299
|
+
this.logger.error(
|
|
300
|
+
`[Marvin] Error response: ${response.status} ${response.statusText}`,
|
|
301
|
+
errorText ? `Body: ${errorText}` : ""
|
|
302
|
+
);
|
|
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
|
+
}
|
|
316
|
+
if (response.status === 404) {
|
|
317
|
+
throw new MarvinNotFoundError(
|
|
318
|
+
`Resource not found: ${endpoint}`,
|
|
319
|
+
endpoint
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (response.status === 401 || response.status === 403) {
|
|
323
|
+
throw new MarvinAuthError(
|
|
324
|
+
`Authentication failed: ${response.statusText}`,
|
|
325
|
+
response.status
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (response.status >= 500) {
|
|
329
|
+
throw new MarvinServerError(
|
|
330
|
+
`Server error: ${response.status} ${response.statusText}`,
|
|
331
|
+
response.status,
|
|
332
|
+
endpoint
|
|
333
|
+
);
|
|
334
|
+
}
|
|
146
335
|
throw MarvinApiError.fromResponse(
|
|
147
336
|
response.status,
|
|
148
337
|
response.statusText,
|
|
@@ -154,21 +343,39 @@ var HttpClient = class {
|
|
|
154
343
|
if (!contentType || !contentType.includes("application/json")) {
|
|
155
344
|
return void 0;
|
|
156
345
|
}
|
|
157
|
-
|
|
346
|
+
const data = await response.json();
|
|
347
|
+
if (this.debug) {
|
|
348
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
349
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
350
|
+
}
|
|
351
|
+
return data;
|
|
158
352
|
} catch (error) {
|
|
159
353
|
clearTimeout(timeoutId);
|
|
160
|
-
if (error instanceof MarvinApiError) {
|
|
354
|
+
if (error instanceof MarvinApiError || error instanceof MarvinNotFoundError || error instanceof MarvinAuthError || error instanceof MarvinServerError) {
|
|
161
355
|
throw error;
|
|
162
356
|
}
|
|
163
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
|
+
}
|
|
164
369
|
if (error.name === "AbortError") {
|
|
165
|
-
throw new
|
|
370
|
+
throw new MarvinNetworkError(
|
|
166
371
|
`Request timeout after ${this.timeout}ms`,
|
|
167
|
-
|
|
168
|
-
endpoint
|
|
372
|
+
error
|
|
169
373
|
);
|
|
170
374
|
}
|
|
171
|
-
throw new
|
|
375
|
+
throw new MarvinNetworkError(
|
|
376
|
+
`Network error: ${error.message}`,
|
|
377
|
+
error
|
|
378
|
+
);
|
|
172
379
|
}
|
|
173
380
|
throw error;
|
|
174
381
|
}
|
|
@@ -213,10 +420,27 @@ var SessionAuth = class {
|
|
|
213
420
|
}
|
|
214
421
|
sessionToken;
|
|
215
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
|
+
}
|
|
216
437
|
injectAuth(headers) {
|
|
217
438
|
if (this.sessionToken) {
|
|
218
439
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
219
440
|
}
|
|
441
|
+
if (this.csrfToken) {
|
|
442
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
443
|
+
}
|
|
220
444
|
}
|
|
221
445
|
async handleUnauthorized() {
|
|
222
446
|
if (this.onUnauthorized) {
|
|
@@ -224,7 +448,9 @@ var SessionAuth = class {
|
|
|
224
448
|
}
|
|
225
449
|
}
|
|
226
450
|
getDescription() {
|
|
227
|
-
|
|
451
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
452
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
453
|
+
return auth + csrf;
|
|
228
454
|
}
|
|
229
455
|
};
|
|
230
456
|
|
|
@@ -244,7 +470,8 @@ var EntriesModule = class {
|
|
|
244
470
|
* Get a single entry by ID
|
|
245
471
|
*/
|
|
246
472
|
async get(id) {
|
|
247
|
-
|
|
473
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
474
|
+
return this.http.get(`/api/platform/entries/${validId}`);
|
|
248
475
|
}
|
|
249
476
|
/**
|
|
250
477
|
* Create a new entry
|
|
@@ -256,26 +483,31 @@ var EntriesModule = class {
|
|
|
256
483
|
* Update an entry
|
|
257
484
|
*/
|
|
258
485
|
async update(id, data) {
|
|
259
|
-
|
|
486
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
487
|
+
return this.http.patch(`/api/platform/entries/${validId}`, data);
|
|
260
488
|
}
|
|
261
489
|
/**
|
|
262
490
|
* Delete an entry
|
|
263
491
|
*/
|
|
264
492
|
async delete(id) {
|
|
265
|
-
|
|
493
|
+
const validId = this.http.validatePathParam(id, "entry ID");
|
|
494
|
+
return this.http.delete(`/api/platform/entries/${validId}`);
|
|
266
495
|
}
|
|
267
496
|
/**
|
|
268
497
|
* Get collections that this entry belongs to
|
|
269
498
|
*/
|
|
270
499
|
async listCollections(entryId) {
|
|
271
|
-
|
|
500
|
+
const validId = this.http.validatePathParam(entryId, "entry ID");
|
|
501
|
+
return this.http.get(`/api/platform/entries/${validId}/collections`);
|
|
272
502
|
}
|
|
273
503
|
/**
|
|
274
504
|
* Add entry to a collection
|
|
275
505
|
*/
|
|
276
506
|
async addToCollection(entryId, collectionId) {
|
|
507
|
+
const validEntryId = this.http.validatePathParam(entryId, "entry ID");
|
|
508
|
+
const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
|
|
277
509
|
return this.http.post(
|
|
278
|
-
`/api/platform/entries/${
|
|
510
|
+
`/api/platform/entries/${validEntryId}/collections/${validCollectionId}`,
|
|
279
511
|
{}
|
|
280
512
|
);
|
|
281
513
|
}
|
|
@@ -283,7 +515,9 @@ var EntriesModule = class {
|
|
|
283
515
|
* Remove entry from a collection
|
|
284
516
|
*/
|
|
285
517
|
async removeFromCollection(entryId, collectionId) {
|
|
286
|
-
|
|
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}`);
|
|
287
521
|
}
|
|
288
522
|
};
|
|
289
523
|
|
|
@@ -303,7 +537,8 @@ var CollectionsModule = class {
|
|
|
303
537
|
* Get a single collection by ID
|
|
304
538
|
*/
|
|
305
539
|
async get(id) {
|
|
306
|
-
|
|
540
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
541
|
+
return this.http.get(`/api/platform/collections/${validId}`);
|
|
307
542
|
}
|
|
308
543
|
/**
|
|
309
544
|
* Create a new collection
|
|
@@ -315,25 +550,29 @@ var CollectionsModule = class {
|
|
|
315
550
|
* Update a collection
|
|
316
551
|
*/
|
|
317
552
|
async update(id, data) {
|
|
318
|
-
|
|
553
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
554
|
+
return this.http.patch(`/api/platform/collections/${validId}`, data);
|
|
319
555
|
}
|
|
320
556
|
/**
|
|
321
557
|
* Delete a collection
|
|
322
558
|
*/
|
|
323
559
|
async delete(id) {
|
|
324
|
-
|
|
560
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
561
|
+
return this.http.delete(`/api/platform/collections/${validId}`);
|
|
325
562
|
}
|
|
326
563
|
/**
|
|
327
564
|
* Get entries in a collection
|
|
328
565
|
*/
|
|
329
566
|
async getEntries(id) {
|
|
330
|
-
|
|
567
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
568
|
+
return this.http.get(`/api/platform/collections/${validId}/entries`);
|
|
331
569
|
}
|
|
332
570
|
/**
|
|
333
571
|
* Reorder entries in a collection
|
|
334
572
|
*/
|
|
335
573
|
async reorderEntries(id, entries) {
|
|
336
|
-
|
|
574
|
+
const validId = this.http.validatePathParam(id, "collection ID");
|
|
575
|
+
return this.http.patch(`/api/platform/collections/${validId}/entries/order`, { entries });
|
|
337
576
|
}
|
|
338
577
|
};
|
|
339
578
|
|
|
@@ -353,7 +592,8 @@ var ResourcesModule = class {
|
|
|
353
592
|
* Get a single resource by ID
|
|
354
593
|
*/
|
|
355
594
|
async get(id) {
|
|
356
|
-
|
|
595
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
596
|
+
return this.http.get(`/api/platform/resources/${validId}`);
|
|
357
597
|
}
|
|
358
598
|
/**
|
|
359
599
|
* Create a new resource
|
|
@@ -365,7 +605,8 @@ var ResourcesModule = class {
|
|
|
365
605
|
* Update a resource
|
|
366
606
|
*/
|
|
367
607
|
async update(id, data) {
|
|
368
|
-
|
|
608
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
609
|
+
return this.http.patch(`/api/platform/resources/${validId}`, data);
|
|
369
610
|
}
|
|
370
611
|
/**
|
|
371
612
|
* Delete a resource
|
|
@@ -387,6 +628,47 @@ var AssetsModule = class {
|
|
|
387
628
|
this.http = http;
|
|
388
629
|
}
|
|
389
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
|
+
}
|
|
390
672
|
/**
|
|
391
673
|
* List all assets
|
|
392
674
|
*/
|
|
@@ -397,16 +679,31 @@ var AssetsModule = class {
|
|
|
397
679
|
* Get a single asset by ID
|
|
398
680
|
*/
|
|
399
681
|
async get(id) {
|
|
400
|
-
|
|
682
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
683
|
+
return this.http.get(`/api/platform/assets/${validId}`);
|
|
401
684
|
}
|
|
402
685
|
/**
|
|
403
686
|
* Upload a new asset file
|
|
404
687
|
*
|
|
405
688
|
* @param file - The file to upload (File or Blob)
|
|
406
689
|
* @param metadata - Asset metadata (slug, name, altText, description, metadata)
|
|
690
|
+
* @param options - Upload options (file size limits, allowed types)
|
|
407
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
|
+
* ```
|
|
408
704
|
*/
|
|
409
|
-
async upload(file, metadata) {
|
|
705
|
+
async upload(file, metadata, options) {
|
|
706
|
+
this.validateFile(file, options);
|
|
410
707
|
const formData = new FormData();
|
|
411
708
|
formData.append("file", file);
|
|
412
709
|
formData.append("slug", metadata.slug);
|
|
@@ -426,13 +723,24 @@ var AssetsModule = class {
|
|
|
426
723
|
* Update an asset (editable fields only: slug, name, altText, description, metadata)
|
|
427
724
|
*/
|
|
428
725
|
async update(id, data) {
|
|
429
|
-
|
|
726
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
727
|
+
return this.http.patch(`/api/platform/assets/${validId}`, data);
|
|
430
728
|
}
|
|
431
729
|
/**
|
|
432
730
|
* Delete an asset (removes file from storage and database)
|
|
433
731
|
*/
|
|
434
732
|
async delete(id) {
|
|
435
|
-
|
|
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`;
|
|
436
744
|
}
|
|
437
745
|
};
|
|
438
746
|
|
|
@@ -452,7 +760,8 @@ var APIClientsModule = class {
|
|
|
452
760
|
* Get a single API client by ID
|
|
453
761
|
*/
|
|
454
762
|
async get(id) {
|
|
455
|
-
|
|
763
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
764
|
+
return this.http.get(`/api/platform/api-clients/${validId}`);
|
|
456
765
|
}
|
|
457
766
|
/**
|
|
458
767
|
* Create a new API client (returns client with token)
|
|
@@ -464,7 +773,8 @@ var APIClientsModule = class {
|
|
|
464
773
|
* Update an API client
|
|
465
774
|
*/
|
|
466
775
|
async update(id, data) {
|
|
467
|
-
|
|
776
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
777
|
+
return this.http.patch(`/api/platform/api-clients/${validId}`, data);
|
|
468
778
|
}
|
|
469
779
|
/**
|
|
470
780
|
* Delete an API client
|
|
@@ -628,6 +938,15 @@ var WorkspacesModule = class {
|
|
|
628
938
|
const url = `/api/admin/groups/${id}${force ? "?force=true" : ""}`;
|
|
629
939
|
await this.http.delete(url);
|
|
630
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Export workspace data as JSON (collections, entry types, entries, site config)
|
|
943
|
+
*/
|
|
944
|
+
async export(options) {
|
|
945
|
+
const pretty = options?.pretty ?? true;
|
|
946
|
+
const endpoint = pretty ? "/api/platform/workspace/export/pretty" : "/api/platform/workspace/export";
|
|
947
|
+
const params = options?.includeSystemTypes ? { include_system_types: true } : void 0;
|
|
948
|
+
return this.http.get(endpoint, params);
|
|
949
|
+
}
|
|
631
950
|
/**
|
|
632
951
|
* Get workspace preferences
|
|
633
952
|
*/
|
|
@@ -675,11 +994,23 @@ var InvitesModule = class {
|
|
|
675
994
|
}
|
|
676
995
|
/**
|
|
677
996
|
* Generate invitation URL from a token
|
|
678
|
-
*
|
|
997
|
+
*
|
|
998
|
+
* @param token - Invitation token
|
|
999
|
+
* @param baseUrl - Base URL for the frontend (required in browser environments)
|
|
1000
|
+
* @returns Full invitation URL
|
|
1001
|
+
*
|
|
1002
|
+
* @example
|
|
1003
|
+
* ```typescript
|
|
1004
|
+
* // Node.js (can use env vars)
|
|
1005
|
+
* const url = invites.getInvitationUrl(token);
|
|
1006
|
+
*
|
|
1007
|
+
* // Browser (must provide baseUrl)
|
|
1008
|
+
* const url = invites.getInvitationUrl(token, 'https://app.example.com');
|
|
1009
|
+
* ```
|
|
679
1010
|
*/
|
|
680
1011
|
getInvitationUrl(token, baseUrl) {
|
|
681
1012
|
let base = baseUrl;
|
|
682
|
-
if (!base) {
|
|
1013
|
+
if (!base && typeof process !== "undefined" && process.env) {
|
|
683
1014
|
base = process.env.MARVIN_FRONTEND_URL;
|
|
684
1015
|
if (!base && process.env.MARVIN_API_URL) {
|
|
685
1016
|
const apiUrl = process.env.MARVIN_API_URL;
|
|
@@ -691,11 +1022,17 @@ var InvitesModule = class {
|
|
|
691
1022
|
base = apiUrl;
|
|
692
1023
|
}
|
|
693
1024
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1025
|
+
}
|
|
1026
|
+
if (!base) {
|
|
1027
|
+
if (typeof window !== "undefined" && window.location) {
|
|
1028
|
+
base = window.location.origin;
|
|
1029
|
+
} else {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
'baseUrl is required when not in Node.js environment. Pass the frontend URL as second argument: getInvitationUrl(token, "https://app.example.com")'
|
|
1032
|
+
);
|
|
696
1033
|
}
|
|
697
1034
|
}
|
|
698
|
-
return `${base}/register?token=${token}`;
|
|
1035
|
+
return `${base}/register?token=${encodeURIComponent(token)}`;
|
|
699
1036
|
}
|
|
700
1037
|
};
|
|
701
1038
|
|
|
@@ -749,6 +1086,57 @@ var WebhooksModule = class {
|
|
|
749
1086
|
this.http = http;
|
|
750
1087
|
}
|
|
751
1088
|
http;
|
|
1089
|
+
/**
|
|
1090
|
+
* Validate webhook URL to prevent SSRF attacks
|
|
1091
|
+
*/
|
|
1092
|
+
validateWebhookUrl(url) {
|
|
1093
|
+
if (!url || typeof url !== "string") {
|
|
1094
|
+
throw new MarvinValidationError("Webhook URL is required and must be a string");
|
|
1095
|
+
}
|
|
1096
|
+
let parsed;
|
|
1097
|
+
try {
|
|
1098
|
+
parsed = new URL(url);
|
|
1099
|
+
} catch {
|
|
1100
|
+
throw new MarvinValidationError("Invalid webhook URL format. Expected: https://example.com/webhook");
|
|
1101
|
+
}
|
|
1102
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
1103
|
+
throw new MarvinValidationError(
|
|
1104
|
+
`Webhook URL must use HTTP or HTTPS protocol. Got: ${parsed.protocol}`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1108
|
+
const blockedHosts = [
|
|
1109
|
+
"localhost",
|
|
1110
|
+
"127.0.0.1",
|
|
1111
|
+
"0.0.0.0",
|
|
1112
|
+
"::1",
|
|
1113
|
+
"169.254.169.254",
|
|
1114
|
+
// AWS metadata service
|
|
1115
|
+
"[::]"
|
|
1116
|
+
];
|
|
1117
|
+
if (blockedHosts.some((blocked) => hostname === blocked || hostname.includes(blocked))) {
|
|
1118
|
+
throw new MarvinValidationError(
|
|
1119
|
+
"Webhook URL cannot target localhost or internal network addresses"
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1123
|
+
const match = hostname.match(ipv4Regex);
|
|
1124
|
+
if (match) {
|
|
1125
|
+
const octets = match.slice(1).map(Number);
|
|
1126
|
+
if (octets[0] === 10) {
|
|
1127
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (10.x.x.x)");
|
|
1128
|
+
}
|
|
1129
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
1130
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (172.16-31.x.x)");
|
|
1131
|
+
}
|
|
1132
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
1133
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (192.168.x.x)");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (url.length > 2048) {
|
|
1137
|
+
throw new MarvinValidationError("Webhook URL exceeds maximum length of 2048 characters");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
752
1140
|
/**
|
|
753
1141
|
* List all webhooks for the current workspace
|
|
754
1142
|
*/
|
|
@@ -760,31 +1148,43 @@ var WebhooksModule = class {
|
|
|
760
1148
|
* Get a webhook by ID
|
|
761
1149
|
*/
|
|
762
1150
|
async get(id) {
|
|
763
|
-
|
|
1151
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1152
|
+
return this.http.get(`/api/groups/webhooks/${validId}`);
|
|
764
1153
|
}
|
|
765
1154
|
/**
|
|
766
1155
|
* Create a new webhook
|
|
1156
|
+
* Validates URL to prevent SSRF attacks
|
|
767
1157
|
*/
|
|
768
1158
|
async create(data) {
|
|
1159
|
+
if (data.url) {
|
|
1160
|
+
this.validateWebhookUrl(data.url);
|
|
1161
|
+
}
|
|
769
1162
|
return this.http.post("/api/groups/webhooks", data);
|
|
770
1163
|
}
|
|
771
1164
|
/**
|
|
772
1165
|
* Update a webhook
|
|
1166
|
+
* Validates URL if provided to prevent SSRF attacks
|
|
773
1167
|
*/
|
|
774
1168
|
async update(id, data) {
|
|
775
|
-
|
|
1169
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1170
|
+
if (data.url) {
|
|
1171
|
+
this.validateWebhookUrl(data.url);
|
|
1172
|
+
}
|
|
1173
|
+
return this.http.put(`/api/groups/webhooks/${validId}`, data);
|
|
776
1174
|
}
|
|
777
1175
|
/**
|
|
778
1176
|
* Delete a webhook
|
|
779
1177
|
*/
|
|
780
1178
|
async delete(id) {
|
|
781
|
-
|
|
1179
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1180
|
+
await this.http.delete(`/api/groups/webhooks/${validId}`);
|
|
782
1181
|
}
|
|
783
1182
|
/**
|
|
784
1183
|
* Test a webhook
|
|
785
1184
|
*/
|
|
786
1185
|
async test(id) {
|
|
787
|
-
|
|
1186
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1187
|
+
return this.http.post(`/api/groups/webhooks/${validId}/test`, {});
|
|
788
1188
|
}
|
|
789
1189
|
/**
|
|
790
1190
|
* Rerun failed webhooks
|
|
@@ -876,6 +1276,7 @@ var AdminSystemModule = class {
|
|
|
876
1276
|
}
|
|
877
1277
|
/**
|
|
878
1278
|
* Update email settings
|
|
1279
|
+
* Note: Password field is write-only and will not be returned in response
|
|
879
1280
|
*/
|
|
880
1281
|
async updateEmailSettings(data) {
|
|
881
1282
|
return this.http.put("/api/admin/email", data);
|
|
@@ -1151,7 +1552,234 @@ var ThemeModule = class {
|
|
|
1151
1552
|
}
|
|
1152
1553
|
};
|
|
1153
1554
|
|
|
1555
|
+
// src/platform/emailTemplates.ts
|
|
1556
|
+
var EmailTemplatesClient = class {
|
|
1557
|
+
constructor(http) {
|
|
1558
|
+
this.http = http;
|
|
1559
|
+
}
|
|
1560
|
+
http;
|
|
1561
|
+
/**
|
|
1562
|
+
* List all email templates for the workspace
|
|
1563
|
+
* @param groupId - Workspace ID
|
|
1564
|
+
*/
|
|
1565
|
+
async list(groupId) {
|
|
1566
|
+
return this.http.get(`/api/platform/workspaces/${groupId}/email-templates`);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Get a specific email template
|
|
1570
|
+
* @param groupId - Workspace ID
|
|
1571
|
+
* @param templateId - Template ID
|
|
1572
|
+
*/
|
|
1573
|
+
async get(groupId, templateId) {
|
|
1574
|
+
return this.http.get(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`);
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Create a new email template
|
|
1578
|
+
* @param groupId - Workspace ID
|
|
1579
|
+
* @param data - Template data
|
|
1580
|
+
*/
|
|
1581
|
+
async create(groupId, data) {
|
|
1582
|
+
return this.http.post(`/api/platform/workspaces/${groupId}/email-templates`, data);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Update an email template
|
|
1586
|
+
* @param groupId - Workspace ID
|
|
1587
|
+
* @param templateId - Template ID
|
|
1588
|
+
* @param data - Updated template data
|
|
1589
|
+
*/
|
|
1590
|
+
async update(groupId, templateId, data) {
|
|
1591
|
+
return this.http.patch(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`, data);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Delete an email template
|
|
1595
|
+
* @param groupId - Workspace ID
|
|
1596
|
+
* @param templateId - Template ID
|
|
1597
|
+
*/
|
|
1598
|
+
async delete(groupId, templateId) {
|
|
1599
|
+
await this.http.delete(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Send a test email using a template
|
|
1603
|
+
* @param groupId - Workspace ID
|
|
1604
|
+
* @param templateId - Template ID
|
|
1605
|
+
* @param recipientEmail - Test recipient email address
|
|
1606
|
+
*/
|
|
1607
|
+
async sendTest(groupId, templateId, recipientEmail) {
|
|
1608
|
+
return this.http.post(`/api/platform/workspaces/${groupId}/email-templates/${templateId}/test`, {
|
|
1609
|
+
recipient_email: recipientEmail
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
// src/platform/scheduledTasks.ts
|
|
1615
|
+
var ScheduledTasksModule = class {
|
|
1616
|
+
constructor(http) {
|
|
1617
|
+
this.http = http;
|
|
1618
|
+
}
|
|
1619
|
+
http;
|
|
1620
|
+
/**
|
|
1621
|
+
* List all scheduled tasks for the current workspace
|
|
1622
|
+
*/
|
|
1623
|
+
async list() {
|
|
1624
|
+
return this.http.get("/api/platform/scheduled-tasks");
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Get a scheduled task by ID or slug
|
|
1628
|
+
*/
|
|
1629
|
+
async get(idOrSlug) {
|
|
1630
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1631
|
+
return this.http.get(`/api/platform/scheduled-tasks/${validIdOrSlug}`);
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Create a new scheduled task
|
|
1635
|
+
*/
|
|
1636
|
+
async create(data) {
|
|
1637
|
+
return this.http.post("/api/platform/scheduled-tasks", data);
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Update a scheduled task
|
|
1641
|
+
*/
|
|
1642
|
+
async update(idOrSlug, data) {
|
|
1643
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1644
|
+
return this.http.patch(`/api/platform/scheduled-tasks/${validIdOrSlug}`, data);
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Delete a scheduled task
|
|
1648
|
+
*/
|
|
1649
|
+
async delete(idOrSlug) {
|
|
1650
|
+
await this.http.delete(`/api/platform/scheduled-tasks/${idOrSlug}`);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Manually trigger task execution (bypass schedule)
|
|
1654
|
+
*/
|
|
1655
|
+
async execute(idOrSlug) {
|
|
1656
|
+
await this.http.post(`/api/platform/scheduled-tasks/${idOrSlug}/execute`, {});
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Get task execution history
|
|
1660
|
+
*/
|
|
1661
|
+
async history(idOrSlug, options) {
|
|
1662
|
+
const params = new URLSearchParams();
|
|
1663
|
+
if (options?.limit) {
|
|
1664
|
+
params.set("limit", options.limit.toString());
|
|
1665
|
+
}
|
|
1666
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1667
|
+
return this.http.get(
|
|
1668
|
+
`/api/platform/scheduled-tasks/${idOrSlug}/history${query}`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Get available task types
|
|
1673
|
+
*/
|
|
1674
|
+
async taskTypes(options) {
|
|
1675
|
+
const params = new URLSearchParams();
|
|
1676
|
+
if (options?.detailed) {
|
|
1677
|
+
params.set("detailed", "true");
|
|
1678
|
+
}
|
|
1679
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1680
|
+
return this.http.get(`/api/platform/scheduled-tasks/task-types${query}`);
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
// src/platform/forms.ts
|
|
1685
|
+
var FormsModule = class {
|
|
1686
|
+
constructor(http) {
|
|
1687
|
+
this.http = http;
|
|
1688
|
+
}
|
|
1689
|
+
http;
|
|
1690
|
+
/**
|
|
1691
|
+
* Validate form submission data
|
|
1692
|
+
* - Prevents XSS by checking for script tags
|
|
1693
|
+
* - Validates data is a proper object
|
|
1694
|
+
* - Checks for reasonable size limits
|
|
1695
|
+
*/
|
|
1696
|
+
validateFormData(data) {
|
|
1697
|
+
if (!data || typeof data !== "object") {
|
|
1698
|
+
throw new MarvinValidationError("Form data must be a valid object");
|
|
1699
|
+
}
|
|
1700
|
+
const jsonString = JSON.stringify(data);
|
|
1701
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
1702
|
+
if (sizeInBytes > 1024 * 1024) {
|
|
1703
|
+
throw new MarvinValidationError("Form data exceeds maximum size of 1MB");
|
|
1704
|
+
}
|
|
1705
|
+
const checkForScripts = (value) => {
|
|
1706
|
+
if (typeof value === "string") {
|
|
1707
|
+
if (/<script[\s\S]*?>[\s\S]*?<\/script>/gi.test(value)) {
|
|
1708
|
+
throw new MarvinValidationError("Form data contains potentially malicious content");
|
|
1709
|
+
}
|
|
1710
|
+
} else if (Array.isArray(value)) {
|
|
1711
|
+
value.forEach(checkForScripts);
|
|
1712
|
+
} else if (value && typeof value === "object") {
|
|
1713
|
+
Object.values(value).forEach(checkForScripts);
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
Object.values(data).forEach(checkForScripts);
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* List all forms
|
|
1720
|
+
*/
|
|
1721
|
+
async list() {
|
|
1722
|
+
return this.http.get("/api/platform/forms");
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Get a single form by ID
|
|
1726
|
+
*/
|
|
1727
|
+
async get(id) {
|
|
1728
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1729
|
+
return this.http.get(`/api/platform/forms/${validId}`);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Create a new form
|
|
1733
|
+
*/
|
|
1734
|
+
async create(data) {
|
|
1735
|
+
return this.http.post("/api/platform/forms", data);
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Update a form
|
|
1739
|
+
*/
|
|
1740
|
+
async update(id, data) {
|
|
1741
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1742
|
+
return this.http.patch(`/api/platform/forms/${validId}`, data);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Delete a form
|
|
1746
|
+
*/
|
|
1747
|
+
async delete(id) {
|
|
1748
|
+
return this.http.delete(`/api/platform/forms/${id}`);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Get submissions for a form
|
|
1752
|
+
*/
|
|
1753
|
+
async getSubmissions(formId) {
|
|
1754
|
+
return this.http.get(`/api/platform/forms/${formId}/submissions`);
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Get a published form by slug (Publishing API - public access)
|
|
1758
|
+
*/
|
|
1759
|
+
async getPublishedForm(slug) {
|
|
1760
|
+
return this.http.get(`/api/publish/forms/${slug}`);
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Submit data to a published form (Publishing API - public access)
|
|
1764
|
+
* Validates data before submission to prevent XSS and malicious content
|
|
1765
|
+
*/
|
|
1766
|
+
async submitForm(slug, data) {
|
|
1767
|
+
const validSlug = this.http.validatePathParam(slug, "form slug");
|
|
1768
|
+
this.validateFormData(data);
|
|
1769
|
+
return this.http.post(`/api/publish/forms/${validSlug}/submit`, {
|
|
1770
|
+
dataJson: data
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1154
1775
|
// src/platform/client.ts
|
|
1776
|
+
function createPlatformConfigFromEnv(overrides) {
|
|
1777
|
+
return {
|
|
1778
|
+
apiUrl: overrides?.apiUrl || process.env.MARVIN_API_URL || "http://localhost:8080",
|
|
1779
|
+
userToken: overrides?.userToken || process.env.MARVIN_USER_TOKEN,
|
|
1780
|
+
credentials: overrides?.credentials || "include"
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1155
1783
|
var PlatformClient = class extends HttpClient {
|
|
1156
1784
|
entries;
|
|
1157
1785
|
collections;
|
|
@@ -1169,13 +1797,16 @@ var PlatformClient = class extends HttpClient {
|
|
|
1169
1797
|
eventLog;
|
|
1170
1798
|
session;
|
|
1171
1799
|
theme;
|
|
1800
|
+
emailTemplates;
|
|
1801
|
+
scheduledTasks;
|
|
1802
|
+
forms;
|
|
1172
1803
|
// Admin modules
|
|
1173
1804
|
adminUsers;
|
|
1174
1805
|
adminSystem;
|
|
1175
1806
|
adminMaintenance;
|
|
1176
1807
|
constructor(config) {
|
|
1177
1808
|
const httpConfig = {
|
|
1178
|
-
baseUrl: config.apiUrl,
|
|
1809
|
+
baseUrl: config.apiUrl || "http://localhost:8080",
|
|
1179
1810
|
auth: new SessionAuth(config.userToken),
|
|
1180
1811
|
credentials: config.credentials || "include"
|
|
1181
1812
|
};
|
|
@@ -1196,13 +1827,17 @@ var PlatformClient = class extends HttpClient {
|
|
|
1196
1827
|
this.eventLog = new EventLogModule(this);
|
|
1197
1828
|
this.session = new AuthModule(this);
|
|
1198
1829
|
this.theme = new ThemeModule(this);
|
|
1830
|
+
this.emailTemplates = new EmailTemplatesClient(this);
|
|
1831
|
+
this.scheduledTasks = new ScheduledTasksModule(this);
|
|
1832
|
+
this.forms = new FormsModule(this);
|
|
1199
1833
|
this.adminUsers = new AdminUsersModule(this);
|
|
1200
1834
|
this.adminSystem = new AdminSystemModule(this);
|
|
1201
1835
|
this.adminMaintenance = new AdminMaintenanceModule(this);
|
|
1202
1836
|
}
|
|
1203
1837
|
};
|
|
1204
1838
|
function createPlatformClient(config) {
|
|
1205
|
-
|
|
1839
|
+
const finalConfig = createPlatformConfigFromEnv(config);
|
|
1840
|
+
return new PlatformClient(finalConfig);
|
|
1206
1841
|
}
|
|
1207
1842
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1208
1843
|
0 && (module.exports = {
|
|
@@ -1213,18 +1848,22 @@ function createPlatformClient(config) {
|
|
|
1213
1848
|
AssetsModule,
|
|
1214
1849
|
AuthModule,
|
|
1215
1850
|
CollectionsModule,
|
|
1851
|
+
EmailTemplatesClient,
|
|
1216
1852
|
EntriesModule,
|
|
1217
1853
|
EntryTypesModule,
|
|
1218
1854
|
EventLogModule,
|
|
1219
1855
|
EventsModule,
|
|
1856
|
+
FormsModule,
|
|
1220
1857
|
InvitesModule,
|
|
1221
1858
|
NotificationsModule,
|
|
1222
1859
|
PlatformClient,
|
|
1223
1860
|
ResourcesModule,
|
|
1861
|
+
ScheduledTasksModule,
|
|
1224
1862
|
ThemeModule,
|
|
1225
1863
|
UserModule,
|
|
1226
1864
|
WebhooksModule,
|
|
1227
1865
|
WorkspaceMembersModule,
|
|
1228
1866
|
WorkspacesModule,
|
|
1229
|
-
createPlatformClient
|
|
1867
|
+
createPlatformClient,
|
|
1868
|
+
createPlatformConfigFromEnv
|
|
1230
1869
|
});
|