@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/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
- this.timeout = config.timeout || 3e4;
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
- * Perform HTTP request
206
+ * Sanitize data for logging by redacting sensitive fields
125
207
  */
126
- async request(method, endpoint, options) {
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
- return await response.json();
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 MarvinApiError(
370
+ throw new MarvinNetworkError(
166
371
  `Request timeout after ${this.timeout}ms`,
167
- 408,
168
- endpoint
372
+ error
169
373
  );
170
374
  }
171
- throw new MarvinApiError(error.message, 0, endpoint);
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
- return this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
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
- return this.http.get(`/api/platform/entries/${id}`);
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
- return this.http.patch(`/api/platform/entries/${id}`, data);
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
- return this.http.delete(`/api/platform/entries/${id}`);
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
- return this.http.get(`/api/platform/entries/${entryId}/collections`);
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/${entryId}/collections/${collectionId}`,
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
- return this.http.delete(`/api/platform/entries/${entryId}/collections/${collectionId}`);
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
- return this.http.get(`/api/platform/collections/${id}`);
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
- return this.http.patch(`/api/platform/collections/${id}`, data);
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
- return this.http.delete(`/api/platform/collections/${id}`);
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
- return this.http.get(`/api/platform/collections/${id}/entries`);
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
- return this.http.patch(`/api/platform/collections/${id}/entries/order`, { entries });
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
- return this.http.get(`/api/platform/resources/${id}`);
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
- return this.http.patch(`/api/platform/resources/${id}`, data);
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
- return this.http.get(`/api/platform/assets/${id}`);
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
- return this.http.patch(`/api/platform/assets/${id}`, data);
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
- return this.http.delete(`/api/platform/assets/${id}`);
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
- return this.http.get(`/api/platform/api-clients/${id}`);
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
- return this.http.patch(`/api/platform/api-clients/${id}`, data);
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
- * Uses MARVIN_FRONTEND_URL for the base URL, falls back to MARVIN_API_URL with common port mappings
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 = process.env.MARVIN_FRONTEND_URL;
684
- if (!base && process.env.MARVIN_API_URL) {
685
- const apiUrl = process.env.MARVIN_API_URL;
686
- if (apiUrl.includes(":8080")) {
687
- base = apiUrl.replace(":8080", ":4321");
688
- } else if (apiUrl.includes(":3000")) {
689
- base = apiUrl.replace(":3000", ":4321");
690
- } else {
691
- base = apiUrl;
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
- if (!base) {
695
- base = "http://localhost:4321";
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
- return this.http.get(`/api/groups/webhooks/${id}`);
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
- return this.http.put(`/api/groups/webhooks/${id}`, data);
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
- await this.http.delete(`/api/groups/webhooks/${id}`);
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
- return this.http.post(`/api/groups/webhooks/${id}/test`, {});
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
- return new PlatformClient(config);
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
  });