@inneropen/marvin-sdk 2.0.0-next.13 → 2.0.0-next.15

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
 
@@ -84,6 +85,15 @@ var MarvinAuthError = class _MarvinAuthError extends MarvinError {
84
85
  Object.setPrototypeOf(this, _MarvinAuthError.prototype);
85
86
  }
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
+ };
87
97
  var MarvinNotFoundError = class _MarvinNotFoundError extends MarvinError {
88
98
  constructor(message, endpoint) {
89
99
  super(message, "NOT_FOUND", 404);
@@ -120,15 +130,46 @@ var HttpClient = class {
120
130
  credentials;
121
131
  timeout;
122
132
  debug;
133
+ retryConfig;
123
134
  logger;
124
135
  constructor(config) {
125
136
  this.baseUrl = config.baseUrl;
126
137
  this.auth = config.auth;
127
138
  this.defaultHeaders = config.defaultHeaders || {};
128
139
  this.credentials = config.credentials || "same-origin";
129
- this.timeout = config.timeout || 3e4;
140
+ const MAX_TIMEOUT = 12e4;
141
+ this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
130
142
  this.debug = config.debug ?? false;
131
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;
132
173
  }
133
174
  /**
134
175
  * Build full URL from endpoint
@@ -162,9 +203,69 @@ var HttpClient = class {
162
203
  return filtered.length > 0 ? `?${filtered.join("&")}` : "";
163
204
  }
164
205
  /**
165
- * Perform HTTP request
206
+ * Sanitize data for logging by redacting sensitive fields
166
207
  */
167
- 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) {
168
269
  const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
169
270
  const isFormData = options?.body instanceof FormData;
170
271
  const headers = this.buildHeaders(options?.headers, isFormData);
@@ -200,6 +301,18 @@ var HttpClient = class {
200
301
  errorText ? `Body: ${errorText}` : ""
201
302
  );
202
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
+ }
203
316
  if (response.status === 404) {
204
317
  throw new MarvinNotFoundError(
205
318
  `Resource not found: ${endpoint}`,
@@ -232,8 +345,8 @@ var HttpClient = class {
232
345
  }
233
346
  const data = await response.json();
234
347
  if (this.debug) {
235
- const preview = typeof data === "object" && data !== null ? Array.isArray(data) ? `Array(${data.length})` : `Object with keys: ${Object.keys(data).join(", ")}` : String(data);
236
- this.logger.log(`[Marvin] Response: ${preview}`);
348
+ const sanitized = this.sanitizeForLogging(data);
349
+ this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
237
350
  }
238
351
  return data;
239
352
  } catch (error) {
@@ -242,6 +355,17 @@ var HttpClient = class {
242
355
  throw error;
243
356
  }
244
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
+ }
245
369
  if (error.name === "AbortError") {
246
370
  throw new MarvinNetworkError(
247
371
  `Request timeout after ${this.timeout}ms`,
@@ -296,10 +420,27 @@ var SessionAuth = class {
296
420
  }
297
421
  sessionToken;
298
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
+ }
299
437
  injectAuth(headers) {
300
438
  if (this.sessionToken) {
301
439
  headers.set("Authorization", `Bearer ${this.sessionToken}`);
302
440
  }
441
+ if (this.csrfToken) {
442
+ headers.set("X-CSRF-Token", this.csrfToken);
443
+ }
303
444
  }
304
445
  async handleUnauthorized() {
305
446
  if (this.onUnauthorized) {
@@ -307,7 +448,9 @@ var SessionAuth = class {
307
448
  }
308
449
  }
309
450
  getDescription() {
310
- 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;
311
454
  }
312
455
  };
313
456
 
@@ -327,7 +470,8 @@ var EntriesModule = class {
327
470
  * Get a single entry by ID
328
471
  */
329
472
  async get(id) {
330
- 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}`);
331
475
  }
332
476
  /**
333
477
  * Create a new entry
@@ -339,26 +483,31 @@ var EntriesModule = class {
339
483
  * Update an entry
340
484
  */
341
485
  async update(id, data) {
342
- 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);
343
488
  }
344
489
  /**
345
490
  * Delete an entry
346
491
  */
347
492
  async delete(id) {
348
- 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}`);
349
495
  }
350
496
  /**
351
497
  * Get collections that this entry belongs to
352
498
  */
353
499
  async listCollections(entryId) {
354
- 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`);
355
502
  }
356
503
  /**
357
504
  * Add entry to a collection
358
505
  */
359
506
  async addToCollection(entryId, collectionId) {
507
+ const validEntryId = this.http.validatePathParam(entryId, "entry ID");
508
+ const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
360
509
  return this.http.post(
361
- `/api/platform/entries/${entryId}/collections/${collectionId}`,
510
+ `/api/platform/entries/${validEntryId}/collections/${validCollectionId}`,
362
511
  {}
363
512
  );
364
513
  }
@@ -366,7 +515,9 @@ var EntriesModule = class {
366
515
  * Remove entry from a collection
367
516
  */
368
517
  async removeFromCollection(entryId, collectionId) {
369
- 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}`);
370
521
  }
371
522
  };
372
523
 
@@ -386,7 +537,8 @@ var CollectionsModule = class {
386
537
  * Get a single collection by ID
387
538
  */
388
539
  async get(id) {
389
- 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}`);
390
542
  }
391
543
  /**
392
544
  * Create a new collection
@@ -398,25 +550,29 @@ var CollectionsModule = class {
398
550
  * Update a collection
399
551
  */
400
552
  async update(id, data) {
401
- 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);
402
555
  }
403
556
  /**
404
557
  * Delete a collection
405
558
  */
406
559
  async delete(id) {
407
- 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}`);
408
562
  }
409
563
  /**
410
564
  * Get entries in a collection
411
565
  */
412
566
  async getEntries(id) {
413
- 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`);
414
569
  }
415
570
  /**
416
571
  * Reorder entries in a collection
417
572
  */
418
573
  async reorderEntries(id, entries) {
419
- 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 });
420
576
  }
421
577
  };
422
578
 
@@ -436,7 +592,8 @@ var ResourcesModule = class {
436
592
  * Get a single resource by ID
437
593
  */
438
594
  async get(id) {
439
- 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}`);
440
597
  }
441
598
  /**
442
599
  * Create a new resource
@@ -448,7 +605,8 @@ var ResourcesModule = class {
448
605
  * Update a resource
449
606
  */
450
607
  async update(id, data) {
451
- 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);
452
610
  }
453
611
  /**
454
612
  * Delete a resource
@@ -470,6 +628,47 @@ var AssetsModule = class {
470
628
  this.http = http;
471
629
  }
472
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
+ }
473
672
  /**
474
673
  * List all assets
475
674
  */
@@ -480,16 +679,31 @@ var AssetsModule = class {
480
679
  * Get a single asset by ID
481
680
  */
482
681
  async get(id) {
483
- 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}`);
484
684
  }
485
685
  /**
486
686
  * Upload a new asset file
487
687
  *
488
688
  * @param file - The file to upload (File or Blob)
489
689
  * @param metadata - Asset metadata (slug, name, altText, description, metadata)
690
+ * @param options - Upload options (file size limits, allowed types)
490
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
+ * ```
491
704
  */
492
- async upload(file, metadata) {
705
+ async upload(file, metadata, options) {
706
+ this.validateFile(file, options);
493
707
  const formData = new FormData();
494
708
  formData.append("file", file);
495
709
  formData.append("slug", metadata.slug);
@@ -509,13 +723,24 @@ var AssetsModule = class {
509
723
  * Update an asset (editable fields only: slug, name, altText, description, metadata)
510
724
  */
511
725
  async update(id, data) {
512
- 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);
513
728
  }
514
729
  /**
515
730
  * Delete an asset (removes file from storage and database)
516
731
  */
517
732
  async delete(id) {
518
- 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`;
519
744
  }
520
745
  };
521
746
 
@@ -535,7 +760,8 @@ var APIClientsModule = class {
535
760
  * Get a single API client by ID
536
761
  */
537
762
  async get(id) {
538
- 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}`);
539
765
  }
540
766
  /**
541
767
  * Create a new API client (returns client with token)
@@ -547,7 +773,8 @@ var APIClientsModule = class {
547
773
  * Update an API client
548
774
  */
549
775
  async update(id, data) {
550
- 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);
551
778
  }
552
779
  /**
553
780
  * Delete an API client
@@ -758,11 +985,23 @@ var InvitesModule = class {
758
985
  }
759
986
  /**
760
987
  * Generate invitation URL from a token
761
- * Uses MARVIN_FRONTEND_URL for the base URL, falls back to MARVIN_API_URL with common port mappings
988
+ *
989
+ * @param token - Invitation token
990
+ * @param baseUrl - Base URL for the frontend (required in browser environments)
991
+ * @returns Full invitation URL
992
+ *
993
+ * @example
994
+ * ```typescript
995
+ * // Node.js (can use env vars)
996
+ * const url = invites.getInvitationUrl(token);
997
+ *
998
+ * // Browser (must provide baseUrl)
999
+ * const url = invites.getInvitationUrl(token, 'https://app.example.com');
1000
+ * ```
762
1001
  */
763
1002
  getInvitationUrl(token, baseUrl) {
764
1003
  let base = baseUrl;
765
- if (!base) {
1004
+ if (!base && typeof process !== "undefined" && process.env) {
766
1005
  base = process.env.MARVIN_FRONTEND_URL;
767
1006
  if (!base && process.env.MARVIN_API_URL) {
768
1007
  const apiUrl = process.env.MARVIN_API_URL;
@@ -774,11 +1013,17 @@ var InvitesModule = class {
774
1013
  base = apiUrl;
775
1014
  }
776
1015
  }
777
- if (!base) {
778
- base = "http://localhost:4321";
1016
+ }
1017
+ if (!base) {
1018
+ if (typeof window !== "undefined" && window.location) {
1019
+ base = window.location.origin;
1020
+ } else {
1021
+ throw new Error(
1022
+ 'baseUrl is required when not in Node.js environment. Pass the frontend URL as second argument: getInvitationUrl(token, "https://app.example.com")'
1023
+ );
779
1024
  }
780
1025
  }
781
- return `${base}/register?token=${token}`;
1026
+ return `${base}/register?token=${encodeURIComponent(token)}`;
782
1027
  }
783
1028
  };
784
1029
 
@@ -832,6 +1077,57 @@ var WebhooksModule = class {
832
1077
  this.http = http;
833
1078
  }
834
1079
  http;
1080
+ /**
1081
+ * Validate webhook URL to prevent SSRF attacks
1082
+ */
1083
+ validateWebhookUrl(url) {
1084
+ if (!url || typeof url !== "string") {
1085
+ throw new MarvinValidationError("Webhook URL is required and must be a string");
1086
+ }
1087
+ let parsed;
1088
+ try {
1089
+ parsed = new URL(url);
1090
+ } catch {
1091
+ throw new MarvinValidationError("Invalid webhook URL format. Expected: https://example.com/webhook");
1092
+ }
1093
+ if (!["http:", "https:"].includes(parsed.protocol)) {
1094
+ throw new MarvinValidationError(
1095
+ `Webhook URL must use HTTP or HTTPS protocol. Got: ${parsed.protocol}`
1096
+ );
1097
+ }
1098
+ const hostname = parsed.hostname.toLowerCase();
1099
+ const blockedHosts = [
1100
+ "localhost",
1101
+ "127.0.0.1",
1102
+ "0.0.0.0",
1103
+ "::1",
1104
+ "169.254.169.254",
1105
+ // AWS metadata service
1106
+ "[::]"
1107
+ ];
1108
+ if (blockedHosts.some((blocked) => hostname === blocked || hostname.includes(blocked))) {
1109
+ throw new MarvinValidationError(
1110
+ "Webhook URL cannot target localhost or internal network addresses"
1111
+ );
1112
+ }
1113
+ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
1114
+ const match = hostname.match(ipv4Regex);
1115
+ if (match) {
1116
+ const octets = match.slice(1).map(Number);
1117
+ if (octets[0] === 10) {
1118
+ throw new MarvinValidationError("Webhook URL cannot target private IP range (10.x.x.x)");
1119
+ }
1120
+ if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
1121
+ throw new MarvinValidationError("Webhook URL cannot target private IP range (172.16-31.x.x)");
1122
+ }
1123
+ if (octets[0] === 192 && octets[1] === 168) {
1124
+ throw new MarvinValidationError("Webhook URL cannot target private IP range (192.168.x.x)");
1125
+ }
1126
+ }
1127
+ if (url.length > 2048) {
1128
+ throw new MarvinValidationError("Webhook URL exceeds maximum length of 2048 characters");
1129
+ }
1130
+ }
835
1131
  /**
836
1132
  * List all webhooks for the current workspace
837
1133
  */
@@ -843,31 +1139,43 @@ var WebhooksModule = class {
843
1139
  * Get a webhook by ID
844
1140
  */
845
1141
  async get(id) {
846
- return this.http.get(`/api/groups/webhooks/${id}`);
1142
+ const validId = this.http.validatePathParam(id, "webhook ID");
1143
+ return this.http.get(`/api/groups/webhooks/${validId}`);
847
1144
  }
848
1145
  /**
849
1146
  * Create a new webhook
1147
+ * Validates URL to prevent SSRF attacks
850
1148
  */
851
1149
  async create(data) {
1150
+ if (data.url) {
1151
+ this.validateWebhookUrl(data.url);
1152
+ }
852
1153
  return this.http.post("/api/groups/webhooks", data);
853
1154
  }
854
1155
  /**
855
1156
  * Update a webhook
1157
+ * Validates URL if provided to prevent SSRF attacks
856
1158
  */
857
1159
  async update(id, data) {
858
- return this.http.put(`/api/groups/webhooks/${id}`, data);
1160
+ const validId = this.http.validatePathParam(id, "webhook ID");
1161
+ if (data.url) {
1162
+ this.validateWebhookUrl(data.url);
1163
+ }
1164
+ return this.http.put(`/api/groups/webhooks/${validId}`, data);
859
1165
  }
860
1166
  /**
861
1167
  * Delete a webhook
862
1168
  */
863
1169
  async delete(id) {
864
- await this.http.delete(`/api/groups/webhooks/${id}`);
1170
+ const validId = this.http.validatePathParam(id, "webhook ID");
1171
+ await this.http.delete(`/api/groups/webhooks/${validId}`);
865
1172
  }
866
1173
  /**
867
1174
  * Test a webhook
868
1175
  */
869
1176
  async test(id) {
870
- return this.http.post(`/api/groups/webhooks/${id}/test`, {});
1177
+ const validId = this.http.validatePathParam(id, "webhook ID");
1178
+ return this.http.post(`/api/groups/webhooks/${validId}/test`, {});
871
1179
  }
872
1180
  /**
873
1181
  * Rerun failed webhooks
@@ -959,6 +1267,7 @@ var AdminSystemModule = class {
959
1267
  }
960
1268
  /**
961
1269
  * Update email settings
1270
+ * Note: Password field is write-only and will not be returned in response
962
1271
  */
963
1272
  async updateEmailSettings(data) {
964
1273
  return this.http.put("/api/admin/email", data);
@@ -1309,7 +1618,8 @@ var ScheduledTasksModule = class {
1309
1618
  * Get a scheduled task by ID or slug
1310
1619
  */
1311
1620
  async get(idOrSlug) {
1312
- return this.http.get(`/api/platform/scheduled-tasks/${idOrSlug}`);
1621
+ const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
1622
+ return this.http.get(`/api/platform/scheduled-tasks/${validIdOrSlug}`);
1313
1623
  }
1314
1624
  /**
1315
1625
  * Create a new scheduled task
@@ -1321,7 +1631,8 @@ var ScheduledTasksModule = class {
1321
1631
  * Update a scheduled task
1322
1632
  */
1323
1633
  async update(idOrSlug, data) {
1324
- return this.http.patch(`/api/platform/scheduled-tasks/${idOrSlug}`, data);
1634
+ const validIdOrSlug = this.http.validatePathParam(idOrSlug, "idOrSlug");
1635
+ return this.http.patch(`/api/platform/scheduled-tasks/${validIdOrSlug}`, data);
1325
1636
  }
1326
1637
  /**
1327
1638
  * Delete a scheduled task
@@ -1367,6 +1678,34 @@ var FormsModule = class {
1367
1678
  this.http = http;
1368
1679
  }
1369
1680
  http;
1681
+ /**
1682
+ * Validate form submission data
1683
+ * - Prevents XSS by checking for script tags
1684
+ * - Validates data is a proper object
1685
+ * - Checks for reasonable size limits
1686
+ */
1687
+ validateFormData(data) {
1688
+ if (!data || typeof data !== "object") {
1689
+ throw new MarvinValidationError("Form data must be a valid object");
1690
+ }
1691
+ const jsonString = JSON.stringify(data);
1692
+ const sizeInBytes = new Blob([jsonString]).size;
1693
+ if (sizeInBytes > 1024 * 1024) {
1694
+ throw new MarvinValidationError("Form data exceeds maximum size of 1MB");
1695
+ }
1696
+ const checkForScripts = (value) => {
1697
+ if (typeof value === "string") {
1698
+ if (/<script[\s\S]*?>[\s\S]*?<\/script>/gi.test(value)) {
1699
+ throw new MarvinValidationError("Form data contains potentially malicious content");
1700
+ }
1701
+ } else if (Array.isArray(value)) {
1702
+ value.forEach(checkForScripts);
1703
+ } else if (value && typeof value === "object") {
1704
+ Object.values(value).forEach(checkForScripts);
1705
+ }
1706
+ };
1707
+ Object.values(data).forEach(checkForScripts);
1708
+ }
1370
1709
  /**
1371
1710
  * List all forms
1372
1711
  */
@@ -1377,7 +1716,8 @@ var FormsModule = class {
1377
1716
  * Get a single form by ID
1378
1717
  */
1379
1718
  async get(id) {
1380
- return this.http.get(`/api/platform/forms/${id}`);
1719
+ const validId = this.http.validatePathParam(id, "id");
1720
+ return this.http.get(`/api/platform/forms/${validId}`);
1381
1721
  }
1382
1722
  /**
1383
1723
  * Create a new form
@@ -1389,7 +1729,8 @@ var FormsModule = class {
1389
1729
  * Update a form
1390
1730
  */
1391
1731
  async update(id, data) {
1392
- return this.http.patch(`/api/platform/forms/${id}`, data);
1732
+ const validId = this.http.validatePathParam(id, "id");
1733
+ return this.http.patch(`/api/platform/forms/${validId}`, data);
1393
1734
  }
1394
1735
  /**
1395
1736
  * Delete a form
@@ -1411,15 +1752,25 @@ var FormsModule = class {
1411
1752
  }
1412
1753
  /**
1413
1754
  * Submit data to a published form (Publishing API - public access)
1755
+ * Validates data before submission to prevent XSS and malicious content
1414
1756
  */
1415
1757
  async submitForm(slug, data) {
1416
- return this.http.post(`/api/publish/forms/${slug}/submit`, {
1758
+ const validSlug = this.http.validatePathParam(slug, "form slug");
1759
+ this.validateFormData(data);
1760
+ return this.http.post(`/api/publish/forms/${validSlug}/submit`, {
1417
1761
  dataJson: data
1418
1762
  });
1419
1763
  }
1420
1764
  };
1421
1765
 
1422
1766
  // src/platform/client.ts
1767
+ function createPlatformConfigFromEnv(overrides) {
1768
+ return {
1769
+ apiUrl: overrides?.apiUrl || process.env.MARVIN_API_URL || "http://localhost:8080",
1770
+ userToken: overrides?.userToken || process.env.MARVIN_USER_TOKEN,
1771
+ credentials: overrides?.credentials || "include"
1772
+ };
1773
+ }
1423
1774
  var PlatformClient = class extends HttpClient {
1424
1775
  entries;
1425
1776
  collections;
@@ -1446,7 +1797,7 @@ var PlatformClient = class extends HttpClient {
1446
1797
  adminMaintenance;
1447
1798
  constructor(config) {
1448
1799
  const httpConfig = {
1449
- baseUrl: config.apiUrl,
1800
+ baseUrl: config.apiUrl || "http://localhost:8080",
1450
1801
  auth: new SessionAuth(config.userToken),
1451
1802
  credentials: config.credentials || "include"
1452
1803
  };
@@ -1476,7 +1827,8 @@ var PlatformClient = class extends HttpClient {
1476
1827
  }
1477
1828
  };
1478
1829
  function createPlatformClient(config) {
1479
- return new PlatformClient(config);
1830
+ const finalConfig = createPlatformConfigFromEnv(config);
1831
+ return new PlatformClient(finalConfig);
1480
1832
  }
1481
1833
  // Annotate the CommonJS export names for ESM import in node:
1482
1834
  0 && (module.exports = {
@@ -1503,5 +1855,6 @@ function createPlatformClient(config) {
1503
1855
  WebhooksModule,
1504
1856
  WorkspaceMembersModule,
1505
1857
  WorkspacesModule,
1506
- createPlatformClient
1858
+ createPlatformClient,
1859
+ createPlatformConfigFromEnv
1507
1860
  });