@inneropen/marvin-sdk 2.0.0-next.2 → 2.0.0-next.21
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 +179 -14
- 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 +697 -50
- package/dist/platform.d.cts +968 -94
- package/dist/platform.d.ts +968 -94
- package/dist/platform.js +465 -40
- 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
|
|
125
207
|
*/
|
|
126
|
-
|
|
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) {
|
|
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
|
*/
|
|
@@ -643,6 +962,12 @@ var WorkspacesModule = class {
|
|
|
643
962
|
};
|
|
644
963
|
|
|
645
964
|
// src/platform/invites.ts
|
|
965
|
+
function getEnv(key) {
|
|
966
|
+
if (typeof process !== "undefined" && process.env) {
|
|
967
|
+
return process.env[key];
|
|
968
|
+
}
|
|
969
|
+
return void 0;
|
|
970
|
+
}
|
|
646
971
|
var InvitesModule = class {
|
|
647
972
|
constructor(http) {
|
|
648
973
|
this.http = http;
|
|
@@ -675,27 +1000,47 @@ var InvitesModule = class {
|
|
|
675
1000
|
}
|
|
676
1001
|
/**
|
|
677
1002
|
* Generate invitation URL from a token
|
|
678
|
-
*
|
|
1003
|
+
*
|
|
1004
|
+
* @param token - Invitation token
|
|
1005
|
+
* @param baseUrl - Base URL for the frontend (required in browser environments)
|
|
1006
|
+
* @returns Full invitation URL
|
|
1007
|
+
*
|
|
1008
|
+
* @example
|
|
1009
|
+
* ```typescript
|
|
1010
|
+
* // Node.js (can use env vars)
|
|
1011
|
+
* const url = invites.getInvitationUrl(token);
|
|
1012
|
+
*
|
|
1013
|
+
* // Browser (must provide baseUrl)
|
|
1014
|
+
* const url = invites.getInvitationUrl(token, 'https://app.example.com');
|
|
1015
|
+
* ```
|
|
679
1016
|
*/
|
|
680
1017
|
getInvitationUrl(token, baseUrl) {
|
|
681
1018
|
let base = baseUrl;
|
|
682
1019
|
if (!base) {
|
|
683
|
-
base =
|
|
684
|
-
if (!base
|
|
685
|
-
const apiUrl =
|
|
686
|
-
if (apiUrl
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1020
|
+
base = getEnv("MARVIN_FRONTEND_URL");
|
|
1021
|
+
if (!base) {
|
|
1022
|
+
const apiUrl = getEnv("MARVIN_API_URL");
|
|
1023
|
+
if (apiUrl) {
|
|
1024
|
+
if (apiUrl.includes(":8080")) {
|
|
1025
|
+
base = apiUrl.replace(":8080", ":4321");
|
|
1026
|
+
} else if (apiUrl.includes(":3000")) {
|
|
1027
|
+
base = apiUrl.replace(":3000", ":4321");
|
|
1028
|
+
} else {
|
|
1029
|
+
base = apiUrl;
|
|
1030
|
+
}
|
|
692
1031
|
}
|
|
693
1032
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1033
|
+
}
|
|
1034
|
+
if (!base) {
|
|
1035
|
+
if (typeof window !== "undefined" && window.location) {
|
|
1036
|
+
base = window.location.origin;
|
|
1037
|
+
} else {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
'baseUrl is required when not in Node.js environment. Pass the frontend URL as second argument: getInvitationUrl(token, "https://app.example.com")'
|
|
1040
|
+
);
|
|
696
1041
|
}
|
|
697
1042
|
}
|
|
698
|
-
return `${base}/register?token=${token}`;
|
|
1043
|
+
return `${base}/register?token=${encodeURIComponent(token)}`;
|
|
699
1044
|
}
|
|
700
1045
|
};
|
|
701
1046
|
|
|
@@ -749,6 +1094,57 @@ var WebhooksModule = class {
|
|
|
749
1094
|
this.http = http;
|
|
750
1095
|
}
|
|
751
1096
|
http;
|
|
1097
|
+
/**
|
|
1098
|
+
* Validate webhook URL to prevent SSRF attacks
|
|
1099
|
+
*/
|
|
1100
|
+
validateWebhookUrl(url) {
|
|
1101
|
+
if (!url || typeof url !== "string") {
|
|
1102
|
+
throw new MarvinValidationError("Webhook URL is required and must be a string");
|
|
1103
|
+
}
|
|
1104
|
+
let parsed;
|
|
1105
|
+
try {
|
|
1106
|
+
parsed = new URL(url);
|
|
1107
|
+
} catch {
|
|
1108
|
+
throw new MarvinValidationError("Invalid webhook URL format. Expected: https://example.com/webhook");
|
|
1109
|
+
}
|
|
1110
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
1111
|
+
throw new MarvinValidationError(
|
|
1112
|
+
`Webhook URL must use HTTP or HTTPS protocol. Got: ${parsed.protocol}`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1116
|
+
const blockedHosts = [
|
|
1117
|
+
"localhost",
|
|
1118
|
+
"127.0.0.1",
|
|
1119
|
+
"0.0.0.0",
|
|
1120
|
+
"::1",
|
|
1121
|
+
"169.254.169.254",
|
|
1122
|
+
// AWS metadata service
|
|
1123
|
+
"[::]"
|
|
1124
|
+
];
|
|
1125
|
+
if (blockedHosts.some((blocked) => hostname === blocked || hostname.includes(blocked))) {
|
|
1126
|
+
throw new MarvinValidationError(
|
|
1127
|
+
"Webhook URL cannot target localhost or internal network addresses"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
1131
|
+
const match = hostname.match(ipv4Regex);
|
|
1132
|
+
if (match) {
|
|
1133
|
+
const octets = match.slice(1).map(Number);
|
|
1134
|
+
if (octets[0] === 10) {
|
|
1135
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (10.x.x.x)");
|
|
1136
|
+
}
|
|
1137
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
1138
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (172.16-31.x.x)");
|
|
1139
|
+
}
|
|
1140
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
1141
|
+
throw new MarvinValidationError("Webhook URL cannot target private IP range (192.168.x.x)");
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (url.length > 2048) {
|
|
1145
|
+
throw new MarvinValidationError("Webhook URL exceeds maximum length of 2048 characters");
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
752
1148
|
/**
|
|
753
1149
|
* List all webhooks for the current workspace
|
|
754
1150
|
*/
|
|
@@ -760,31 +1156,43 @@ var WebhooksModule = class {
|
|
|
760
1156
|
* Get a webhook by ID
|
|
761
1157
|
*/
|
|
762
1158
|
async get(id) {
|
|
763
|
-
|
|
1159
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1160
|
+
return this.http.get(`/api/groups/webhooks/${validId}`);
|
|
764
1161
|
}
|
|
765
1162
|
/**
|
|
766
1163
|
* Create a new webhook
|
|
1164
|
+
* Validates URL to prevent SSRF attacks
|
|
767
1165
|
*/
|
|
768
1166
|
async create(data) {
|
|
1167
|
+
if (data.url) {
|
|
1168
|
+
this.validateWebhookUrl(data.url);
|
|
1169
|
+
}
|
|
769
1170
|
return this.http.post("/api/groups/webhooks", data);
|
|
770
1171
|
}
|
|
771
1172
|
/**
|
|
772
1173
|
* Update a webhook
|
|
1174
|
+
* Validates URL if provided to prevent SSRF attacks
|
|
773
1175
|
*/
|
|
774
1176
|
async update(id, data) {
|
|
775
|
-
|
|
1177
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1178
|
+
if (data.url) {
|
|
1179
|
+
this.validateWebhookUrl(data.url);
|
|
1180
|
+
}
|
|
1181
|
+
return this.http.put(`/api/groups/webhooks/${validId}`, data);
|
|
776
1182
|
}
|
|
777
1183
|
/**
|
|
778
1184
|
* Delete a webhook
|
|
779
1185
|
*/
|
|
780
1186
|
async delete(id) {
|
|
781
|
-
|
|
1187
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1188
|
+
await this.http.delete(`/api/groups/webhooks/${validId}`);
|
|
782
1189
|
}
|
|
783
1190
|
/**
|
|
784
1191
|
* Test a webhook
|
|
785
1192
|
*/
|
|
786
1193
|
async test(id) {
|
|
787
|
-
|
|
1194
|
+
const validId = this.http.validatePathParam(id, "webhook ID");
|
|
1195
|
+
return this.http.post(`/api/groups/webhooks/${validId}/test`, {});
|
|
788
1196
|
}
|
|
789
1197
|
/**
|
|
790
1198
|
* Rerun failed webhooks
|
|
@@ -876,6 +1284,7 @@ var AdminSystemModule = class {
|
|
|
876
1284
|
}
|
|
877
1285
|
/**
|
|
878
1286
|
* Update email settings
|
|
1287
|
+
* Note: Password field is write-only and will not be returned in response
|
|
879
1288
|
*/
|
|
880
1289
|
async updateEmailSettings(data) {
|
|
881
1290
|
return this.http.put("/api/admin/email", data);
|
|
@@ -1151,7 +1560,234 @@ var ThemeModule = class {
|
|
|
1151
1560
|
}
|
|
1152
1561
|
};
|
|
1153
1562
|
|
|
1563
|
+
// src/platform/emailTemplates.ts
|
|
1564
|
+
var EmailTemplatesClient = class {
|
|
1565
|
+
constructor(http) {
|
|
1566
|
+
this.http = http;
|
|
1567
|
+
}
|
|
1568
|
+
http;
|
|
1569
|
+
/**
|
|
1570
|
+
* List all email templates for the workspace
|
|
1571
|
+
* @param groupId - Workspace ID
|
|
1572
|
+
*/
|
|
1573
|
+
async list(groupId) {
|
|
1574
|
+
return this.http.get(`/api/platform/workspaces/${groupId}/email-templates`);
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Get a specific email template
|
|
1578
|
+
* @param groupId - Workspace ID
|
|
1579
|
+
* @param templateId - Template ID
|
|
1580
|
+
*/
|
|
1581
|
+
async get(groupId, templateId) {
|
|
1582
|
+
return this.http.get(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Create a new email template
|
|
1586
|
+
* @param groupId - Workspace ID
|
|
1587
|
+
* @param data - Template data
|
|
1588
|
+
*/
|
|
1589
|
+
async create(groupId, data) {
|
|
1590
|
+
return this.http.post(`/api/platform/workspaces/${groupId}/email-templates`, data);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Update an email template
|
|
1594
|
+
* @param groupId - Workspace ID
|
|
1595
|
+
* @param templateId - Template ID
|
|
1596
|
+
* @param data - Updated template data
|
|
1597
|
+
*/
|
|
1598
|
+
async update(groupId, templateId, data) {
|
|
1599
|
+
return this.http.patch(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`, data);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Delete an email template
|
|
1603
|
+
* @param groupId - Workspace ID
|
|
1604
|
+
* @param templateId - Template ID
|
|
1605
|
+
*/
|
|
1606
|
+
async delete(groupId, templateId) {
|
|
1607
|
+
await this.http.delete(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Send a test email using a template
|
|
1611
|
+
* @param groupId - Workspace ID
|
|
1612
|
+
* @param templateId - Template ID
|
|
1613
|
+
* @param recipientEmail - Test recipient email address
|
|
1614
|
+
*/
|
|
1615
|
+
async sendTest(groupId, templateId, recipientEmail) {
|
|
1616
|
+
return this.http.post(`/api/platform/workspaces/${groupId}/email-templates/${templateId}/test`, {
|
|
1617
|
+
recipient_email: recipientEmail
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
// src/platform/scheduledTasks.ts
|
|
1623
|
+
var ScheduledTasksModule = class {
|
|
1624
|
+
constructor(http) {
|
|
1625
|
+
this.http = http;
|
|
1626
|
+
}
|
|
1627
|
+
http;
|
|
1628
|
+
/**
|
|
1629
|
+
* List all scheduled tasks for the current workspace
|
|
1630
|
+
*/
|
|
1631
|
+
async list() {
|
|
1632
|
+
return this.http.get("/api/platform/scheduled-tasks");
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Get a scheduled task by ID or slug
|
|
1636
|
+
*/
|
|
1637
|
+
async get(idOrSlug) {
|
|
1638
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1639
|
+
return this.http.get(`/api/platform/scheduled-tasks/${validIdOrSlug}`);
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Create a new scheduled task
|
|
1643
|
+
*/
|
|
1644
|
+
async create(data) {
|
|
1645
|
+
return this.http.post("/api/platform/scheduled-tasks", data);
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Update a scheduled task
|
|
1649
|
+
*/
|
|
1650
|
+
async update(idOrSlug, data) {
|
|
1651
|
+
const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
|
|
1652
|
+
return this.http.patch(`/api/platform/scheduled-tasks/${validIdOrSlug}`, data);
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Delete a scheduled task
|
|
1656
|
+
*/
|
|
1657
|
+
async delete(idOrSlug) {
|
|
1658
|
+
await this.http.delete(`/api/platform/scheduled-tasks/${idOrSlug}`);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Manually trigger task execution (bypass schedule)
|
|
1662
|
+
*/
|
|
1663
|
+
async execute(idOrSlug) {
|
|
1664
|
+
await this.http.post(`/api/platform/scheduled-tasks/${idOrSlug}/execute`, {});
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Get task execution history
|
|
1668
|
+
*/
|
|
1669
|
+
async history(idOrSlug, options) {
|
|
1670
|
+
const params = new URLSearchParams();
|
|
1671
|
+
if (options?.limit) {
|
|
1672
|
+
params.set("limit", options.limit.toString());
|
|
1673
|
+
}
|
|
1674
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1675
|
+
return this.http.get(
|
|
1676
|
+
`/api/platform/scheduled-tasks/${idOrSlug}/history${query}`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Get available task types
|
|
1681
|
+
*/
|
|
1682
|
+
async taskTypes(options) {
|
|
1683
|
+
const params = new URLSearchParams();
|
|
1684
|
+
if (options?.detailed) {
|
|
1685
|
+
params.set("detailed", "true");
|
|
1686
|
+
}
|
|
1687
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1688
|
+
return this.http.get(`/api/platform/scheduled-tasks/task-types${query}`);
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
// src/platform/forms.ts
|
|
1693
|
+
var FormsModule = class {
|
|
1694
|
+
constructor(http) {
|
|
1695
|
+
this.http = http;
|
|
1696
|
+
}
|
|
1697
|
+
http;
|
|
1698
|
+
/**
|
|
1699
|
+
* Validate form submission data
|
|
1700
|
+
* - Prevents XSS by checking for script tags
|
|
1701
|
+
* - Validates data is a proper object
|
|
1702
|
+
* - Checks for reasonable size limits
|
|
1703
|
+
*/
|
|
1704
|
+
validateFormData(data) {
|
|
1705
|
+
if (!data || typeof data !== "object") {
|
|
1706
|
+
throw new MarvinValidationError("Form data must be a valid object");
|
|
1707
|
+
}
|
|
1708
|
+
const jsonString = JSON.stringify(data);
|
|
1709
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
1710
|
+
if (sizeInBytes > 1024 * 1024) {
|
|
1711
|
+
throw new MarvinValidationError("Form data exceeds maximum size of 1MB");
|
|
1712
|
+
}
|
|
1713
|
+
const checkForScripts = (value) => {
|
|
1714
|
+
if (typeof value === "string") {
|
|
1715
|
+
if (/<script[\s\S]*?>[\s\S]*?<\/script>/gi.test(value)) {
|
|
1716
|
+
throw new MarvinValidationError("Form data contains potentially malicious content");
|
|
1717
|
+
}
|
|
1718
|
+
} else if (Array.isArray(value)) {
|
|
1719
|
+
value.forEach(checkForScripts);
|
|
1720
|
+
} else if (value && typeof value === "object") {
|
|
1721
|
+
Object.values(value).forEach(checkForScripts);
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
Object.values(data).forEach(checkForScripts);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* List all forms
|
|
1728
|
+
*/
|
|
1729
|
+
async list() {
|
|
1730
|
+
return this.http.get("/api/platform/forms");
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Get a single form by ID
|
|
1734
|
+
*/
|
|
1735
|
+
async get(id) {
|
|
1736
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1737
|
+
return this.http.get(`/api/platform/forms/${validId}`);
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Create a new form
|
|
1741
|
+
*/
|
|
1742
|
+
async create(data) {
|
|
1743
|
+
return this.http.post("/api/platform/forms", data);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Update a form
|
|
1747
|
+
*/
|
|
1748
|
+
async update(id, data) {
|
|
1749
|
+
const validId = this.http.validatePathParam(id, "id");
|
|
1750
|
+
return this.http.patch(`/api/platform/forms/${validId}`, data);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Delete a form
|
|
1754
|
+
*/
|
|
1755
|
+
async delete(id) {
|
|
1756
|
+
return this.http.delete(`/api/platform/forms/${id}`);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Get submissions for a form
|
|
1760
|
+
*/
|
|
1761
|
+
async getSubmissions(formId) {
|
|
1762
|
+
return this.http.get(`/api/platform/forms/${formId}/submissions`);
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Get a published form by slug (Publishing API - public access)
|
|
1766
|
+
*/
|
|
1767
|
+
async getPublishedForm(slug) {
|
|
1768
|
+
return this.http.get(`/api/publish/forms/${slug}`);
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Submit data to a published form (Publishing API - public access)
|
|
1772
|
+
* Validates data before submission to prevent XSS and malicious content
|
|
1773
|
+
*/
|
|
1774
|
+
async submitForm(slug, data) {
|
|
1775
|
+
const validSlug = this.http.validatePathParam(slug, "form slug");
|
|
1776
|
+
this.validateFormData(data);
|
|
1777
|
+
return this.http.post(`/api/publish/forms/${validSlug}/submit`, {
|
|
1778
|
+
dataJson: data
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1154
1783
|
// src/platform/client.ts
|
|
1784
|
+
function createPlatformConfigFromEnv(overrides) {
|
|
1785
|
+
return {
|
|
1786
|
+
apiUrl: overrides?.apiUrl || process.env.MARVIN_API_URL || "http://localhost:8080",
|
|
1787
|
+
userToken: overrides?.userToken || process.env.MARVIN_USER_TOKEN,
|
|
1788
|
+
credentials: overrides?.credentials || "include"
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1155
1791
|
var PlatformClient = class extends HttpClient {
|
|
1156
1792
|
entries;
|
|
1157
1793
|
collections;
|
|
@@ -1169,13 +1805,16 @@ var PlatformClient = class extends HttpClient {
|
|
|
1169
1805
|
eventLog;
|
|
1170
1806
|
session;
|
|
1171
1807
|
theme;
|
|
1808
|
+
emailTemplates;
|
|
1809
|
+
scheduledTasks;
|
|
1810
|
+
forms;
|
|
1172
1811
|
// Admin modules
|
|
1173
1812
|
adminUsers;
|
|
1174
1813
|
adminSystem;
|
|
1175
1814
|
adminMaintenance;
|
|
1176
1815
|
constructor(config) {
|
|
1177
1816
|
const httpConfig = {
|
|
1178
|
-
baseUrl: config.apiUrl,
|
|
1817
|
+
baseUrl: config.apiUrl || "http://localhost:8080",
|
|
1179
1818
|
auth: new SessionAuth(config.userToken),
|
|
1180
1819
|
credentials: config.credentials || "include"
|
|
1181
1820
|
};
|
|
@@ -1196,13 +1835,17 @@ var PlatformClient = class extends HttpClient {
|
|
|
1196
1835
|
this.eventLog = new EventLogModule(this);
|
|
1197
1836
|
this.session = new AuthModule(this);
|
|
1198
1837
|
this.theme = new ThemeModule(this);
|
|
1838
|
+
this.emailTemplates = new EmailTemplatesClient(this);
|
|
1839
|
+
this.scheduledTasks = new ScheduledTasksModule(this);
|
|
1840
|
+
this.forms = new FormsModule(this);
|
|
1199
1841
|
this.adminUsers = new AdminUsersModule(this);
|
|
1200
1842
|
this.adminSystem = new AdminSystemModule(this);
|
|
1201
1843
|
this.adminMaintenance = new AdminMaintenanceModule(this);
|
|
1202
1844
|
}
|
|
1203
1845
|
};
|
|
1204
1846
|
function createPlatformClient(config) {
|
|
1205
|
-
|
|
1847
|
+
const finalConfig = createPlatformConfigFromEnv(config);
|
|
1848
|
+
return new PlatformClient(finalConfig);
|
|
1206
1849
|
}
|
|
1207
1850
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1208
1851
|
0 && (module.exports = {
|
|
@@ -1213,18 +1856,22 @@ function createPlatformClient(config) {
|
|
|
1213
1856
|
AssetsModule,
|
|
1214
1857
|
AuthModule,
|
|
1215
1858
|
CollectionsModule,
|
|
1859
|
+
EmailTemplatesClient,
|
|
1216
1860
|
EntriesModule,
|
|
1217
1861
|
EntryTypesModule,
|
|
1218
1862
|
EventLogModule,
|
|
1219
1863
|
EventsModule,
|
|
1864
|
+
FormsModule,
|
|
1220
1865
|
InvitesModule,
|
|
1221
1866
|
NotificationsModule,
|
|
1222
1867
|
PlatformClient,
|
|
1223
1868
|
ResourcesModule,
|
|
1869
|
+
ScheduledTasksModule,
|
|
1224
1870
|
ThemeModule,
|
|
1225
1871
|
UserModule,
|
|
1226
1872
|
WebhooksModule,
|
|
1227
1873
|
WorkspaceMembersModule,
|
|
1228
1874
|
WorkspacesModule,
|
|
1229
|
-
createPlatformClient
|
|
1875
|
+
createPlatformClient,
|
|
1876
|
+
createPlatformConfigFromEnv
|
|
1230
1877
|
});
|