@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/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
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
- async request(method, endpoint, options) {
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
  */
@@ -675,11 +994,23 @@ var InvitesModule = class {
675
994
  }
676
995
  /**
677
996
  * 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
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
- if (!base) {
695
- base = "http://localhost:4321";
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
- return this.http.get(`/api/groups/webhooks/${id}`);
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
- return this.http.put(`/api/groups/webhooks/${id}`, data);
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
- await this.http.delete(`/api/groups/webhooks/${id}`);
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
- return this.http.post(`/api/groups/webhooks/${id}/test`, {});
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
- return new PlatformClient(config);
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
  });