@inneropen/marvin-sdk 2.0.0-next.2 → 2.0.0-next.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -11
- package/dist/{HttpClient-CLtgQGXH.d.cts → HttpClient-CdF3IHM1.d.cts} +49 -2
- package/dist/{HttpClient-CLtgQGXH.d.ts → HttpClient-CdF3IHM1.d.ts} +49 -2
- package/dist/chunk-TZ2TZFWP.js +1009 -0
- package/dist/chunk-WJC4X7TB.js +738 -0
- package/dist/index.cjs +565 -25
- package/dist/index.d.cts +20 -544
- package/dist/index.d.ts +20 -544
- package/dist/index.js +27 -791
- package/dist/marvin-BAslemIz.d.cts +666 -0
- package/dist/marvin-BmbMUJXh.d.ts +666 -0
- package/dist/platform.cjs +681 -42
- package/dist/platform.d.cts +968 -94
- package/dist/platform.d.ts +968 -94
- package/dist/platform.js +449 -32
- package/dist/publish.cjs +1509 -0
- package/dist/publish.d.cts +49 -0
- package/dist/publish.d.ts +49 -0
- package/dist/publish.js +75 -0
- package/dist/types/index.d.cts +46 -1
- package/dist/types/index.d.ts +46 -1
- package/package.json +18 -7
- package/dist/chunk-25F3IF6S.js +0 -396
package/dist/index.cjs
CHANGED
|
@@ -27,14 +27,99 @@ __export(config_exports, {
|
|
|
27
27
|
validateConfig: () => validateConfig
|
|
28
28
|
});
|
|
29
29
|
function validateConfig(config) {
|
|
30
|
+
const receivedKeys = Object.keys(config).join(", ");
|
|
30
31
|
if (!config.apiUrl) {
|
|
31
|
-
throw new Error(
|
|
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
|
+
);
|
|
32
47
|
}
|
|
33
48
|
if (!config.siteClientToken) {
|
|
34
|
-
throw new Error(
|
|
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
|
+
);
|
|
35
69
|
}
|
|
36
70
|
if (!config.workspaceSlug) {
|
|
37
|
-
throw new Error(
|
|
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
|
+
}
|
|
38
123
|
}
|
|
39
124
|
}
|
|
40
125
|
function createConfigFromEnv(overrides) {
|
|
@@ -43,8 +128,10 @@ function createConfigFromEnv(overrides) {
|
|
|
43
128
|
siteClientToken: overrides?.siteClientToken || process.env.MARVIN_SITE_CLIENT_TOKEN || "",
|
|
44
129
|
workspaceSlug: overrides?.workspaceSlug || process.env.MARVIN_WORKSPACE_SLUG || "",
|
|
45
130
|
autoInitialize: overrides?.autoInitialize ?? false,
|
|
46
|
-
cacheDuration: overrides?.cacheDuration ?? 5 * 60 * 1e3
|
|
131
|
+
cacheDuration: overrides?.cacheDuration ?? 5 * 60 * 1e3,
|
|
47
132
|
// 5 minutes
|
|
133
|
+
debug: overrides?.debug ?? process.env.MARVIN_DEBUG === "true",
|
|
134
|
+
logger: overrides?.logger ?? console
|
|
48
135
|
};
|
|
49
136
|
}
|
|
50
137
|
var init_config = __esm({
|
|
@@ -54,7 +141,7 @@ var init_config = __esm({
|
|
|
54
141
|
});
|
|
55
142
|
|
|
56
143
|
// src/core/errors/MarvinError.ts
|
|
57
|
-
var MarvinError, MarvinApiError, MarvinAuthError, MarvinConfigError, MarvinValidationError;
|
|
144
|
+
var MarvinError, MarvinApiError, MarvinAuthError, MarvinConfigError, MarvinValidationError, MarvinNotFoundError, MarvinNetworkError, MarvinServerError;
|
|
58
145
|
var init_MarvinError = __esm({
|
|
59
146
|
"src/core/errors/MarvinError.ts"() {
|
|
60
147
|
"use strict";
|
|
@@ -110,6 +197,33 @@ ${body}` : ""}`;
|
|
|
110
197
|
}
|
|
111
198
|
field;
|
|
112
199
|
};
|
|
200
|
+
MarvinNotFoundError = class _MarvinNotFoundError extends MarvinError {
|
|
201
|
+
constructor(message, endpoint) {
|
|
202
|
+
super(message, "NOT_FOUND", 404);
|
|
203
|
+
this.endpoint = endpoint;
|
|
204
|
+
this.name = "MarvinNotFoundError";
|
|
205
|
+
Object.setPrototypeOf(this, _MarvinNotFoundError.prototype);
|
|
206
|
+
}
|
|
207
|
+
endpoint;
|
|
208
|
+
};
|
|
209
|
+
MarvinNetworkError = class _MarvinNetworkError extends MarvinError {
|
|
210
|
+
constructor(message, originalError) {
|
|
211
|
+
super(message, "NETWORK_ERROR");
|
|
212
|
+
this.originalError = originalError;
|
|
213
|
+
this.name = "MarvinNetworkError";
|
|
214
|
+
Object.setPrototypeOf(this, _MarvinNetworkError.prototype);
|
|
215
|
+
}
|
|
216
|
+
originalError;
|
|
217
|
+
};
|
|
218
|
+
MarvinServerError = class _MarvinServerError extends MarvinError {
|
|
219
|
+
constructor(message, statusCode, endpoint) {
|
|
220
|
+
super(message, "SERVER_ERROR", statusCode);
|
|
221
|
+
this.endpoint = endpoint;
|
|
222
|
+
this.name = "MarvinServerError";
|
|
223
|
+
Object.setPrototypeOf(this, _MarvinServerError.prototype);
|
|
224
|
+
}
|
|
225
|
+
endpoint;
|
|
226
|
+
};
|
|
113
227
|
}
|
|
114
228
|
});
|
|
115
229
|
|
|
@@ -133,12 +247,47 @@ var init_HttpClient = __esm({
|
|
|
133
247
|
defaultHeaders;
|
|
134
248
|
credentials;
|
|
135
249
|
timeout;
|
|
250
|
+
debug;
|
|
251
|
+
retryConfig;
|
|
252
|
+
logger;
|
|
136
253
|
constructor(config) {
|
|
137
254
|
this.baseUrl = config.baseUrl;
|
|
138
255
|
this.auth = config.auth;
|
|
139
256
|
this.defaultHeaders = config.defaultHeaders || {};
|
|
140
257
|
this.credentials = config.credentials || "same-origin";
|
|
141
|
-
|
|
258
|
+
const MAX_TIMEOUT = 12e4;
|
|
259
|
+
this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
|
|
260
|
+
this.debug = config.debug ?? false;
|
|
261
|
+
this.logger = config.logger ?? console;
|
|
262
|
+
this.retryConfig = {
|
|
263
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
264
|
+
initialDelay: config.retry?.initialDelay ?? 1e3,
|
|
265
|
+
maxDelay: config.retry?.maxDelay ?? 1e4,
|
|
266
|
+
retryableStatuses: config.retry?.retryableStatuses ?? [408, 429, 500, 502, 503, 504]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Validate path parameter to prevent path traversal attacks
|
|
271
|
+
*/
|
|
272
|
+
validatePathParam(param, paramName = "parameter") {
|
|
273
|
+
if (typeof param === "number") {
|
|
274
|
+
return String(param);
|
|
275
|
+
}
|
|
276
|
+
if (!param || typeof param !== "string") {
|
|
277
|
+
throw new MarvinValidationError(`Invalid ${paramName}: must be a non-empty string or number`);
|
|
278
|
+
}
|
|
279
|
+
if (param.includes("..") || param.includes("/") || param.includes("\\")) {
|
|
280
|
+
throw new MarvinValidationError(
|
|
281
|
+
`Invalid ${paramName}: cannot contain path traversal characters (.. / \\)`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (param.includes("\0")) {
|
|
285
|
+
throw new MarvinValidationError(`Invalid ${paramName}: cannot contain null bytes`);
|
|
286
|
+
}
|
|
287
|
+
if (param.length > 255) {
|
|
288
|
+
throw new MarvinValidationError(`Invalid ${paramName}: exceeds maximum length of 255 characters`);
|
|
289
|
+
}
|
|
290
|
+
return param;
|
|
142
291
|
}
|
|
143
292
|
/**
|
|
144
293
|
* Build full URL from endpoint
|
|
@@ -172,14 +321,78 @@ var init_HttpClient = __esm({
|
|
|
172
321
|
return filtered.length > 0 ? `?${filtered.join("&")}` : "";
|
|
173
322
|
}
|
|
174
323
|
/**
|
|
175
|
-
*
|
|
324
|
+
* Sanitize data for logging by redacting sensitive fields
|
|
176
325
|
*/
|
|
177
|
-
|
|
326
|
+
sanitizeForLogging(data) {
|
|
327
|
+
if (!data || typeof data !== "object") {
|
|
328
|
+
return data;
|
|
329
|
+
}
|
|
330
|
+
const sensitiveFields = [
|
|
331
|
+
"token",
|
|
332
|
+
"access_token",
|
|
333
|
+
"refresh_token",
|
|
334
|
+
"password",
|
|
335
|
+
"secret",
|
|
336
|
+
"apiKey",
|
|
337
|
+
"api_key",
|
|
338
|
+
"bearer",
|
|
339
|
+
"authorization",
|
|
340
|
+
"csrf_token",
|
|
341
|
+
"session",
|
|
342
|
+
"cookie"
|
|
343
|
+
];
|
|
344
|
+
const sanitize = (obj) => {
|
|
345
|
+
const sanitized = {};
|
|
346
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
347
|
+
const keyLower = key.toLowerCase();
|
|
348
|
+
const isSensitive = sensitiveFields.some((field) => keyLower.includes(field));
|
|
349
|
+
if (isSensitive) {
|
|
350
|
+
sanitized[key] = "[REDACTED]";
|
|
351
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
352
|
+
sanitized[key] = sanitize(value);
|
|
353
|
+
} else if (Array.isArray(value)) {
|
|
354
|
+
sanitized[key] = value.map(
|
|
355
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
sanitized[key] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return sanitized;
|
|
362
|
+
};
|
|
363
|
+
if (Array.isArray(data)) {
|
|
364
|
+
return data.map(
|
|
365
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return sanitize(data);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Calculate delay for retry with exponential backoff
|
|
372
|
+
*/
|
|
373
|
+
calculateRetryDelay(attempt) {
|
|
374
|
+
const delay = this.retryConfig.initialDelay * Math.pow(2, attempt);
|
|
375
|
+
return Math.min(delay, this.retryConfig.maxDelay);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Sleep for specified milliseconds
|
|
379
|
+
*/
|
|
380
|
+
sleep(ms) {
|
|
381
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Perform HTTP request with automatic retry on transient failures
|
|
385
|
+
*/
|
|
386
|
+
async request(method, endpoint, options, retryAttempt = 0) {
|
|
178
387
|
const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
|
|
179
388
|
const isFormData = options?.body instanceof FormData;
|
|
180
389
|
const headers = this.buildHeaders(options?.headers, isFormData);
|
|
181
390
|
const controller = new AbortController();
|
|
182
391
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
392
|
+
const startTime = Date.now();
|
|
393
|
+
if (this.debug) {
|
|
394
|
+
this.logger.log(`[Marvin] ${method} ${url}`);
|
|
395
|
+
}
|
|
183
396
|
try {
|
|
184
397
|
const response = await fetch(url, {
|
|
185
398
|
method,
|
|
@@ -189,11 +402,54 @@ var init_HttpClient = __esm({
|
|
|
189
402
|
signal: controller.signal
|
|
190
403
|
});
|
|
191
404
|
clearTimeout(timeoutId);
|
|
405
|
+
const duration = Date.now() - startTime;
|
|
406
|
+
if (this.debug) {
|
|
407
|
+
this.logger.log(
|
|
408
|
+
`[Marvin] ${response.status} ${response.statusText} (${duration}ms)`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
192
411
|
if (response.status === 401 && this.auth.handleUnauthorized) {
|
|
193
412
|
await this.auth.handleUnauthorized();
|
|
194
413
|
}
|
|
195
414
|
if (!response.ok) {
|
|
196
415
|
const errorText = await response.text().catch(() => "");
|
|
416
|
+
if (this.debug) {
|
|
417
|
+
this.logger.error(
|
|
418
|
+
`[Marvin] Error response: ${response.status} ${response.statusText}`,
|
|
419
|
+
errorText ? `Body: ${errorText}` : ""
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const isRetryable = this.retryConfig.retryableStatuses.includes(response.status);
|
|
423
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
424
|
+
if (isRetryable && canRetry) {
|
|
425
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
426
|
+
if (this.debug) {
|
|
427
|
+
this.logger.warn(
|
|
428
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (status: ${response.status})`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
await this.sleep(delay);
|
|
432
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
433
|
+
}
|
|
434
|
+
if (response.status === 404) {
|
|
435
|
+
throw new MarvinNotFoundError(
|
|
436
|
+
`Resource not found: ${endpoint}`,
|
|
437
|
+
endpoint
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (response.status === 401 || response.status === 403) {
|
|
441
|
+
throw new MarvinAuthError(
|
|
442
|
+
`Authentication failed: ${response.statusText}`,
|
|
443
|
+
response.status
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (response.status >= 500) {
|
|
447
|
+
throw new MarvinServerError(
|
|
448
|
+
`Server error: ${response.status} ${response.statusText}`,
|
|
449
|
+
response.status,
|
|
450
|
+
endpoint
|
|
451
|
+
);
|
|
452
|
+
}
|
|
197
453
|
throw MarvinApiError.fromResponse(
|
|
198
454
|
response.status,
|
|
199
455
|
response.statusText,
|
|
@@ -205,21 +461,39 @@ var init_HttpClient = __esm({
|
|
|
205
461
|
if (!contentType || !contentType.includes("application/json")) {
|
|
206
462
|
return void 0;
|
|
207
463
|
}
|
|
208
|
-
|
|
464
|
+
const data = await response.json();
|
|
465
|
+
if (this.debug) {
|
|
466
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
467
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
468
|
+
}
|
|
469
|
+
return data;
|
|
209
470
|
} catch (error) {
|
|
210
471
|
clearTimeout(timeoutId);
|
|
211
|
-
if (error instanceof MarvinApiError) {
|
|
472
|
+
if (error instanceof MarvinApiError || error instanceof MarvinNotFoundError || error instanceof MarvinAuthError || error instanceof MarvinServerError) {
|
|
212
473
|
throw error;
|
|
213
474
|
}
|
|
214
475
|
if (error instanceof Error) {
|
|
476
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
477
|
+
if (canRetry && error.name !== "AbortError") {
|
|
478
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
479
|
+
if (this.debug) {
|
|
480
|
+
this.logger.warn(
|
|
481
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (network error)`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
await this.sleep(delay);
|
|
485
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
486
|
+
}
|
|
215
487
|
if (error.name === "AbortError") {
|
|
216
|
-
throw new
|
|
488
|
+
throw new MarvinNetworkError(
|
|
217
489
|
`Request timeout after ${this.timeout}ms`,
|
|
218
|
-
|
|
219
|
-
endpoint
|
|
490
|
+
error
|
|
220
491
|
);
|
|
221
492
|
}
|
|
222
|
-
throw new
|
|
493
|
+
throw new MarvinNetworkError(
|
|
494
|
+
`Network error: ${error.message}`,
|
|
495
|
+
error
|
|
496
|
+
);
|
|
223
497
|
}
|
|
224
498
|
throw error;
|
|
225
499
|
}
|
|
@@ -290,10 +564,27 @@ var init_AuthStrategy = __esm({
|
|
|
290
564
|
}
|
|
291
565
|
sessionToken;
|
|
292
566
|
onUnauthorized;
|
|
567
|
+
csrfToken;
|
|
568
|
+
/**
|
|
569
|
+
* Set CSRF token for cross-site request forgery protection
|
|
570
|
+
* The token should be obtained from the server after login
|
|
571
|
+
*/
|
|
572
|
+
setCsrfToken(token) {
|
|
573
|
+
this.csrfToken = token;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get current CSRF token
|
|
577
|
+
*/
|
|
578
|
+
getCsrfToken() {
|
|
579
|
+
return this.csrfToken;
|
|
580
|
+
}
|
|
293
581
|
injectAuth(headers) {
|
|
294
582
|
if (this.sessionToken) {
|
|
295
583
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
296
584
|
}
|
|
585
|
+
if (this.csrfToken) {
|
|
586
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
587
|
+
}
|
|
297
588
|
}
|
|
298
589
|
async handleUnauthorized() {
|
|
299
590
|
if (this.onUnauthorized) {
|
|
@@ -301,7 +592,9 @@ var init_AuthStrategy = __esm({
|
|
|
301
592
|
}
|
|
302
593
|
}
|
|
303
594
|
getDescription() {
|
|
304
|
-
|
|
595
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
596
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
597
|
+
return auth + csrf;
|
|
305
598
|
}
|
|
306
599
|
};
|
|
307
600
|
NoAuth = class {
|
|
@@ -361,11 +654,17 @@ var init_cache = __esm({
|
|
|
361
654
|
"src/core/cache.ts"() {
|
|
362
655
|
"use strict";
|
|
363
656
|
MarvinCache = class {
|
|
364
|
-
constructor(duration = 5 * 60 * 1e3) {
|
|
657
|
+
constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
|
|
365
658
|
this.duration = duration;
|
|
659
|
+
this.maxEntries = maxEntries;
|
|
366
660
|
}
|
|
367
661
|
duration;
|
|
662
|
+
maxEntries;
|
|
368
663
|
cache = /* @__PURE__ */ new Map();
|
|
664
|
+
/**
|
|
665
|
+
* Get cached data by key
|
|
666
|
+
* Returns undefined if not found or expired
|
|
667
|
+
*/
|
|
369
668
|
get(key) {
|
|
370
669
|
const entry = this.cache.get(key);
|
|
371
670
|
if (!entry) return void 0;
|
|
@@ -376,25 +675,149 @@ var init_cache = __esm({
|
|
|
376
675
|
}
|
|
377
676
|
return entry.data;
|
|
378
677
|
}
|
|
379
|
-
|
|
678
|
+
/**
|
|
679
|
+
* Set cached data with optional tags for invalidation
|
|
680
|
+
*/
|
|
681
|
+
set(key, data, tags) {
|
|
682
|
+
if (this.cache.size >= this.maxEntries) {
|
|
683
|
+
const oldestKey = this.cache.keys().next().value;
|
|
684
|
+
if (oldestKey) {
|
|
685
|
+
this.cache.delete(oldestKey);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
380
688
|
this.cache.set(key, {
|
|
381
689
|
data,
|
|
382
|
-
timestamp: Date.now()
|
|
690
|
+
timestamp: Date.now(),
|
|
691
|
+
tags
|
|
383
692
|
});
|
|
384
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Check if key exists and is not expired
|
|
696
|
+
*/
|
|
385
697
|
has(key) {
|
|
386
698
|
return this.get(key) !== void 0;
|
|
387
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Clear all cached entries
|
|
702
|
+
*/
|
|
388
703
|
clear() {
|
|
389
704
|
this.cache.clear();
|
|
390
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Delete a specific cache entry
|
|
708
|
+
*/
|
|
391
709
|
delete(key) {
|
|
392
710
|
this.cache.delete(key);
|
|
393
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Invalidate all cache entries with a specific tag
|
|
714
|
+
* Useful for invalidating related data after mutations
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```typescript
|
|
718
|
+
* // Cache entries with tags
|
|
719
|
+
* cache.set('entry:1', data, ['entries', 'entry:1']);
|
|
720
|
+
* cache.set('entries:list', data, ['entries']);
|
|
721
|
+
*
|
|
722
|
+
* // Invalidate all entries
|
|
723
|
+
* cache.invalidateByTag('entries');
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
invalidateByTag(tag) {
|
|
727
|
+
let count = 0;
|
|
728
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
729
|
+
if (entry.tags?.includes(tag)) {
|
|
730
|
+
this.cache.delete(key);
|
|
731
|
+
count++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return count;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Get cache statistics
|
|
738
|
+
*/
|
|
739
|
+
getStats() {
|
|
740
|
+
let oldest = null;
|
|
741
|
+
let newest = null;
|
|
742
|
+
for (const entry of this.cache.values()) {
|
|
743
|
+
if (oldest === null || entry.timestamp < oldest) {
|
|
744
|
+
oldest = entry.timestamp;
|
|
745
|
+
}
|
|
746
|
+
if (newest === null || entry.timestamp > newest) {
|
|
747
|
+
newest = entry.timestamp;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
size: this.cache.size,
|
|
752
|
+
maxEntries: this.maxEntries,
|
|
753
|
+
oldestEntry: oldest,
|
|
754
|
+
newestEntry: newest
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Remove expired entries (garbage collection)
|
|
759
|
+
* Returns number of entries removed
|
|
760
|
+
*/
|
|
761
|
+
prune() {
|
|
762
|
+
let count = 0;
|
|
763
|
+
const now = Date.now();
|
|
764
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
765
|
+
const age = now - entry.timestamp;
|
|
766
|
+
if (age > this.duration) {
|
|
767
|
+
this.cache.delete(key);
|
|
768
|
+
count++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return count;
|
|
772
|
+
}
|
|
394
773
|
};
|
|
395
774
|
}
|
|
396
775
|
});
|
|
397
776
|
|
|
777
|
+
// src/core/validation.ts
|
|
778
|
+
function validateEmail(email, fieldName = "Email") {
|
|
779
|
+
if (!email || typeof email !== "string") {
|
|
780
|
+
throw new MarvinValidationError(`${fieldName} is required and must be a string`);
|
|
781
|
+
}
|
|
782
|
+
const trimmed = email.trim();
|
|
783
|
+
if (trimmed.length === 0) {
|
|
784
|
+
throw new MarvinValidationError(`${fieldName} cannot be empty`);
|
|
785
|
+
}
|
|
786
|
+
if (trimmed.length < 5) {
|
|
787
|
+
throw new MarvinValidationError(`${fieldName} is too short`);
|
|
788
|
+
}
|
|
789
|
+
if (trimmed.length > 254) {
|
|
790
|
+
throw new MarvinValidationError(`${fieldName} exceeds maximum length of 254 characters`);
|
|
791
|
+
}
|
|
792
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
793
|
+
throw new MarvinValidationError(
|
|
794
|
+
`${fieldName} format is invalid. Expected format: user@example.com`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const [localPart, domain] = trimmed.split("@");
|
|
798
|
+
if (localPart.length > 64) {
|
|
799
|
+
throw new MarvinValidationError(`${fieldName} local part exceeds 64 characters`);
|
|
800
|
+
}
|
|
801
|
+
if (domain.length > 253) {
|
|
802
|
+
throw new MarvinValidationError(`${fieldName} domain exceeds 253 characters`);
|
|
803
|
+
}
|
|
804
|
+
if (trimmed.includes("..")) {
|
|
805
|
+
throw new MarvinValidationError(`${fieldName} cannot contain consecutive dots`);
|
|
806
|
+
}
|
|
807
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
808
|
+
throw new MarvinValidationError(`${fieldName} local part cannot start or end with a dot`);
|
|
809
|
+
}
|
|
810
|
+
return trimmed;
|
|
811
|
+
}
|
|
812
|
+
var EMAIL_REGEX;
|
|
813
|
+
var init_validation = __esm({
|
|
814
|
+
"src/core/validation.ts"() {
|
|
815
|
+
"use strict";
|
|
816
|
+
init_errors();
|
|
817
|
+
EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
398
821
|
// src/core/index.ts
|
|
399
822
|
var init_core = __esm({
|
|
400
823
|
"src/core/index.ts"() {
|
|
@@ -404,6 +827,7 @@ var init_core = __esm({
|
|
|
404
827
|
init_errors();
|
|
405
828
|
init_pagination();
|
|
406
829
|
init_cache();
|
|
830
|
+
init_validation();
|
|
407
831
|
}
|
|
408
832
|
});
|
|
409
833
|
|
|
@@ -418,7 +842,9 @@ var init_http2 = __esm({
|
|
|
418
842
|
super({
|
|
419
843
|
baseUrl: config.apiUrl,
|
|
420
844
|
auth: new BearerTokenAuth(config.siteClientToken),
|
|
421
|
-
credentials: "same-origin"
|
|
845
|
+
credentials: "same-origin",
|
|
846
|
+
debug: config.debug,
|
|
847
|
+
logger: config.logger
|
|
422
848
|
});
|
|
423
849
|
}
|
|
424
850
|
/**
|
|
@@ -555,6 +981,7 @@ var init_entries = __esm({
|
|
|
555
981
|
"src/entries/entries.ts"() {
|
|
556
982
|
"use strict";
|
|
557
983
|
init_entry();
|
|
984
|
+
init_errors();
|
|
558
985
|
EntriesModule = class {
|
|
559
986
|
constructor(http, workspaceSlug) {
|
|
560
987
|
this.http = http;
|
|
@@ -579,11 +1006,19 @@ var init_entries = __esm({
|
|
|
579
1006
|
}
|
|
580
1007
|
/**
|
|
581
1008
|
* Get a single entry by slug
|
|
1009
|
+
* Returns null if entry is not found (404)
|
|
582
1010
|
*/
|
|
583
1011
|
async get(slug) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1012
|
+
try {
|
|
1013
|
+
const endpoint = `/api/publish/${this.workspaceSlug}/entries/${slug}`;
|
|
1014
|
+
const data = await this.http.fetch(endpoint);
|
|
1015
|
+
return new Entry(data, this.http, this.workspaceSlug);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (error instanceof MarvinNotFoundError) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
587
1022
|
}
|
|
588
1023
|
/**
|
|
589
1024
|
* Convenience: Get all pages
|
|
@@ -689,6 +1124,7 @@ var init_collections = __esm({
|
|
|
689
1124
|
"src/collections/collections.ts"() {
|
|
690
1125
|
"use strict";
|
|
691
1126
|
init_collection();
|
|
1127
|
+
init_errors();
|
|
692
1128
|
CollectionsModule = class {
|
|
693
1129
|
constructor(http, workspaceSlug) {
|
|
694
1130
|
this.http = http;
|
|
@@ -706,19 +1142,63 @@ var init_collections = __esm({
|
|
|
706
1142
|
}
|
|
707
1143
|
/**
|
|
708
1144
|
* Get a single collection by slug
|
|
1145
|
+
* Returns null if collection is not found (404)
|
|
709
1146
|
*/
|
|
710
1147
|
async get(slug) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1148
|
+
try {
|
|
1149
|
+
const endpoint = `/api/publish/${this.workspaceSlug}/collections/${slug}`;
|
|
1150
|
+
const data = await this.http.fetch(endpoint);
|
|
1151
|
+
return new Collection(data, this.http, this.workspaceSlug);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
if (error instanceof MarvinNotFoundError) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
714
1158
|
}
|
|
715
1159
|
/**
|
|
716
1160
|
* Get entries in a collection
|
|
1161
|
+
* Returns empty array if collection is not found
|
|
717
1162
|
*/
|
|
718
1163
|
async entries(slug) {
|
|
719
1164
|
const collection = await this.get(slug);
|
|
1165
|
+
if (!collection) {
|
|
1166
|
+
return [];
|
|
1167
|
+
}
|
|
720
1168
|
return collection.entries();
|
|
721
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Try multiple collection slugs in fallback order
|
|
1172
|
+
* Returns the first collection that exists and has entries
|
|
1173
|
+
*
|
|
1174
|
+
* @param slugs - Collection slugs to try in order
|
|
1175
|
+
* @param options - Options for fallback behavior
|
|
1176
|
+
* @returns Entries from the first matching collection, or empty array
|
|
1177
|
+
*
|
|
1178
|
+
* @example
|
|
1179
|
+
* ```ts
|
|
1180
|
+
* // Try bench-notes, then journal, then blog
|
|
1181
|
+
* const entries = await collections.fallback(['bench-notes', 'journal', 'blog']);
|
|
1182
|
+
* ```
|
|
1183
|
+
*/
|
|
1184
|
+
async fallback(slugs, options = {}) {
|
|
1185
|
+
const { requireEntries = true } = options;
|
|
1186
|
+
for (const slug of slugs) {
|
|
1187
|
+
try {
|
|
1188
|
+
const collection = await this.get(slug);
|
|
1189
|
+
if (!collection) {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const entries = await collection.entries();
|
|
1193
|
+
if (!requireEntries || entries.length > 0) {
|
|
1194
|
+
return entries;
|
|
1195
|
+
}
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return [];
|
|
1201
|
+
}
|
|
722
1202
|
};
|
|
723
1203
|
}
|
|
724
1204
|
});
|
|
@@ -890,6 +1370,26 @@ var init_resources = __esm({
|
|
|
890
1370
|
}
|
|
891
1371
|
});
|
|
892
1372
|
|
|
1373
|
+
// src/renderers/renderers.ts
|
|
1374
|
+
var RenderersModule;
|
|
1375
|
+
var init_renderers = __esm({
|
|
1376
|
+
"src/renderers/renderers.ts"() {
|
|
1377
|
+
"use strict";
|
|
1378
|
+
RenderersModule = class {
|
|
1379
|
+
constructor(http, workspaceSlug) {
|
|
1380
|
+
this.http = http;
|
|
1381
|
+
this.workspaceSlug = workspaceSlug;
|
|
1382
|
+
}
|
|
1383
|
+
http;
|
|
1384
|
+
workspaceSlug;
|
|
1385
|
+
async list() {
|
|
1386
|
+
const endpoint = `/api/publish/${this.workspaceSlug}/entry-types`;
|
|
1387
|
+
return this.http.fetch(endpoint);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
893
1393
|
// src/workspaces/workspace.ts
|
|
894
1394
|
var Workspace;
|
|
895
1395
|
var init_workspace = __esm({
|
|
@@ -899,6 +1399,7 @@ var init_workspace = __esm({
|
|
|
899
1399
|
init_collections();
|
|
900
1400
|
init_assets();
|
|
901
1401
|
init_resources();
|
|
1402
|
+
init_renderers();
|
|
902
1403
|
Workspace = class {
|
|
903
1404
|
constructor(http, slug) {
|
|
904
1405
|
this.http = http;
|
|
@@ -907,6 +1408,7 @@ var init_workspace = __esm({
|
|
|
907
1408
|
this.collections = new CollectionsModule(http, slug);
|
|
908
1409
|
this.assets = new AssetsModule(http, slug);
|
|
909
1410
|
this.resources = new ResourcesModule(http, slug);
|
|
1411
|
+
this.renderers = new RenderersModule(http, slug);
|
|
910
1412
|
}
|
|
911
1413
|
http;
|
|
912
1414
|
_site = null;
|
|
@@ -915,6 +1417,7 @@ var init_workspace = __esm({
|
|
|
915
1417
|
collections;
|
|
916
1418
|
assets;
|
|
917
1419
|
resources;
|
|
1420
|
+
renderers;
|
|
918
1421
|
/**
|
|
919
1422
|
* Get workspace slug
|
|
920
1423
|
*/
|
|
@@ -1042,6 +1545,12 @@ var init_marvin = __esm({
|
|
|
1042
1545
|
get resources() {
|
|
1043
1546
|
return this.workspace.resources;
|
|
1044
1547
|
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Get renderers module
|
|
1550
|
+
*/
|
|
1551
|
+
get renderers() {
|
|
1552
|
+
return this.workspace.renderers;
|
|
1553
|
+
}
|
|
1045
1554
|
// ======================
|
|
1046
1555
|
// Convenience Methods
|
|
1047
1556
|
// ======================
|
|
@@ -1081,6 +1590,23 @@ var init_marvin = __esm({
|
|
|
1081
1590
|
async resource(slug) {
|
|
1082
1591
|
return this.workspace.resources.get(slug);
|
|
1083
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Try multiple collection slugs in fallback order
|
|
1595
|
+
* Returns entries from the first collection that exists and has content
|
|
1596
|
+
*
|
|
1597
|
+
* @param slugs - Collection slugs to try in order
|
|
1598
|
+
* @param options - Options for fallback behavior
|
|
1599
|
+
* @returns Entries from the first matching collection, or empty array
|
|
1600
|
+
*
|
|
1601
|
+
* @example
|
|
1602
|
+
* ```ts
|
|
1603
|
+
* // Try bench-notes, then journal, then blog
|
|
1604
|
+
* const entries = await client.collectionFallback(['bench-notes', 'journal', 'blog']);
|
|
1605
|
+
* ```
|
|
1606
|
+
*/
|
|
1607
|
+
async collectionFallback(slugs, options) {
|
|
1608
|
+
return this.workspace.collections.fallback(slugs, options);
|
|
1609
|
+
}
|
|
1084
1610
|
// ======================
|
|
1085
1611
|
// Backwards-Compatible Publishing API
|
|
1086
1612
|
// ======================
|
|
@@ -1104,6 +1630,9 @@ var init_marvin = __esm({
|
|
|
1104
1630
|
*/
|
|
1105
1631
|
async getEntry(slug) {
|
|
1106
1632
|
const entry = await this.workspace.entries.get(slug);
|
|
1633
|
+
if (!entry) {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1107
1636
|
return entry.toJSON();
|
|
1108
1637
|
}
|
|
1109
1638
|
/**
|
|
@@ -1119,6 +1648,9 @@ var init_marvin = __esm({
|
|
|
1119
1648
|
*/
|
|
1120
1649
|
async getCollection(slug) {
|
|
1121
1650
|
const collection = await this.workspace.collections.get(slug);
|
|
1651
|
+
if (!collection) {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1122
1654
|
return collection.toJSON();
|
|
1123
1655
|
}
|
|
1124
1656
|
/**
|
|
@@ -1175,6 +1707,9 @@ __export(index_exports, {
|
|
|
1175
1707
|
MarvinClient: () => MarvinClient,
|
|
1176
1708
|
MarvinConfigError: () => MarvinConfigError,
|
|
1177
1709
|
MarvinError: () => MarvinError,
|
|
1710
|
+
MarvinNetworkError: () => MarvinNetworkError,
|
|
1711
|
+
MarvinNotFoundError: () => MarvinNotFoundError,
|
|
1712
|
+
MarvinServerError: () => MarvinServerError,
|
|
1178
1713
|
MarvinValidationError: () => MarvinValidationError,
|
|
1179
1714
|
NoAuth: () => NoAuth,
|
|
1180
1715
|
Resource: () => Resource,
|
|
@@ -1209,12 +1744,14 @@ var AuthClient = class extends HttpClient {
|
|
|
1209
1744
|
* Returns the created user object
|
|
1210
1745
|
*/
|
|
1211
1746
|
async register(data) {
|
|
1747
|
+
validateEmail(data.email, "Email");
|
|
1212
1748
|
return this.post("/api/users/register", data);
|
|
1213
1749
|
}
|
|
1214
1750
|
/**
|
|
1215
1751
|
* Request password reset (sends email with token)
|
|
1216
1752
|
*/
|
|
1217
1753
|
async forgotPassword(data) {
|
|
1754
|
+
validateEmail(data.email, "Email");
|
|
1218
1755
|
return this.post("/api/users/forgot-password", data);
|
|
1219
1756
|
}
|
|
1220
1757
|
/**
|
|
@@ -1271,6 +1808,9 @@ function createMarvinClient(config) {
|
|
|
1271
1808
|
MarvinClient,
|
|
1272
1809
|
MarvinConfigError,
|
|
1273
1810
|
MarvinError,
|
|
1811
|
+
MarvinNetworkError,
|
|
1812
|
+
MarvinNotFoundError,
|
|
1813
|
+
MarvinServerError,
|
|
1274
1814
|
MarvinValidationError,
|
|
1275
1815
|
NoAuth,
|
|
1276
1816
|
Resource,
|