@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.
@@ -0,0 +1,1355 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+
23
+ // src/client/config.ts
24
+ var config_exports = {};
25
+ __export(config_exports, {
26
+ createConfigFromEnv: () => createConfigFromEnv,
27
+ validateConfig: () => validateConfig
28
+ });
29
+ function validateConfig(config) {
30
+ const receivedKeys = Object.keys(config).join(", ");
31
+ if (!config.apiUrl) {
32
+ throw new Error(
33
+ `Missing required config: 'apiUrl'
34
+
35
+ The API URL is the base URL of your Marvin server.
36
+
37
+ Examples:
38
+ - Production: https://marvin.example.com
39
+ - Development: http://localhost:8080
40
+
41
+ Solutions:
42
+ 1. Set environment variable: MARVIN_API_URL=https://marvin.example.com
43
+ 2. Pass directly: createMarvinClient({ apiUrl: 'https://marvin.example.com' })
44
+
45
+ Received config keys: ${receivedKeys || "(none)"}`
46
+ );
47
+ }
48
+ if (!config.siteClientToken) {
49
+ throw new Error(
50
+ `Missing required config: 'siteClientToken'
51
+
52
+ The site client token provides read-only access to published content.
53
+
54
+ How to get it:
55
+ 1. Log into Marvin admin panel
56
+ 2. Navigate to: Settings \u2192 API Clients
57
+ 3. Copy the token for your workspace
58
+
59
+ Examples:
60
+ - site_client_abc123xyz
61
+ - sc_1234567890abcdef
62
+
63
+ Solutions:
64
+ 1. Set environment variable: MARVIN_SITE_CLIENT_TOKEN=site_client_abc123
65
+ 2. Pass directly: createMarvinClient({ siteClientToken: 'site_client_abc123' })
66
+
67
+ Received config keys: ${receivedKeys}`
68
+ );
69
+ }
70
+ if (!config.workspaceSlug) {
71
+ throw new Error(
72
+ `Missing required config: 'workspaceSlug'
73
+
74
+ The workspace slug identifies which workspace to access.
75
+
76
+ Examples:
77
+ - my-blog
78
+ - company-website
79
+ - acme-corp
80
+
81
+ How to find it:
82
+ 1. Check your workspace URL: https://marvin.example.com/workspace/[slug]
83
+ 2. Or find it in: Settings \u2192 General \u2192 Workspace Slug
84
+
85
+ Solutions:
86
+ 1. Set environment variable: MARVIN_WORKSPACE_SLUG=my-blog
87
+ 2. Pass directly: createMarvinClient({ workspaceSlug: 'my-blog' })
88
+
89
+ Received config keys: ${receivedKeys}`
90
+ );
91
+ }
92
+ if (config.apiUrl) {
93
+ try {
94
+ new URL(config.apiUrl);
95
+ } catch {
96
+ throw new Error(
97
+ `Invalid 'apiUrl' format: "${config.apiUrl}"
98
+
99
+ The API URL must be a valid HTTP or HTTPS URL.
100
+
101
+ Valid examples:
102
+ \u2713 https://marvin.example.com
103
+ \u2713 http://localhost:8080
104
+ \u2713 https://api.marvin.io
105
+
106
+ Invalid examples:
107
+ \u2717 marvin.example.com (missing protocol)
108
+ \u2717 ftp://marvin.example.com (wrong protocol)
109
+ \u2717 /api (not a full URL)`
110
+ );
111
+ }
112
+ const url = new URL(config.apiUrl);
113
+ if (!["http:", "https:"].includes(url.protocol)) {
114
+ throw new Error(
115
+ `Invalid 'apiUrl' protocol: "${url.protocol}"
116
+
117
+ Only HTTP and HTTPS protocols are supported.
118
+
119
+ Your URL: ${config.apiUrl}
120
+ Change to: ${config.apiUrl.replace(url.protocol, "https:")}`
121
+ );
122
+ }
123
+ }
124
+ }
125
+ function getEnv(key) {
126
+ if (typeof process !== "undefined" && process.env) {
127
+ return process.env[key];
128
+ }
129
+ return void 0;
130
+ }
131
+ function createConfigFromEnv(overrides) {
132
+ return {
133
+ apiUrl: overrides?.apiUrl || getEnv("MARVIN_API_URL") || "",
134
+ siteClientToken: overrides?.siteClientToken || getEnv("MARVIN_SITE_CLIENT_TOKEN") || "",
135
+ workspaceSlug: overrides?.workspaceSlug || getEnv("MARVIN_WORKSPACE_SLUG") || "",
136
+ autoInitialize: overrides?.autoInitialize ?? false,
137
+ cacheDuration: overrides?.cacheDuration ?? 5 * 60 * 1e3,
138
+ // 5 minutes
139
+ debug: overrides?.debug ?? getEnv("MARVIN_DEBUG") === "true",
140
+ logger: overrides?.logger ?? console
141
+ };
142
+ }
143
+ var init_config = __esm({
144
+ "src/client/config.ts"() {
145
+ "use strict";
146
+ }
147
+ });
148
+
149
+ // src/publish.ts
150
+ var publish_exports = {};
151
+ __export(publish_exports, {
152
+ BearerTokenAuth: () => BearerTokenAuth,
153
+ Collection: () => Collection,
154
+ Entry: () => Entry,
155
+ HttpClient: () => HttpClient,
156
+ MarvinApiError: () => MarvinApiError,
157
+ MarvinAuthError: () => MarvinAuthError,
158
+ MarvinCache: () => MarvinCache,
159
+ MarvinClient: () => MarvinClient,
160
+ MarvinConfigError: () => MarvinConfigError,
161
+ MarvinError: () => MarvinError,
162
+ MarvinNetworkError: () => MarvinNetworkError,
163
+ MarvinNotFoundError: () => MarvinNotFoundError,
164
+ MarvinServerError: () => MarvinServerError,
165
+ MarvinValidationError: () => MarvinValidationError,
166
+ Resource: () => Resource,
167
+ Workspace: () => Workspace,
168
+ createConfigFromEnv: () => createConfigFromEnv,
169
+ createMarvinClient: () => createMarvinClient,
170
+ createPaginatedResponse: () => createPaginatedResponse,
171
+ extractData: () => extractData,
172
+ isPaginatedResponse: () => isPaginatedResponse,
173
+ offsetToPage: () => offsetToPage,
174
+ pageToOffset: () => pageToOffset
175
+ });
176
+ module.exports = __toCommonJS(publish_exports);
177
+
178
+ // src/client/marvin.ts
179
+ init_config();
180
+
181
+ // src/core/errors/MarvinError.ts
182
+ var MarvinError = class _MarvinError extends Error {
183
+ constructor(message, code, statusCode) {
184
+ super(message);
185
+ this.code = code;
186
+ this.statusCode = statusCode;
187
+ this.name = "MarvinError";
188
+ Object.setPrototypeOf(this, _MarvinError.prototype);
189
+ }
190
+ code;
191
+ statusCode;
192
+ };
193
+ var MarvinApiError = class _MarvinApiError extends MarvinError {
194
+ constructor(message, statusCode, endpoint, responseBody) {
195
+ super(message, "API_ERROR", statusCode);
196
+ this.statusCode = statusCode;
197
+ this.endpoint = endpoint;
198
+ this.responseBody = responseBody;
199
+ this.name = "MarvinApiError";
200
+ Object.setPrototypeOf(this, _MarvinApiError.prototype);
201
+ }
202
+ statusCode;
203
+ endpoint;
204
+ responseBody;
205
+ static fromResponse(status, statusText, endpoint, body) {
206
+ const message = `Marvin API error: ${status} ${statusText} at ${endpoint}${body ? `
207
+ ${body}` : ""}`;
208
+ return new _MarvinApiError(message, status, endpoint, body);
209
+ }
210
+ };
211
+ var MarvinAuthError = class _MarvinAuthError extends MarvinError {
212
+ constructor(message, statusCode) {
213
+ super(message, "AUTH_ERROR", statusCode);
214
+ this.name = "MarvinAuthError";
215
+ Object.setPrototypeOf(this, _MarvinAuthError.prototype);
216
+ }
217
+ };
218
+ var MarvinConfigError = class _MarvinConfigError extends MarvinError {
219
+ constructor(message) {
220
+ super(message, "CONFIG_ERROR");
221
+ this.name = "MarvinConfigError";
222
+ Object.setPrototypeOf(this, _MarvinConfigError.prototype);
223
+ }
224
+ };
225
+ var MarvinValidationError = class _MarvinValidationError extends MarvinError {
226
+ constructor(message, field) {
227
+ super(message, "VALIDATION_ERROR");
228
+ this.field = field;
229
+ this.name = "MarvinValidationError";
230
+ Object.setPrototypeOf(this, _MarvinValidationError.prototype);
231
+ }
232
+ field;
233
+ };
234
+ var MarvinNotFoundError = class _MarvinNotFoundError extends MarvinError {
235
+ constructor(message, endpoint) {
236
+ super(message, "NOT_FOUND", 404);
237
+ this.endpoint = endpoint;
238
+ this.name = "MarvinNotFoundError";
239
+ Object.setPrototypeOf(this, _MarvinNotFoundError.prototype);
240
+ }
241
+ endpoint;
242
+ };
243
+ var MarvinNetworkError = class _MarvinNetworkError extends MarvinError {
244
+ constructor(message, originalError) {
245
+ super(message, "NETWORK_ERROR");
246
+ this.originalError = originalError;
247
+ this.name = "MarvinNetworkError";
248
+ Object.setPrototypeOf(this, _MarvinNetworkError.prototype);
249
+ }
250
+ originalError;
251
+ };
252
+ var MarvinServerError = class _MarvinServerError extends MarvinError {
253
+ constructor(message, statusCode, endpoint) {
254
+ super(message, "SERVER_ERROR", statusCode);
255
+ this.endpoint = endpoint;
256
+ this.name = "MarvinServerError";
257
+ Object.setPrototypeOf(this, _MarvinServerError.prototype);
258
+ }
259
+ endpoint;
260
+ };
261
+
262
+ // src/core/http/HttpClient.ts
263
+ var HttpClient = class {
264
+ baseUrl;
265
+ auth;
266
+ defaultHeaders;
267
+ credentials;
268
+ timeout;
269
+ debug;
270
+ retryConfig;
271
+ logger;
272
+ constructor(config) {
273
+ this.baseUrl = config.baseUrl;
274
+ this.auth = config.auth;
275
+ this.defaultHeaders = config.defaultHeaders || {};
276
+ this.credentials = config.credentials || "same-origin";
277
+ const MAX_TIMEOUT = 12e4;
278
+ this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
279
+ this.debug = config.debug ?? false;
280
+ this.logger = config.logger ?? console;
281
+ this.retryConfig = {
282
+ maxRetries: config.retry?.maxRetries ?? 3,
283
+ initialDelay: config.retry?.initialDelay ?? 1e3,
284
+ maxDelay: config.retry?.maxDelay ?? 1e4,
285
+ retryableStatuses: config.retry?.retryableStatuses ?? [408, 429, 500, 502, 503, 504]
286
+ };
287
+ }
288
+ /**
289
+ * Validate path parameter to prevent path traversal attacks
290
+ */
291
+ validatePathParam(param, paramName = "parameter") {
292
+ if (typeof param === "number") {
293
+ return String(param);
294
+ }
295
+ if (!param || typeof param !== "string") {
296
+ throw new MarvinValidationError(`Invalid ${paramName}: must be a non-empty string or number`);
297
+ }
298
+ if (param.includes("..") || param.includes("/") || param.includes("\\")) {
299
+ throw new MarvinValidationError(
300
+ `Invalid ${paramName}: cannot contain path traversal characters (.. / \\)`
301
+ );
302
+ }
303
+ if (param.includes("\0")) {
304
+ throw new MarvinValidationError(`Invalid ${paramName}: cannot contain null bytes`);
305
+ }
306
+ if (param.length > 255) {
307
+ throw new MarvinValidationError(`Invalid ${paramName}: exceeds maximum length of 255 characters`);
308
+ }
309
+ return param;
310
+ }
311
+ /**
312
+ * Build full URL from endpoint
313
+ */
314
+ buildUrl(endpoint) {
315
+ const base = this.baseUrl.replace(/\/$/, "");
316
+ const path = endpoint.replace(/^\//, "");
317
+ return `${base}/${path}`;
318
+ }
319
+ /**
320
+ * Build headers for request
321
+ */
322
+ buildHeaders(additionalHeaders, isFormData = false) {
323
+ const headerInit = {};
324
+ if (!isFormData) {
325
+ headerInit["Content-Type"] = "application/json";
326
+ }
327
+ const headers = new Headers({
328
+ ...headerInit,
329
+ ...this.defaultHeaders,
330
+ ...additionalHeaders
331
+ });
332
+ this.auth.injectAuth(headers);
333
+ return headers;
334
+ }
335
+ /**
336
+ * Build query string from params
337
+ */
338
+ buildQueryString(params) {
339
+ const filtered = Object.entries(params).filter(([_, value]) => value !== void 0).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
340
+ return filtered.length > 0 ? `?${filtered.join("&")}` : "";
341
+ }
342
+ /**
343
+ * Sanitize data for logging by redacting sensitive fields
344
+ */
345
+ sanitizeForLogging(data) {
346
+ if (!data || typeof data !== "object") {
347
+ return data;
348
+ }
349
+ const sensitiveFields = [
350
+ "token",
351
+ "access_token",
352
+ "refresh_token",
353
+ "password",
354
+ "secret",
355
+ "apiKey",
356
+ "api_key",
357
+ "bearer",
358
+ "authorization",
359
+ "csrf_token",
360
+ "session",
361
+ "cookie"
362
+ ];
363
+ const sanitize = (obj) => {
364
+ const sanitized = {};
365
+ for (const [key, value] of Object.entries(obj)) {
366
+ const keyLower = key.toLowerCase();
367
+ const isSensitive = sensitiveFields.some((field) => keyLower.includes(field));
368
+ if (isSensitive) {
369
+ sanitized[key] = "[REDACTED]";
370
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
371
+ sanitized[key] = sanitize(value);
372
+ } else if (Array.isArray(value)) {
373
+ sanitized[key] = value.map(
374
+ (item) => item && typeof item === "object" ? sanitize(item) : item
375
+ );
376
+ } else {
377
+ sanitized[key] = value;
378
+ }
379
+ }
380
+ return sanitized;
381
+ };
382
+ if (Array.isArray(data)) {
383
+ return data.map(
384
+ (item) => item && typeof item === "object" ? sanitize(item) : item
385
+ );
386
+ }
387
+ return sanitize(data);
388
+ }
389
+ /**
390
+ * Calculate delay for retry with exponential backoff
391
+ */
392
+ calculateRetryDelay(attempt) {
393
+ const delay = this.retryConfig.initialDelay * Math.pow(2, attempt);
394
+ return Math.min(delay, this.retryConfig.maxDelay);
395
+ }
396
+ /**
397
+ * Sleep for specified milliseconds
398
+ */
399
+ sleep(ms) {
400
+ return new Promise((resolve) => setTimeout(resolve, ms));
401
+ }
402
+ /**
403
+ * Perform HTTP request with automatic retry on transient failures
404
+ */
405
+ async request(method, endpoint, options, retryAttempt = 0) {
406
+ const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
407
+ const isFormData = options?.body instanceof FormData;
408
+ const headers = this.buildHeaders(options?.headers, isFormData);
409
+ const controller = new AbortController();
410
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
411
+ const startTime = Date.now();
412
+ if (this.debug) {
413
+ this.logger.log(`[Marvin] ${method} ${url}`);
414
+ }
415
+ try {
416
+ const response = await fetch(url, {
417
+ method,
418
+ headers,
419
+ body: isFormData ? options.body : options?.body ? JSON.stringify(options.body) : void 0,
420
+ credentials: this.credentials,
421
+ signal: controller.signal
422
+ });
423
+ clearTimeout(timeoutId);
424
+ const duration = Date.now() - startTime;
425
+ if (this.debug) {
426
+ this.logger.log(
427
+ `[Marvin] ${response.status} ${response.statusText} (${duration}ms)`
428
+ );
429
+ }
430
+ if (response.status === 401 && this.auth.handleUnauthorized) {
431
+ await this.auth.handleUnauthorized();
432
+ }
433
+ if (!response.ok) {
434
+ const errorText = await response.text().catch(() => "");
435
+ if (this.debug) {
436
+ this.logger.error(
437
+ `[Marvin] Error response: ${response.status} ${response.statusText}`,
438
+ errorText ? `Body: ${errorText}` : ""
439
+ );
440
+ }
441
+ const isRetryable = this.retryConfig.retryableStatuses.includes(response.status);
442
+ const canRetry = retryAttempt < this.retryConfig.maxRetries;
443
+ if (isRetryable && canRetry) {
444
+ const delay = this.calculateRetryDelay(retryAttempt);
445
+ if (this.debug) {
446
+ this.logger.warn(
447
+ `[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (status: ${response.status})`
448
+ );
449
+ }
450
+ await this.sleep(delay);
451
+ return this.request(method, endpoint, options, retryAttempt + 1);
452
+ }
453
+ if (response.status === 404) {
454
+ throw new MarvinNotFoundError(
455
+ `Resource not found: ${endpoint}`,
456
+ endpoint
457
+ );
458
+ }
459
+ if (response.status === 401 || response.status === 403) {
460
+ throw new MarvinAuthError(
461
+ `Authentication failed: ${response.statusText}`,
462
+ response.status
463
+ );
464
+ }
465
+ if (response.status >= 500) {
466
+ throw new MarvinServerError(
467
+ `Server error: ${response.status} ${response.statusText}`,
468
+ response.status,
469
+ endpoint
470
+ );
471
+ }
472
+ throw MarvinApiError.fromResponse(
473
+ response.status,
474
+ response.statusText,
475
+ endpoint,
476
+ errorText
477
+ );
478
+ }
479
+ const contentType = response.headers.get("content-type");
480
+ if (!contentType || !contentType.includes("application/json")) {
481
+ return void 0;
482
+ }
483
+ const data = await response.json();
484
+ if (this.debug) {
485
+ const sanitized = this.sanitizeForLogging(data);
486
+ this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
487
+ }
488
+ return data;
489
+ } catch (error) {
490
+ clearTimeout(timeoutId);
491
+ if (error instanceof MarvinApiError || error instanceof MarvinNotFoundError || error instanceof MarvinAuthError || error instanceof MarvinServerError) {
492
+ throw error;
493
+ }
494
+ if (error instanceof Error) {
495
+ const canRetry = retryAttempt < this.retryConfig.maxRetries;
496
+ if (canRetry && error.name !== "AbortError") {
497
+ const delay = this.calculateRetryDelay(retryAttempt);
498
+ if (this.debug) {
499
+ this.logger.warn(
500
+ `[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (network error)`
501
+ );
502
+ }
503
+ await this.sleep(delay);
504
+ return this.request(method, endpoint, options, retryAttempt + 1);
505
+ }
506
+ if (error.name === "AbortError") {
507
+ throw new MarvinNetworkError(
508
+ `Request timeout after ${this.timeout}ms`,
509
+ error
510
+ );
511
+ }
512
+ throw new MarvinNetworkError(
513
+ `Network error: ${error.message}`,
514
+ error
515
+ );
516
+ }
517
+ throw error;
518
+ }
519
+ }
520
+ /**
521
+ * GET request
522
+ */
523
+ async get(endpoint, params) {
524
+ return this.request("GET", endpoint, { params });
525
+ }
526
+ /**
527
+ * POST request
528
+ */
529
+ async post(endpoint, body) {
530
+ return this.request("POST", endpoint, { body });
531
+ }
532
+ /**
533
+ * PUT request
534
+ */
535
+ async put(endpoint, body) {
536
+ return this.request("PUT", endpoint, { body });
537
+ }
538
+ /**
539
+ * PATCH request
540
+ */
541
+ async patch(endpoint, body) {
542
+ return this.request("PATCH", endpoint, { body });
543
+ }
544
+ /**
545
+ * DELETE request
546
+ */
547
+ async delete(endpoint) {
548
+ return this.request("DELETE", endpoint);
549
+ }
550
+ };
551
+
552
+ // src/core/auth/AuthStrategy.ts
553
+ var BearerTokenAuth = class {
554
+ constructor(token) {
555
+ this.token = token;
556
+ }
557
+ token;
558
+ injectAuth(headers) {
559
+ headers.set("Authorization", `Bearer ${this.token}`);
560
+ }
561
+ getDescription() {
562
+ return "Bearer token authentication";
563
+ }
564
+ };
565
+
566
+ // src/core/pagination/PaginatedResponse.ts
567
+ function createPaginatedResponse(data, meta) {
568
+ return { data, meta };
569
+ }
570
+ function extractData(response) {
571
+ if (Array.isArray(response)) {
572
+ return response;
573
+ }
574
+ return response.data;
575
+ }
576
+ function isPaginatedResponse(response) {
577
+ return !Array.isArray(response) && "data" in response;
578
+ }
579
+ function pageToOffset(page, limit) {
580
+ return (page - 1) * limit;
581
+ }
582
+ function offsetToPage(offset, limit) {
583
+ return Math.floor(offset / limit) + 1;
584
+ }
585
+
586
+ // src/core/cache.ts
587
+ var MarvinCache = class {
588
+ constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
589
+ this.duration = duration;
590
+ this.maxEntries = maxEntries;
591
+ }
592
+ duration;
593
+ maxEntries;
594
+ cache = /* @__PURE__ */ new Map();
595
+ /**
596
+ * Get cached data by key
597
+ * Returns undefined if not found or expired
598
+ */
599
+ get(key) {
600
+ const entry = this.cache.get(key);
601
+ if (!entry) return void 0;
602
+ const age = Date.now() - entry.timestamp;
603
+ if (age > this.duration) {
604
+ this.cache.delete(key);
605
+ return void 0;
606
+ }
607
+ return entry.data;
608
+ }
609
+ /**
610
+ * Set cached data with optional tags for invalidation
611
+ */
612
+ set(key, data, tags) {
613
+ if (this.cache.size >= this.maxEntries) {
614
+ const oldestKey = this.cache.keys().next().value;
615
+ if (oldestKey) {
616
+ this.cache.delete(oldestKey);
617
+ }
618
+ }
619
+ this.cache.set(key, {
620
+ data,
621
+ timestamp: Date.now(),
622
+ tags
623
+ });
624
+ }
625
+ /**
626
+ * Check if key exists and is not expired
627
+ */
628
+ has(key) {
629
+ return this.get(key) !== void 0;
630
+ }
631
+ /**
632
+ * Clear all cached entries
633
+ */
634
+ clear() {
635
+ this.cache.clear();
636
+ }
637
+ /**
638
+ * Delete a specific cache entry
639
+ */
640
+ delete(key) {
641
+ this.cache.delete(key);
642
+ }
643
+ /**
644
+ * Invalidate all cache entries with a specific tag
645
+ * Useful for invalidating related data after mutations
646
+ *
647
+ * @example
648
+ * ```typescript
649
+ * // Cache entries with tags
650
+ * cache.set('entry:1', data, ['entries', 'entry:1']);
651
+ * cache.set('entries:list', data, ['entries']);
652
+ *
653
+ * // Invalidate all entries
654
+ * cache.invalidateByTag('entries');
655
+ * ```
656
+ */
657
+ invalidateByTag(tag) {
658
+ let count = 0;
659
+ for (const [key, entry] of this.cache.entries()) {
660
+ if (entry.tags?.includes(tag)) {
661
+ this.cache.delete(key);
662
+ count++;
663
+ }
664
+ }
665
+ return count;
666
+ }
667
+ /**
668
+ * Get cache statistics
669
+ */
670
+ getStats() {
671
+ let oldest = null;
672
+ let newest = null;
673
+ for (const entry of this.cache.values()) {
674
+ if (oldest === null || entry.timestamp < oldest) {
675
+ oldest = entry.timestamp;
676
+ }
677
+ if (newest === null || entry.timestamp > newest) {
678
+ newest = entry.timestamp;
679
+ }
680
+ }
681
+ return {
682
+ size: this.cache.size,
683
+ maxEntries: this.maxEntries,
684
+ oldestEntry: oldest,
685
+ newestEntry: newest
686
+ };
687
+ }
688
+ /**
689
+ * Remove expired entries (garbage collection)
690
+ * Returns number of entries removed
691
+ */
692
+ prune() {
693
+ let count = 0;
694
+ const now = Date.now();
695
+ for (const [key, entry] of this.cache.entries()) {
696
+ const age = now - entry.timestamp;
697
+ if (age > this.duration) {
698
+ this.cache.delete(key);
699
+ count++;
700
+ }
701
+ }
702
+ return count;
703
+ }
704
+ };
705
+
706
+ // src/client/http.ts
707
+ var MarvinHttpClient = class extends HttpClient {
708
+ constructor(config) {
709
+ super({
710
+ baseUrl: config.apiUrl,
711
+ auth: new BearerTokenAuth(config.siteClientToken),
712
+ credentials: "same-origin",
713
+ debug: config.debug,
714
+ logger: config.logger
715
+ });
716
+ }
717
+ /**
718
+ * Fetch endpoint (backward compatibility)
719
+ */
720
+ async fetch(endpoint) {
721
+ return this.get(endpoint);
722
+ }
723
+ };
724
+
725
+ // src/entries/entry.ts
726
+ var Entry = class {
727
+ constructor(raw) {
728
+ this.raw = raw;
729
+ }
730
+ raw;
731
+ get title() {
732
+ return this.raw.title;
733
+ }
734
+ get slug() {
735
+ return this.raw.slug;
736
+ }
737
+ get summary() {
738
+ return this.raw.summary;
739
+ }
740
+ get entryType() {
741
+ return this.raw.entryType;
742
+ }
743
+ get entryTypeInfo() {
744
+ return this.raw.entryTypeInfo;
745
+ }
746
+ get data() {
747
+ return this.raw.data;
748
+ }
749
+ get metadataJson() {
750
+ return this.raw.metadataJson;
751
+ }
752
+ get publishedAt() {
753
+ return this.raw.publishedAt;
754
+ }
755
+ get order() {
756
+ return this.raw.order;
757
+ }
758
+ get assets() {
759
+ return this.raw.assets;
760
+ }
761
+ get collections() {
762
+ return this.raw.collections;
763
+ }
764
+ get resources() {
765
+ return this.raw.resources;
766
+ }
767
+ field(key) {
768
+ return this.raw.data[key];
769
+ }
770
+ get fields() {
771
+ return this.raw.data;
772
+ }
773
+ toJSON() {
774
+ return this.raw;
775
+ }
776
+ };
777
+
778
+ // src/entries/entries.ts
779
+ var EntriesModule = class {
780
+ constructor(http, workspaceSlug) {
781
+ this.http = http;
782
+ this.workspaceSlug = workspaceSlug;
783
+ }
784
+ http;
785
+ workspaceSlug;
786
+ /**
787
+ * Get all published entries
788
+ */
789
+ async list(options = {}) {
790
+ const queryString = this.http.buildQueryString({
791
+ entry_type: options.entryType,
792
+ collection: options.collection,
793
+ limit: options.limit,
794
+ offset: options.offset
795
+ });
796
+ const endpoint = `/api/publish/${this.workspaceSlug}/entries${queryString}`;
797
+ const response = await this.http.fetch(endpoint);
798
+ return response.data || [];
799
+ }
800
+ /**
801
+ * Get a single entry by slug
802
+ * Returns null if entry is not found (404)
803
+ */
804
+ async get(slug) {
805
+ try {
806
+ const endpoint = `/api/publish/${this.workspaceSlug}/entries/${slug}`;
807
+ const data = await this.http.fetch(endpoint);
808
+ return new Entry(data);
809
+ } catch (error) {
810
+ if (error instanceof MarvinNotFoundError) {
811
+ return null;
812
+ }
813
+ throw error;
814
+ }
815
+ }
816
+ /**
817
+ * Convenience: Get all pages
818
+ */
819
+ async pages(options) {
820
+ return this.list({ ...options, entryType: "page" });
821
+ }
822
+ /**
823
+ * Convenience: Get all blog posts
824
+ */
825
+ async posts(options) {
826
+ return this.list({ ...options, entryType: "blog" });
827
+ }
828
+ /**
829
+ * Convenience: Get all projects
830
+ */
831
+ async projects(options) {
832
+ return this.list({ ...options, entryType: "project" });
833
+ }
834
+ };
835
+
836
+ // src/collections/collection.ts
837
+ var Collection = class {
838
+ constructor(data) {
839
+ this.data = data;
840
+ }
841
+ data;
842
+ get name() {
843
+ return this.data.name;
844
+ }
845
+ get slug() {
846
+ return this.data.slug;
847
+ }
848
+ get description() {
849
+ return this.data.description;
850
+ }
851
+ get isSmart() {
852
+ return this.data.isSmart;
853
+ }
854
+ get smartRules() {
855
+ return this.data.smartRules;
856
+ }
857
+ get metadataJson() {
858
+ return this.data.metadataJson;
859
+ }
860
+ get entryCount() {
861
+ return this.data.entryCount;
862
+ }
863
+ get entries() {
864
+ return this.data.entries;
865
+ }
866
+ toJSON() {
867
+ return this.data;
868
+ }
869
+ };
870
+
871
+ // src/collections/collections.ts
872
+ var CollectionsModule = class {
873
+ constructor(http, workspaceSlug) {
874
+ this.http = http;
875
+ this.workspaceSlug = workspaceSlug;
876
+ }
877
+ http;
878
+ workspaceSlug;
879
+ /**
880
+ * Get all collections
881
+ */
882
+ async list() {
883
+ const endpoint = `/api/publish/${this.workspaceSlug}/collections`;
884
+ const response = await this.http.fetch(endpoint);
885
+ return response.data || [];
886
+ }
887
+ /**
888
+ * Get a single collection by slug
889
+ * Returns null if collection is not found (404)
890
+ */
891
+ async get(slug) {
892
+ try {
893
+ const endpoint = `/api/publish/${this.workspaceSlug}/collections/${slug}`;
894
+ const data = await this.http.fetch(endpoint);
895
+ return new Collection(data);
896
+ } catch (error) {
897
+ if (error instanceof MarvinNotFoundError) {
898
+ return null;
899
+ }
900
+ throw error;
901
+ }
902
+ }
903
+ /**
904
+ * Get entries in a collection
905
+ * Returns empty array if collection is not found
906
+ */
907
+ async entries(slug) {
908
+ const collection = await this.get(slug);
909
+ if (!collection) {
910
+ return [];
911
+ }
912
+ return collection.entries;
913
+ }
914
+ /**
915
+ * Try multiple collection slugs in fallback order
916
+ * Returns the first collection that exists and has entries
917
+ */
918
+ async fallback(slugs, options = {}) {
919
+ const { requireEntries = true } = options;
920
+ for (const slug of slugs) {
921
+ try {
922
+ const collection = await this.get(slug);
923
+ if (!collection) {
924
+ continue;
925
+ }
926
+ const entries = collection.entries;
927
+ if (!requireEntries || entries.length > 0) {
928
+ return entries;
929
+ }
930
+ } catch (error) {
931
+ continue;
932
+ }
933
+ }
934
+ return [];
935
+ }
936
+ };
937
+
938
+ // src/assets/assets.ts
939
+ var AssetsModule = class {
940
+ constructor(http, workspaceSlug) {
941
+ this.http = http;
942
+ this.workspaceSlug = workspaceSlug;
943
+ }
944
+ http;
945
+ workspaceSlug;
946
+ /**
947
+ * Get all published assets
948
+ */
949
+ async list(options = {}) {
950
+ const queryString = this.http.buildQueryString({
951
+ type: options.type,
952
+ limit: options.limit,
953
+ offset: options.offset
954
+ });
955
+ const endpoint = `/api/publish/${this.workspaceSlug}/assets${queryString}`;
956
+ const response = await this.http.fetch(endpoint);
957
+ return response.data || [];
958
+ }
959
+ /**
960
+ * Get a single published asset by slug or ID
961
+ */
962
+ async get(slugOrId) {
963
+ const endpoint = `/api/publish/${this.workspaceSlug}/assets/${slugOrId}`;
964
+ return this.http.fetch(endpoint);
965
+ }
966
+ /**
967
+ * Convenience: Get all images
968
+ */
969
+ async images(options) {
970
+ return this.list({ ...options, type: "image" });
971
+ }
972
+ /**
973
+ * Convenience: Get all videos
974
+ */
975
+ async videos(options) {
976
+ return this.list({ ...options, type: "video" });
977
+ }
978
+ /**
979
+ * Convenience: Get all documents
980
+ */
981
+ async documents(options) {
982
+ return this.list({ ...options, type: "document" });
983
+ }
984
+ };
985
+
986
+ // src/resources/resource.ts
987
+ var Resource = class {
988
+ constructor(data) {
989
+ this.data = data;
990
+ }
991
+ data;
992
+ get name() {
993
+ return this.data.name;
994
+ }
995
+ get slug() {
996
+ return this.data.slug;
997
+ }
998
+ get resourceType() {
999
+ return this.data.resourceType;
1000
+ }
1001
+ get description() {
1002
+ return this.data.description;
1003
+ }
1004
+ get externalId() {
1005
+ return this.data.externalId;
1006
+ }
1007
+ get url() {
1008
+ return this.data.url;
1009
+ }
1010
+ get metadataJson() {
1011
+ return this.data.metadataJson;
1012
+ }
1013
+ get entries() {
1014
+ return this.data.entries;
1015
+ }
1016
+ toJSON() {
1017
+ return this.data;
1018
+ }
1019
+ };
1020
+
1021
+ // src/resources/resources.ts
1022
+ var ResourcesModule = class {
1023
+ constructor(http, workspaceSlug) {
1024
+ this.http = http;
1025
+ this.workspaceSlug = workspaceSlug;
1026
+ }
1027
+ http;
1028
+ workspaceSlug;
1029
+ /**
1030
+ * Get all published resources
1031
+ */
1032
+ async list(options = {}) {
1033
+ const queryString = this.http.buildQueryString({
1034
+ resource_type: options.resourceType,
1035
+ limit: options.limit,
1036
+ offset: options.offset
1037
+ });
1038
+ const endpoint = `/api/publish/${this.workspaceSlug}/resources${queryString}`;
1039
+ const response = await this.http.fetch(endpoint);
1040
+ return response.data || [];
1041
+ }
1042
+ /**
1043
+ * Get a single resource by slug
1044
+ */
1045
+ async get(slug) {
1046
+ const endpoint = `/api/publish/${this.workspaceSlug}/resources/${slug}`;
1047
+ const data = await this.http.fetch(endpoint);
1048
+ return new Resource(data);
1049
+ }
1050
+ /**
1051
+ * Get entry slugs that reference this resource
1052
+ */
1053
+ async entries(slug) {
1054
+ const resource = await this.get(slug);
1055
+ return resource.entries;
1056
+ }
1057
+ };
1058
+
1059
+ // src/renderers/renderers.ts
1060
+ var RenderersModule = class {
1061
+ constructor(http, workspaceSlug) {
1062
+ this.http = http;
1063
+ this.workspaceSlug = workspaceSlug;
1064
+ }
1065
+ http;
1066
+ workspaceSlug;
1067
+ async list() {
1068
+ const endpoint = `/api/publish/${this.workspaceSlug}/entry-types`;
1069
+ return this.http.fetch(endpoint);
1070
+ }
1071
+ };
1072
+
1073
+ // src/workspaces/workspace.ts
1074
+ var Workspace = class {
1075
+ constructor(http, slug) {
1076
+ this.http = http;
1077
+ this._slug = slug;
1078
+ this.entries = new EntriesModule(http, slug);
1079
+ this.collections = new CollectionsModule(http, slug);
1080
+ this.assets = new AssetsModule(http, slug);
1081
+ this.resources = new ResourcesModule(http, slug);
1082
+ this.renderers = new RenderersModule(http, slug);
1083
+ }
1084
+ http;
1085
+ _site = null;
1086
+ _slug;
1087
+ entries;
1088
+ collections;
1089
+ assets;
1090
+ resources;
1091
+ renderers;
1092
+ /**
1093
+ * Get workspace slug
1094
+ */
1095
+ get slug() {
1096
+ return this._slug;
1097
+ }
1098
+ /**
1099
+ * Get site configuration (cached after first load)
1100
+ */
1101
+ get site() {
1102
+ return this._site;
1103
+ }
1104
+ /**
1105
+ * Load site configuration
1106
+ */
1107
+ async loadSite() {
1108
+ const endpoint = `/api/publish/${this._slug}/site`;
1109
+ this._site = await this.http.fetch(endpoint);
1110
+ return this._site;
1111
+ }
1112
+ /**
1113
+ * Get workspace info (name and slug only)
1114
+ */
1115
+ async getInfo() {
1116
+ const endpoint = `/api/publish/${this._slug}`;
1117
+ return this.http.fetch(endpoint);
1118
+ }
1119
+ };
1120
+
1121
+ // src/client/marvin.ts
1122
+ var MarvinClient = class {
1123
+ http;
1124
+ cache;
1125
+ workspace;
1126
+ initialized = false;
1127
+ constructor(config) {
1128
+ validateConfig(config);
1129
+ this.http = new MarvinHttpClient(config);
1130
+ this.cache = new MarvinCache(config.cacheDuration);
1131
+ this.workspace = new Workspace(this.http, config.workspaceSlug);
1132
+ if (config.autoInitialize) {
1133
+ this.initialize();
1134
+ }
1135
+ }
1136
+ /**
1137
+ * Initialize the SDK
1138
+ * Preloads site configuration and metadata
1139
+ */
1140
+ async initialize() {
1141
+ if (this.initialized) return;
1142
+ try {
1143
+ await this.workspace.loadSite();
1144
+ this.initialized = true;
1145
+ } catch (error) {
1146
+ console.warn("Marvin SDK initialization warning:", error);
1147
+ }
1148
+ }
1149
+ // ======================
1150
+ // Workspace-First API
1151
+ // ======================
1152
+ /**
1153
+ * Get the workspace object
1154
+ */
1155
+ async getWorkspace() {
1156
+ if (!this.initialized) {
1157
+ await this.initialize();
1158
+ }
1159
+ return this.workspace;
1160
+ }
1161
+ /**
1162
+ * Get site configuration (cached)
1163
+ */
1164
+ get site() {
1165
+ return this.workspace.site;
1166
+ }
1167
+ /**
1168
+ * Get workspace info (name and slug)
1169
+ */
1170
+ async getWorkspaceInfo() {
1171
+ return this.workspace.getInfo();
1172
+ }
1173
+ /**
1174
+ * Get entries module
1175
+ */
1176
+ get entries() {
1177
+ return this.workspace.entries;
1178
+ }
1179
+ /**
1180
+ * Get collections module
1181
+ */
1182
+ get collections() {
1183
+ return this.workspace.collections;
1184
+ }
1185
+ /**
1186
+ * Get assets module
1187
+ */
1188
+ get assets() {
1189
+ return this.workspace.assets;
1190
+ }
1191
+ /**
1192
+ * Get resources module
1193
+ */
1194
+ get resources() {
1195
+ return this.workspace.resources;
1196
+ }
1197
+ /**
1198
+ * Get renderers module
1199
+ */
1200
+ get renderers() {
1201
+ return this.workspace.renderers;
1202
+ }
1203
+ // ======================
1204
+ // Convenience Methods
1205
+ // ======================
1206
+ /**
1207
+ * Get a single entry by slug
1208
+ */
1209
+ async entry(slug) {
1210
+ return this.workspace.entries.get(slug);
1211
+ }
1212
+ /**
1213
+ * Get a single collection by slug
1214
+ */
1215
+ async collection(slug) {
1216
+ return this.workspace.collections.get(slug);
1217
+ }
1218
+ /**
1219
+ * Get all pages
1220
+ */
1221
+ async pages(options) {
1222
+ return this.workspace.entries.pages(options);
1223
+ }
1224
+ /**
1225
+ * Get all blog posts
1226
+ */
1227
+ async posts(options) {
1228
+ return this.workspace.entries.posts(options);
1229
+ }
1230
+ /**
1231
+ * Get all projects
1232
+ */
1233
+ async projects(options) {
1234
+ return this.workspace.entries.projects(options);
1235
+ }
1236
+ /**
1237
+ * Get a single resource by slug
1238
+ */
1239
+ async resource(slug) {
1240
+ return this.workspace.resources.get(slug);
1241
+ }
1242
+ /**
1243
+ * Try multiple collection slugs in fallback order
1244
+ * Returns entries from the first collection that exists and has content
1245
+ */
1246
+ async collectionFallback(slugs, options) {
1247
+ return this.workspace.collections.fallback(slugs, options);
1248
+ }
1249
+ // ======================
1250
+ // Backwards-Compatible Publishing API
1251
+ // ======================
1252
+ /**
1253
+ * @deprecated Use workspace.site or marvin.site instead
1254
+ */
1255
+ async getSite() {
1256
+ return this.workspace.loadSite();
1257
+ }
1258
+ /**
1259
+ * @deprecated Use workspace.entries.list() instead
1260
+ */
1261
+ async getEntries(options) {
1262
+ return this.workspace.entries.list(options);
1263
+ }
1264
+ /**
1265
+ * @deprecated Use workspace.entries.get() instead
1266
+ */
1267
+ async getEntry(slug) {
1268
+ const entry = await this.workspace.entries.get(slug);
1269
+ if (!entry) {
1270
+ return null;
1271
+ }
1272
+ return entry.toJSON();
1273
+ }
1274
+ /**
1275
+ * @deprecated Use workspace.collections.list() instead
1276
+ */
1277
+ async getCollections() {
1278
+ return this.workspace.collections.list();
1279
+ }
1280
+ /**
1281
+ * @deprecated Use workspace.collections.get() instead
1282
+ */
1283
+ async getCollection(slug) {
1284
+ const collection = await this.workspace.collections.get(slug);
1285
+ if (!collection) {
1286
+ return null;
1287
+ }
1288
+ return collection.toJSON();
1289
+ }
1290
+ /**
1291
+ * @deprecated Use workspace.collections.entries() instead
1292
+ */
1293
+ async getCollectionEntries(slug) {
1294
+ return this.workspace.collections.entries(slug);
1295
+ }
1296
+ /**
1297
+ * @deprecated Use workspace.assets.list() instead
1298
+ */
1299
+ async getAssets(options) {
1300
+ return this.workspace.assets.list(options);
1301
+ }
1302
+ /**
1303
+ * @deprecated Use workspace.resources.list() instead
1304
+ */
1305
+ async getResources(options) {
1306
+ return this.workspace.resources.list(options);
1307
+ }
1308
+ /**
1309
+ * @deprecated Use workspace.resources.get() instead
1310
+ */
1311
+ async getResource(slug) {
1312
+ const resource = await this.workspace.resources.get(slug);
1313
+ return resource.toJSON();
1314
+ }
1315
+ /**
1316
+ * @deprecated Use workspace.resources.entries() instead
1317
+ */
1318
+ async getResourceEntries(slug) {
1319
+ return this.workspace.resources.entries(slug);
1320
+ }
1321
+ };
1322
+
1323
+ // src/publish.ts
1324
+ init_config();
1325
+ function createMarvinClient(config) {
1326
+ const { createConfigFromEnv: createConfigFromEnv2 } = (init_config(), __toCommonJS(config_exports));
1327
+ const finalConfig = createConfigFromEnv2(config);
1328
+ return new MarvinClient(finalConfig);
1329
+ }
1330
+ // Annotate the CommonJS export names for ESM import in node:
1331
+ 0 && (module.exports = {
1332
+ BearerTokenAuth,
1333
+ Collection,
1334
+ Entry,
1335
+ HttpClient,
1336
+ MarvinApiError,
1337
+ MarvinAuthError,
1338
+ MarvinCache,
1339
+ MarvinClient,
1340
+ MarvinConfigError,
1341
+ MarvinError,
1342
+ MarvinNetworkError,
1343
+ MarvinNotFoundError,
1344
+ MarvinServerError,
1345
+ MarvinValidationError,
1346
+ Resource,
1347
+ Workspace,
1348
+ createConfigFromEnv,
1349
+ createMarvinClient,
1350
+ createPaginatedResponse,
1351
+ extractData,
1352
+ isPaginatedResponse,
1353
+ offsetToPage,
1354
+ pageToOffset
1355
+ });