@inneropen/marvin-sdk 2.0.1 → 3.0.1

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
@@ -43,7 +43,8 @@ __export(platform_exports, {
43
43
  WebhooksModule: () => WebhooksModule,
44
44
  WorkspaceMembersModule: () => WorkspaceMembersModule,
45
45
  WorkspacesModule: () => WorkspacesModule,
46
- createPlatformClient: () => createPlatformClient
46
+ createPlatformClient: () => createPlatformClient,
47
+ createPlatformConfigFromEnv: () => createPlatformConfigFromEnv
47
48
  });
48
49
  module.exports = __toCommonJS(platform_exports);
49
50
 
@@ -77,6 +78,49 @@ ${body}` : ""}`;
77
78
  return new _MarvinApiError(message, status, endpoint, body);
78
79
  }
79
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
+ };
80
124
 
81
125
  // src/core/http/HttpClient.ts
82
126
  var HttpClient = class {
@@ -85,12 +129,47 @@ var HttpClient = class {
85
129
  defaultHeaders;
86
130
  credentials;
87
131
  timeout;
132
+ debug;
133
+ retryConfig;
134
+ logger;
88
135
  constructor(config) {
89
136
  this.baseUrl = config.baseUrl;
90
137
  this.auth = config.auth;
91
138
  this.defaultHeaders = config.defaultHeaders || {};
92
139
  this.credentials = config.credentials || "same-origin";
93
- 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;
94
173
  }
95
174
  /**
96
175
  * Build full URL from endpoint
@@ -124,14 +203,78 @@ var HttpClient = class {
124
203
  return filtered.length > 0 ? `?${filtered.join("&")}` : "";
125
204
  }
126
205
  /**
127
- * 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
128
254
  */
129
- async request(method, endpoint, options) {
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) {
130
269
  const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
131
270
  const isFormData = options?.body instanceof FormData;
132
271
  const headers = this.buildHeaders(options?.headers, isFormData);
133
272
  const controller = new AbortController();
134
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
+ }
135
278
  try {
136
279
  const response = await fetch(url, {
137
280
  method,
@@ -141,11 +284,54 @@ var HttpClient = class {
141
284
  signal: controller.signal
142
285
  });
143
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
+ }
144
293
  if (response.status === 401 && this.auth.handleUnauthorized) {
145
294
  await this.auth.handleUnauthorized();
146
295
  }
147
296
  if (!response.ok) {
148
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
+ }
149
335
  throw MarvinApiError.fromResponse(
150
336
  response.status,
151
337
  response.statusText,
@@ -157,21 +343,39 @@ var HttpClient = class {
157
343
  if (!contentType || !contentType.includes("application/json")) {
158
344
  return void 0;
159
345
  }
160
- 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;
161
352
  } catch (error) {
162
353
  clearTimeout(timeoutId);
163
- if (error instanceof MarvinApiError) {
354
+ if (error instanceof MarvinApiError || error instanceof MarvinNotFoundError || error instanceof MarvinAuthError || error instanceof MarvinServerError) {
164
355
  throw error;
165
356
  }
166
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
+ }
167
369
  if (error.name === "AbortError") {
168
- throw new MarvinApiError(
370
+ throw new MarvinNetworkError(
169
371
  `Request timeout after ${this.timeout}ms`,
170
- 408,
171
- endpoint
372
+ error
172
373
  );
173
374
  }
174
- throw new MarvinApiError(error.message, 0, endpoint);
375
+ throw new MarvinNetworkError(
376
+ `Network error: ${error.message}`,
377
+ error
378
+ );
175
379
  }
176
380
  throw error;
177
381
  }
@@ -216,10 +420,27 @@ var SessionAuth = class {
216
420
  }
217
421
  sessionToken;
218
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
+ }
219
437
  injectAuth(headers) {
220
438
  if (this.sessionToken) {
221
439
  headers.set("Authorization", `Bearer ${this.sessionToken}`);
222
440
  }
441
+ if (this.csrfToken) {
442
+ headers.set("X-CSRF-Token", this.csrfToken);
443
+ }
223
444
  }
224
445
  async handleUnauthorized() {
225
446
  if (this.onUnauthorized) {
@@ -227,7 +448,9 @@ var SessionAuth = class {
227
448
  }
228
449
  }
229
450
  getDescription() {
230
- 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;
231
454
  }
232
455
  };
233
456
 
@@ -247,7 +470,8 @@ var EntriesModule = class {
247
470
  * Get a single entry by ID
248
471
  */
249
472
  async get(id) {
250
- 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}`);
251
475
  }
252
476
  /**
253
477
  * Create a new entry
@@ -259,26 +483,31 @@ var EntriesModule = class {
259
483
  * Update an entry
260
484
  */
261
485
  async update(id, data) {
262
- 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);
263
488
  }
264
489
  /**
265
490
  * Delete an entry
266
491
  */
267
492
  async delete(id) {
268
- 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}`);
269
495
  }
270
496
  /**
271
497
  * Get collections that this entry belongs to
272
498
  */
273
499
  async listCollections(entryId) {
274
- 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`);
275
502
  }
276
503
  /**
277
504
  * Add entry to a collection
278
505
  */
279
506
  async addToCollection(entryId, collectionId) {
507
+ const validEntryId = this.http.validatePathParam(entryId, "entry ID");
508
+ const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
280
509
  return this.http.post(
281
- `/api/platform/entries/${entryId}/collections/${collectionId}`,
510
+ `/api/platform/entries/${validEntryId}/collections/${validCollectionId}`,
282
511
  {}
283
512
  );
284
513
  }
@@ -286,7 +515,9 @@ var EntriesModule = class {
286
515
  * Remove entry from a collection
287
516
  */
288
517
  async removeFromCollection(entryId, collectionId) {
289
- 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}`);
290
521
  }
291
522
  };
292
523
 
@@ -306,7 +537,8 @@ var CollectionsModule = class {
306
537
  * Get a single collection by ID
307
538
  */
308
539
  async get(id) {
309
- 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}`);
310
542
  }
311
543
  /**
312
544
  * Create a new collection
@@ -318,25 +550,29 @@ var CollectionsModule = class {
318
550
  * Update a collection
319
551
  */
320
552
  async update(id, data) {
321
- 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);
322
555
  }
323
556
  /**
324
557
  * Delete a collection
325
558
  */
326
559
  async delete(id) {
327
- 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}`);
328
562
  }
329
563
  /**
330
564
  * Get entries in a collection
331
565
  */
332
566
  async getEntries(id) {
333
- 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`);
334
569
  }
335
570
  /**
336
571
  * Reorder entries in a collection
337
572
  */
338
573
  async reorderEntries(id, entries) {
339
- 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 });
340
576
  }
341
577
  };
342
578
 
@@ -356,7 +592,8 @@ var ResourcesModule = class {
356
592
  * Get a single resource by ID
357
593
  */
358
594
  async get(id) {
359
- 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}`);
360
597
  }
361
598
  /**
362
599
  * Create a new resource
@@ -368,7 +605,8 @@ var ResourcesModule = class {
368
605
  * Update a resource
369
606
  */
370
607
  async update(id, data) {
371
- 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);
372
610
  }
373
611
  /**
374
612
  * Delete a resource
@@ -390,6 +628,47 @@ var AssetsModule = class {
390
628
  this.http = http;
391
629
  }
392
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
+ }
393
672
  /**
394
673
  * List all assets
395
674
  */
@@ -400,16 +679,31 @@ var AssetsModule = class {
400
679
  * Get a single asset by ID
401
680
  */
402
681
  async get(id) {
403
- 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}`);
404
684
  }
405
685
  /**
406
686
  * Upload a new asset file
407
687
  *
408
688
  * @param file - The file to upload (File or Blob)
409
689
  * @param metadata - Asset metadata (slug, name, altText, description, metadata)
690
+ * @param options - Upload options (file size limits, allowed types)
410
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
+ * ```
411
704
  */
412
- async upload(file, metadata) {
705
+ async upload(file, metadata, options) {
706
+ this.validateFile(file, options);
413
707
  const formData = new FormData();
414
708
  formData.append("file", file);
415
709
  formData.append("slug", metadata.slug);
@@ -429,13 +723,24 @@ var AssetsModule = class {
429
723
  * Update an asset (editable fields only: slug, name, altText, description, metadata)
430
724
  */
431
725
  async update(id, data) {
432
- 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);
433
728
  }
434
729
  /**
435
730
  * Delete an asset (removes file from storage and database)
436
731
  */
437
732
  async delete(id) {
438
- 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`;
439
744
  }
440
745
  };
441
746
 
@@ -455,7 +760,8 @@ var APIClientsModule = class {
455
760
  * Get a single API client by ID
456
761
  */
457
762
  async get(id) {
458
- 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}`);
459
765
  }
460
766
  /**
461
767
  * Create a new API client (returns client with token)
@@ -467,7 +773,8 @@ var APIClientsModule = class {
467
773
  * Update an API client
468
774
  */
469
775
  async update(id, data) {
470
- 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);
471
778
  }
472
779
  /**
473
780
  * Delete an API client
@@ -631,6 +938,15 @@ var WorkspacesModule = class {
631
938
  const url = `/api/admin/groups/${id}${force ? "?force=true" : ""}`;
632
939
  await this.http.delete(url);
633
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
+ }
634
950
  /**
635
951
  * Get workspace preferences
636
952
  */
@@ -646,6 +962,12 @@ var WorkspacesModule = class {
646
962
  };
647
963
 
648
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
+ }
649
971
  var InvitesModule = class {
650
972
  constructor(http) {
651
973
  this.http = http;
@@ -678,27 +1000,47 @@ var InvitesModule = class {
678
1000
  }
679
1001
  /**
680
1002
  * Generate invitation URL from a token
681
- * 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
+ * ```
682
1016
  */
683
1017
  getInvitationUrl(token, baseUrl) {
684
1018
  let base = baseUrl;
685
1019
  if (!base) {
686
- base = process.env.MARVIN_FRONTEND_URL;
687
- if (!base && process.env.MARVIN_API_URL) {
688
- const apiUrl = process.env.MARVIN_API_URL;
689
- if (apiUrl.includes(":8080")) {
690
- base = apiUrl.replace(":8080", ":4321");
691
- } else if (apiUrl.includes(":3000")) {
692
- base = apiUrl.replace(":3000", ":4321");
693
- } else {
694
- 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
+ }
695
1031
  }
696
1032
  }
697
- if (!base) {
698
- 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
+ );
699
1041
  }
700
1042
  }
701
- return `${base}/register?token=${token}`;
1043
+ return `${base}/register?token=${encodeURIComponent(token)}`;
702
1044
  }
703
1045
  };
704
1046
 
@@ -752,6 +1094,57 @@ var WebhooksModule = class {
752
1094
  this.http = http;
753
1095
  }
754
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
+ }
755
1148
  /**
756
1149
  * List all webhooks for the current workspace
757
1150
  */
@@ -763,31 +1156,43 @@ var WebhooksModule = class {
763
1156
  * Get a webhook by ID
764
1157
  */
765
1158
  async get(id) {
766
- 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}`);
767
1161
  }
768
1162
  /**
769
1163
  * Create a new webhook
1164
+ * Validates URL to prevent SSRF attacks
770
1165
  */
771
1166
  async create(data) {
1167
+ if (data.url) {
1168
+ this.validateWebhookUrl(data.url);
1169
+ }
772
1170
  return this.http.post("/api/groups/webhooks", data);
773
1171
  }
774
1172
  /**
775
1173
  * Update a webhook
1174
+ * Validates URL if provided to prevent SSRF attacks
776
1175
  */
777
1176
  async update(id, data) {
778
- 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);
779
1182
  }
780
1183
  /**
781
1184
  * Delete a webhook
782
1185
  */
783
1186
  async delete(id) {
784
- 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}`);
785
1189
  }
786
1190
  /**
787
1191
  * Test a webhook
788
1192
  */
789
1193
  async test(id) {
790
- 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`, {});
791
1196
  }
792
1197
  /**
793
1198
  * Rerun failed webhooks
@@ -879,6 +1284,7 @@ var AdminSystemModule = class {
879
1284
  }
880
1285
  /**
881
1286
  * Update email settings
1287
+ * Note: Password field is write-only and will not be returned in response
882
1288
  */
883
1289
  async updateEmailSettings(data) {
884
1290
  return this.http.put("/api/admin/email", data);
@@ -1162,53 +1568,40 @@ var EmailTemplatesClient = class {
1162
1568
  http;
1163
1569
  /**
1164
1570
  * List all email templates for the workspace
1165
- * @param groupId - Workspace ID
1166
1571
  */
1167
1572
  async list(groupId) {
1168
1573
  return this.http.get(`/api/platform/workspaces/${groupId}/email-templates`);
1169
1574
  }
1170
1575
  /**
1171
1576
  * Get a specific email template
1172
- * @param groupId - Workspace ID
1173
- * @param templateId - Template ID
1174
1577
  */
1175
1578
  async get(groupId, templateId) {
1176
1579
  return this.http.get(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`);
1177
1580
  }
1178
1581
  /**
1179
1582
  * Create a new email template
1180
- * @param groupId - Workspace ID
1181
- * @param data - Template data
1182
1583
  */
1183
1584
  async create(groupId, data) {
1184
1585
  return this.http.post(`/api/platform/workspaces/${groupId}/email-templates`, data);
1185
1586
  }
1186
1587
  /**
1187
1588
  * Update an email template
1188
- * @param groupId - Workspace ID
1189
- * @param templateId - Template ID
1190
- * @param data - Updated template data
1191
1589
  */
1192
1590
  async update(groupId, templateId, data) {
1193
1591
  return this.http.patch(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`, data);
1194
1592
  }
1195
1593
  /**
1196
1594
  * Delete an email template
1197
- * @param groupId - Workspace ID
1198
- * @param templateId - Template ID
1199
1595
  */
1200
1596
  async delete(groupId, templateId) {
1201
1597
  await this.http.delete(`/api/platform/workspaces/${groupId}/email-templates/${templateId}`);
1202
1598
  }
1203
1599
  /**
1204
1600
  * Send a test email using a template
1205
- * @param groupId - Workspace ID
1206
- * @param templateId - Template ID
1207
- * @param recipientEmail - Test recipient email address
1208
1601
  */
1209
1602
  async sendTest(groupId, templateId, recipientEmail) {
1210
1603
  return this.http.post(`/api/platform/workspaces/${groupId}/email-templates/${templateId}/test`, {
1211
- recipient_email: recipientEmail
1604
+ email: recipientEmail
1212
1605
  });
1213
1606
  }
1214
1607
  };
@@ -1229,7 +1622,8 @@ var ScheduledTasksModule = class {
1229
1622
  * Get a scheduled task by ID or slug
1230
1623
  */
1231
1624
  async get(idOrSlug) {
1232
- return this.http.get(`/api/platform/scheduled-tasks/${idOrSlug}`);
1625
+ const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
1626
+ return this.http.get(`/api/platform/scheduled-tasks/${validIdOrSlug}`);
1233
1627
  }
1234
1628
  /**
1235
1629
  * Create a new scheduled task
@@ -1241,7 +1635,8 @@ var ScheduledTasksModule = class {
1241
1635
  * Update a scheduled task
1242
1636
  */
1243
1637
  async update(idOrSlug, data) {
1244
- return this.http.patch(`/api/platform/scheduled-tasks/${idOrSlug}`, data);
1638
+ const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
1639
+ return this.http.patch(`/api/platform/scheduled-tasks/${validIdOrSlug}`, data);
1245
1640
  }
1246
1641
  /**
1247
1642
  * Delete a scheduled task
@@ -1287,6 +1682,34 @@ var FormsModule = class {
1287
1682
  this.http = http;
1288
1683
  }
1289
1684
  http;
1685
+ /**
1686
+ * Validate form submission data
1687
+ * - Prevents XSS by checking for script tags
1688
+ * - Validates data is a proper object
1689
+ * - Checks for reasonable size limits
1690
+ */
1691
+ validateFormData(data) {
1692
+ if (!data || typeof data !== "object") {
1693
+ throw new MarvinValidationError("Form data must be a valid object");
1694
+ }
1695
+ const jsonString = JSON.stringify(data);
1696
+ const sizeInBytes = new Blob([jsonString]).size;
1697
+ if (sizeInBytes > 1024 * 1024) {
1698
+ throw new MarvinValidationError("Form data exceeds maximum size of 1MB");
1699
+ }
1700
+ const checkForScripts = (value) => {
1701
+ if (typeof value === "string") {
1702
+ if (/<script[\s\S]*?>[\s\S]*?<\/script>/gi.test(value)) {
1703
+ throw new MarvinValidationError("Form data contains potentially malicious content");
1704
+ }
1705
+ } else if (Array.isArray(value)) {
1706
+ value.forEach(checkForScripts);
1707
+ } else if (value && typeof value === "object") {
1708
+ Object.values(value).forEach(checkForScripts);
1709
+ }
1710
+ };
1711
+ Object.values(data).forEach(checkForScripts);
1712
+ }
1290
1713
  /**
1291
1714
  * List all forms
1292
1715
  */
@@ -1297,7 +1720,8 @@ var FormsModule = class {
1297
1720
  * Get a single form by ID
1298
1721
  */
1299
1722
  async get(id) {
1300
- return this.http.get(`/api/platform/forms/${id}`);
1723
+ const validId = this.http.validatePathParam(id, "id");
1724
+ return this.http.get(`/api/platform/forms/${validId}`);
1301
1725
  }
1302
1726
  /**
1303
1727
  * Create a new form
@@ -1309,7 +1733,8 @@ var FormsModule = class {
1309
1733
  * Update a form
1310
1734
  */
1311
1735
  async update(id, data) {
1312
- return this.http.patch(`/api/platform/forms/${id}`, data);
1736
+ const validId = this.http.validatePathParam(id, "id");
1737
+ return this.http.patch(`/api/platform/forms/${validId}`, data);
1313
1738
  }
1314
1739
  /**
1315
1740
  * Delete a form
@@ -1331,15 +1756,31 @@ var FormsModule = class {
1331
1756
  }
1332
1757
  /**
1333
1758
  * Submit data to a published form (Publishing API - public access)
1759
+ * Validates data before submission to prevent XSS and malicious content
1334
1760
  */
1335
1761
  async submitForm(slug, data) {
1336
- return this.http.post(`/api/publish/forms/${slug}/submit`, {
1762
+ const validSlug = this.http.validatePathParam(slug, "form slug");
1763
+ this.validateFormData(data);
1764
+ return this.http.post(`/api/publish/forms/${validSlug}/submit`, {
1337
1765
  dataJson: data
1338
1766
  });
1339
1767
  }
1340
1768
  };
1341
1769
 
1342
1770
  // src/platform/client.ts
1771
+ function getEnv2(key) {
1772
+ if (typeof process !== "undefined" && process.env) {
1773
+ return process.env[key];
1774
+ }
1775
+ return void 0;
1776
+ }
1777
+ function createPlatformConfigFromEnv(overrides) {
1778
+ return {
1779
+ apiUrl: overrides?.apiUrl || getEnv2("MARVIN_API_URL") || "http://localhost:8080",
1780
+ userToken: overrides?.userToken || getEnv2("MARVIN_USER_TOKEN"),
1781
+ credentials: overrides?.credentials || "include"
1782
+ };
1783
+ }
1343
1784
  var PlatformClient = class extends HttpClient {
1344
1785
  entries;
1345
1786
  collections;
@@ -1366,7 +1807,7 @@ var PlatformClient = class extends HttpClient {
1366
1807
  adminMaintenance;
1367
1808
  constructor(config) {
1368
1809
  const httpConfig = {
1369
- baseUrl: config.apiUrl,
1810
+ baseUrl: config.apiUrl || "http://localhost:8080",
1370
1811
  auth: new SessionAuth(config.userToken),
1371
1812
  credentials: config.credentials || "include"
1372
1813
  };
@@ -1396,7 +1837,8 @@ var PlatformClient = class extends HttpClient {
1396
1837
  }
1397
1838
  };
1398
1839
  function createPlatformClient(config) {
1399
- return new PlatformClient(config);
1840
+ const finalConfig = createPlatformConfigFromEnv(config);
1841
+ return new PlatformClient(finalConfig);
1400
1842
  }
1401
1843
  // Annotate the CommonJS export names for ESM import in node:
1402
1844
  0 && (module.exports = {
@@ -1423,5 +1865,6 @@ function createPlatformClient(config) {
1423
1865
  WebhooksModule,
1424
1866
  WorkspaceMembersModule,
1425
1867
  WorkspacesModule,
1426
- createPlatformClient
1868
+ createPlatformClient,
1869
+ createPlatformConfigFromEnv
1427
1870
  });