@inneropen/marvin-sdk 2.0.0-next.14 → 2.0.0-next.16

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,7 +345,8 @@ var HttpClient = class {
232
345
  }
233
346
  const data = await response.json();
234
347
  if (this.debug) {
235
- this.logger.log("[Marvin] Response:", JSON.stringify(data, null, 2));
348
+ const sanitized = this.sanitizeForLogging(data);
349
+ this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
236
350
  }
237
351
  return data;
238
352
  } catch (error) {
@@ -241,6 +355,17 @@ var HttpClient = class {
241
355
  throw error;
242
356
  }
243
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
+ }
244
369
  if (error.name === "AbortError") {
245
370
  throw new MarvinNetworkError(
246
371
  `Request timeout after ${this.timeout}ms`,
@@ -295,10 +420,27 @@ var SessionAuth = class {
295
420
  }
296
421
  sessionToken;
297
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
+ }
298
437
  injectAuth(headers) {
299
438
  if (this.sessionToken) {
300
439
  headers.set("Authorization", `Bearer ${this.sessionToken}`);
301
440
  }
441
+ if (this.csrfToken) {
442
+ headers.set("X-CSRF-Token", this.csrfToken);
443
+ }
302
444
  }
303
445
  async handleUnauthorized() {
304
446
  if (this.onUnauthorized) {
@@ -306,7 +448,9 @@ var SessionAuth = class {
306
448
  }
307
449
  }
308
450
  getDescription() {
309
- 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;
310
454
  }
311
455
  };
312
456
 
@@ -326,7 +470,8 @@ var EntriesModule = class {
326
470
  * Get a single entry by ID
327
471
  */
328
472
  async get(id) {
329
- 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}`);
330
475
  }
331
476
  /**
332
477
  * Create a new entry
@@ -338,26 +483,31 @@ var EntriesModule = class {
338
483
  * Update an entry
339
484
  */
340
485
  async update(id, data) {
341
- 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);
342
488
  }
343
489
  /**
344
490
  * Delete an entry
345
491
  */
346
492
  async delete(id) {
347
- 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}`);
348
495
  }
349
496
  /**
350
497
  * Get collections that this entry belongs to
351
498
  */
352
499
  async listCollections(entryId) {
353
- 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`);
354
502
  }
355
503
  /**
356
504
  * Add entry to a collection
357
505
  */
358
506
  async addToCollection(entryId, collectionId) {
507
+ const validEntryId = this.http.validatePathParam(entryId, "entry ID");
508
+ const validCollectionId = this.http.validatePathParam(collectionId, "collection ID");
359
509
  return this.http.post(
360
- `/api/platform/entries/${entryId}/collections/${collectionId}`,
510
+ `/api/platform/entries/${validEntryId}/collections/${validCollectionId}`,
361
511
  {}
362
512
  );
363
513
  }
@@ -365,7 +515,9 @@ var EntriesModule = class {
365
515
  * Remove entry from a collection
366
516
  */
367
517
  async removeFromCollection(entryId, collectionId) {
368
- 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}`);
369
521
  }
370
522
  };
371
523
 
@@ -385,7 +537,8 @@ var CollectionsModule = class {
385
537
  * Get a single collection by ID
386
538
  */
387
539
  async get(id) {
388
- 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}`);
389
542
  }
390
543
  /**
391
544
  * Create a new collection
@@ -397,25 +550,29 @@ var CollectionsModule = class {
397
550
  * Update a collection
398
551
  */
399
552
  async update(id, data) {
400
- 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);
401
555
  }
402
556
  /**
403
557
  * Delete a collection
404
558
  */
405
559
  async delete(id) {
406
- 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}`);
407
562
  }
408
563
  /**
409
564
  * Get entries in a collection
410
565
  */
411
566
  async getEntries(id) {
412
- 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`);
413
569
  }
414
570
  /**
415
571
  * Reorder entries in a collection
416
572
  */
417
573
  async reorderEntries(id, entries) {
418
- 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 });
419
576
  }
420
577
  };
421
578
 
@@ -435,7 +592,8 @@ var ResourcesModule = class {
435
592
  * Get a single resource by ID
436
593
  */
437
594
  async get(id) {
438
- 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}`);
439
597
  }
440
598
  /**
441
599
  * Create a new resource
@@ -447,7 +605,8 @@ var ResourcesModule = class {
447
605
  * Update a resource
448
606
  */
449
607
  async update(id, data) {
450
- 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);
451
610
  }
452
611
  /**
453
612
  * Delete a resource
@@ -469,6 +628,47 @@ var AssetsModule = class {
469
628
  this.http = http;
470
629
  }
471
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
+ }
472
672
  /**
473
673
  * List all assets
474
674
  */
@@ -479,16 +679,31 @@ var AssetsModule = class {
479
679
  * Get a single asset by ID
480
680
  */
481
681
  async get(id) {
482
- 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}`);
483
684
  }
484
685
  /**
485
686
  * Upload a new asset file
486
687
  *
487
688
  * @param file - The file to upload (File or Blob)
488
689
  * @param metadata - Asset metadata (slug, name, altText, description, metadata)
690
+ * @param options - Upload options (file size limits, allowed types)
489
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
+ * ```
490
704
  */
491
- async upload(file, metadata) {
705
+ async upload(file, metadata, options) {
706
+ this.validateFile(file, options);
492
707
  const formData = new FormData();
493
708
  formData.append("file", file);
494
709
  formData.append("slug", metadata.slug);
@@ -508,13 +723,24 @@ var AssetsModule = class {
508
723
  * Update an asset (editable fields only: slug, name, altText, description, metadata)
509
724
  */
510
725
  async update(id, data) {
511
- 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);
512
728
  }
513
729
  /**
514
730
  * Delete an asset (removes file from storage and database)
515
731
  */
516
732
  async delete(id) {
517
- 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`;
518
744
  }
519
745
  };
520
746
 
@@ -534,7 +760,8 @@ var APIClientsModule = class {
534
760
  * Get a single API client by ID
535
761
  */
536
762
  async get(id) {
537
- 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}`);
538
765
  }
539
766
  /**
540
767
  * Create a new API client (returns client with token)
@@ -546,7 +773,8 @@ var APIClientsModule = class {
546
773
  * Update an API client
547
774
  */
548
775
  async update(id, data) {
549
- 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);
550
778
  }
551
779
  /**
552
780
  * Delete an API client
@@ -757,11 +985,23 @@ var InvitesModule = class {
757
985
  }
758
986
  /**
759
987
  * Generate invitation URL from a token
760
- * 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
+ * ```
761
1001
  */
762
1002
  getInvitationUrl(token, baseUrl) {
763
1003
  let base = baseUrl;
764
- if (!base) {
1004
+ if (!base && typeof process !== "undefined" && process.env) {
765
1005
  base = process.env.MARVIN_FRONTEND_URL;
766
1006
  if (!base && process.env.MARVIN_API_URL) {
767
1007
  const apiUrl = process.env.MARVIN_API_URL;
@@ -773,11 +1013,17 @@ var InvitesModule = class {
773
1013
  base = apiUrl;
774
1014
  }
775
1015
  }
776
- if (!base) {
777
- 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
+ );
778
1024
  }
779
1025
  }
780
- return `${base}/register?token=${token}`;
1026
+ return `${base}/register?token=${encodeURIComponent(token)}`;
781
1027
  }
782
1028
  };
783
1029
 
@@ -831,6 +1077,57 @@ var WebhooksModule = class {
831
1077
  this.http = http;
832
1078
  }
833
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
+ }
834
1131
  /**
835
1132
  * List all webhooks for the current workspace
836
1133
  */
@@ -842,31 +1139,43 @@ var WebhooksModule = class {
842
1139
  * Get a webhook by ID
843
1140
  */
844
1141
  async get(id) {
845
- 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}`);
846
1144
  }
847
1145
  /**
848
1146
  * Create a new webhook
1147
+ * Validates URL to prevent SSRF attacks
849
1148
  */
850
1149
  async create(data) {
1150
+ if (data.url) {
1151
+ this.validateWebhookUrl(data.url);
1152
+ }
851
1153
  return this.http.post("/api/groups/webhooks", data);
852
1154
  }
853
1155
  /**
854
1156
  * Update a webhook
1157
+ * Validates URL if provided to prevent SSRF attacks
855
1158
  */
856
1159
  async update(id, data) {
857
- 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);
858
1165
  }
859
1166
  /**
860
1167
  * Delete a webhook
861
1168
  */
862
1169
  async delete(id) {
863
- 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}`);
864
1172
  }
865
1173
  /**
866
1174
  * Test a webhook
867
1175
  */
868
1176
  async test(id) {
869
- 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`, {});
870
1179
  }
871
1180
  /**
872
1181
  * Rerun failed webhooks
@@ -958,6 +1267,7 @@ var AdminSystemModule = class {
958
1267
  }
959
1268
  /**
960
1269
  * Update email settings
1270
+ * Note: Password field is write-only and will not be returned in response
961
1271
  */
962
1272
  async updateEmailSettings(data) {
963
1273
  return this.http.put("/api/admin/email", data);
@@ -1308,7 +1618,8 @@ var ScheduledTasksModule = class {
1308
1618
  * Get a scheduled task by ID or slug
1309
1619
  */
1310
1620
  async get(idOrSlug) {
1311
- 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}`);
1312
1623
  }
1313
1624
  /**
1314
1625
  * Create a new scheduled task
@@ -1320,7 +1631,8 @@ var ScheduledTasksModule = class {
1320
1631
  * Update a scheduled task
1321
1632
  */
1322
1633
  async update(idOrSlug, data) {
1323
- 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);
1324
1636
  }
1325
1637
  /**
1326
1638
  * Delete a scheduled task
@@ -1366,6 +1678,34 @@ var FormsModule = class {
1366
1678
  this.http = http;
1367
1679
  }
1368
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
+ }
1369
1709
  /**
1370
1710
  * List all forms
1371
1711
  */
@@ -1376,7 +1716,8 @@ var FormsModule = class {
1376
1716
  * Get a single form by ID
1377
1717
  */
1378
1718
  async get(id) {
1379
- 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}`);
1380
1721
  }
1381
1722
  /**
1382
1723
  * Create a new form
@@ -1388,7 +1729,8 @@ var FormsModule = class {
1388
1729
  * Update a form
1389
1730
  */
1390
1731
  async update(id, data) {
1391
- 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);
1392
1734
  }
1393
1735
  /**
1394
1736
  * Delete a form
@@ -1410,15 +1752,25 @@ var FormsModule = class {
1410
1752
  }
1411
1753
  /**
1412
1754
  * Submit data to a published form (Publishing API - public access)
1755
+ * Validates data before submission to prevent XSS and malicious content
1413
1756
  */
1414
1757
  async submitForm(slug, data) {
1415
- 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`, {
1416
1761
  dataJson: data
1417
1762
  });
1418
1763
  }
1419
1764
  };
1420
1765
 
1421
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
+ }
1422
1774
  var PlatformClient = class extends HttpClient {
1423
1775
  entries;
1424
1776
  collections;
@@ -1445,7 +1797,7 @@ var PlatformClient = class extends HttpClient {
1445
1797
  adminMaintenance;
1446
1798
  constructor(config) {
1447
1799
  const httpConfig = {
1448
- baseUrl: config.apiUrl,
1800
+ baseUrl: config.apiUrl || "http://localhost:8080",
1449
1801
  auth: new SessionAuth(config.userToken),
1450
1802
  credentials: config.credentials || "include"
1451
1803
  };
@@ -1475,7 +1827,8 @@ var PlatformClient = class extends HttpClient {
1475
1827
  }
1476
1828
  };
1477
1829
  function createPlatformClient(config) {
1478
- return new PlatformClient(config);
1830
+ const finalConfig = createPlatformConfigFromEnv(config);
1831
+ return new PlatformClient(finalConfig);
1479
1832
  }
1480
1833
  // Annotate the CommonJS export names for ESM import in node:
1481
1834
  0 && (module.exports = {
@@ -1502,5 +1855,6 @@ function createPlatformClient(config) {
1502
1855
  WebhooksModule,
1503
1856
  WorkspaceMembersModule,
1504
1857
  WorkspacesModule,
1505
- createPlatformClient
1858
+ createPlatformClient,
1859
+ createPlatformConfigFromEnv
1506
1860
  });