@inneropen/marvin-sdk 2.0.1 → 3.0.0
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 +179 -14
- 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-SGZG7U7V.js +1015 -0
- package/dist/chunk-WJC4X7TB.js +738 -0
- package/dist/index.cjs +574 -28
- 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 +511 -55
- package/dist/platform.d.cts +287 -35
- package/dist/platform.d.ts +287 -35
- package/dist/platform.js +282 -45
- package/dist/publish.cjs +1515 -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 +11 -6
- package/dist/chunk-25F3IF6S.js +0 -396
package/dist/index.cjs
CHANGED
|
@@ -27,24 +27,117 @@ __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
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function getEnv(key) {
|
|
126
|
+
if (typeof process !== "undefined" && process.env) {
|
|
127
|
+
return process.env[key];
|
|
38
128
|
}
|
|
129
|
+
return void 0;
|
|
39
130
|
}
|
|
40
131
|
function createConfigFromEnv(overrides) {
|
|
41
132
|
return {
|
|
42
|
-
apiUrl: overrides?.apiUrl ||
|
|
43
|
-
siteClientToken: overrides?.siteClientToken ||
|
|
44
|
-
workspaceSlug: overrides?.workspaceSlug ||
|
|
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") || "",
|
|
45
136
|
autoInitialize: overrides?.autoInitialize ?? false,
|
|
46
|
-
cacheDuration: overrides?.cacheDuration ?? 5 * 60 * 1e3
|
|
137
|
+
cacheDuration: overrides?.cacheDuration ?? 5 * 60 * 1e3,
|
|
47
138
|
// 5 minutes
|
|
139
|
+
debug: overrides?.debug ?? getEnv("MARVIN_DEBUG") === "true",
|
|
140
|
+
logger: overrides?.logger ?? console
|
|
48
141
|
};
|
|
49
142
|
}
|
|
50
143
|
var init_config = __esm({
|
|
@@ -54,7 +147,7 @@ var init_config = __esm({
|
|
|
54
147
|
});
|
|
55
148
|
|
|
56
149
|
// src/core/errors/MarvinError.ts
|
|
57
|
-
var MarvinError, MarvinApiError, MarvinAuthError, MarvinConfigError, MarvinValidationError;
|
|
150
|
+
var MarvinError, MarvinApiError, MarvinAuthError, MarvinConfigError, MarvinValidationError, MarvinNotFoundError, MarvinNetworkError, MarvinServerError;
|
|
58
151
|
var init_MarvinError = __esm({
|
|
59
152
|
"src/core/errors/MarvinError.ts"() {
|
|
60
153
|
"use strict";
|
|
@@ -110,6 +203,33 @@ ${body}` : ""}`;
|
|
|
110
203
|
}
|
|
111
204
|
field;
|
|
112
205
|
};
|
|
206
|
+
MarvinNotFoundError = class _MarvinNotFoundError extends MarvinError {
|
|
207
|
+
constructor(message, endpoint) {
|
|
208
|
+
super(message, "NOT_FOUND", 404);
|
|
209
|
+
this.endpoint = endpoint;
|
|
210
|
+
this.name = "MarvinNotFoundError";
|
|
211
|
+
Object.setPrototypeOf(this, _MarvinNotFoundError.prototype);
|
|
212
|
+
}
|
|
213
|
+
endpoint;
|
|
214
|
+
};
|
|
215
|
+
MarvinNetworkError = class _MarvinNetworkError extends MarvinError {
|
|
216
|
+
constructor(message, originalError) {
|
|
217
|
+
super(message, "NETWORK_ERROR");
|
|
218
|
+
this.originalError = originalError;
|
|
219
|
+
this.name = "MarvinNetworkError";
|
|
220
|
+
Object.setPrototypeOf(this, _MarvinNetworkError.prototype);
|
|
221
|
+
}
|
|
222
|
+
originalError;
|
|
223
|
+
};
|
|
224
|
+
MarvinServerError = class _MarvinServerError extends MarvinError {
|
|
225
|
+
constructor(message, statusCode, endpoint) {
|
|
226
|
+
super(message, "SERVER_ERROR", statusCode);
|
|
227
|
+
this.endpoint = endpoint;
|
|
228
|
+
this.name = "MarvinServerError";
|
|
229
|
+
Object.setPrototypeOf(this, _MarvinServerError.prototype);
|
|
230
|
+
}
|
|
231
|
+
endpoint;
|
|
232
|
+
};
|
|
113
233
|
}
|
|
114
234
|
});
|
|
115
235
|
|
|
@@ -133,12 +253,47 @@ var init_HttpClient = __esm({
|
|
|
133
253
|
defaultHeaders;
|
|
134
254
|
credentials;
|
|
135
255
|
timeout;
|
|
256
|
+
debug;
|
|
257
|
+
retryConfig;
|
|
258
|
+
logger;
|
|
136
259
|
constructor(config) {
|
|
137
260
|
this.baseUrl = config.baseUrl;
|
|
138
261
|
this.auth = config.auth;
|
|
139
262
|
this.defaultHeaders = config.defaultHeaders || {};
|
|
140
263
|
this.credentials = config.credentials || "same-origin";
|
|
141
|
-
|
|
264
|
+
const MAX_TIMEOUT = 12e4;
|
|
265
|
+
this.timeout = Math.min(config.timeout || 3e4, MAX_TIMEOUT);
|
|
266
|
+
this.debug = config.debug ?? false;
|
|
267
|
+
this.logger = config.logger ?? console;
|
|
268
|
+
this.retryConfig = {
|
|
269
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
270
|
+
initialDelay: config.retry?.initialDelay ?? 1e3,
|
|
271
|
+
maxDelay: config.retry?.maxDelay ?? 1e4,
|
|
272
|
+
retryableStatuses: config.retry?.retryableStatuses ?? [408, 429, 500, 502, 503, 504]
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Validate path parameter to prevent path traversal attacks
|
|
277
|
+
*/
|
|
278
|
+
validatePathParam(param, paramName = "parameter") {
|
|
279
|
+
if (typeof param === "number") {
|
|
280
|
+
return String(param);
|
|
281
|
+
}
|
|
282
|
+
if (!param || typeof param !== "string") {
|
|
283
|
+
throw new MarvinValidationError(`Invalid ${paramName}: must be a non-empty string or number`);
|
|
284
|
+
}
|
|
285
|
+
if (param.includes("..") || param.includes("/") || param.includes("\\")) {
|
|
286
|
+
throw new MarvinValidationError(
|
|
287
|
+
`Invalid ${paramName}: cannot contain path traversal characters (.. / \\)`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (param.includes("\0")) {
|
|
291
|
+
throw new MarvinValidationError(`Invalid ${paramName}: cannot contain null bytes`);
|
|
292
|
+
}
|
|
293
|
+
if (param.length > 255) {
|
|
294
|
+
throw new MarvinValidationError(`Invalid ${paramName}: exceeds maximum length of 255 characters`);
|
|
295
|
+
}
|
|
296
|
+
return param;
|
|
142
297
|
}
|
|
143
298
|
/**
|
|
144
299
|
* Build full URL from endpoint
|
|
@@ -172,14 +327,78 @@ var init_HttpClient = __esm({
|
|
|
172
327
|
return filtered.length > 0 ? `?${filtered.join("&")}` : "";
|
|
173
328
|
}
|
|
174
329
|
/**
|
|
175
|
-
*
|
|
330
|
+
* Sanitize data for logging by redacting sensitive fields
|
|
331
|
+
*/
|
|
332
|
+
sanitizeForLogging(data) {
|
|
333
|
+
if (!data || typeof data !== "object") {
|
|
334
|
+
return data;
|
|
335
|
+
}
|
|
336
|
+
const sensitiveFields = [
|
|
337
|
+
"token",
|
|
338
|
+
"access_token",
|
|
339
|
+
"refresh_token",
|
|
340
|
+
"password",
|
|
341
|
+
"secret",
|
|
342
|
+
"apiKey",
|
|
343
|
+
"api_key",
|
|
344
|
+
"bearer",
|
|
345
|
+
"authorization",
|
|
346
|
+
"csrf_token",
|
|
347
|
+
"session",
|
|
348
|
+
"cookie"
|
|
349
|
+
];
|
|
350
|
+
const sanitize = (obj) => {
|
|
351
|
+
const sanitized = {};
|
|
352
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
353
|
+
const keyLower = key.toLowerCase();
|
|
354
|
+
const isSensitive = sensitiveFields.some((field) => keyLower.includes(field));
|
|
355
|
+
if (isSensitive) {
|
|
356
|
+
sanitized[key] = "[REDACTED]";
|
|
357
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
358
|
+
sanitized[key] = sanitize(value);
|
|
359
|
+
} else if (Array.isArray(value)) {
|
|
360
|
+
sanitized[key] = value.map(
|
|
361
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
sanitized[key] = value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return sanitized;
|
|
368
|
+
};
|
|
369
|
+
if (Array.isArray(data)) {
|
|
370
|
+
return data.map(
|
|
371
|
+
(item) => item && typeof item === "object" ? sanitize(item) : item
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return sanitize(data);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Calculate delay for retry with exponential backoff
|
|
378
|
+
*/
|
|
379
|
+
calculateRetryDelay(attempt) {
|
|
380
|
+
const delay = this.retryConfig.initialDelay * Math.pow(2, attempt);
|
|
381
|
+
return Math.min(delay, this.retryConfig.maxDelay);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Sleep for specified milliseconds
|
|
176
385
|
*/
|
|
177
|
-
|
|
386
|
+
sleep(ms) {
|
|
387
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Perform HTTP request with automatic retry on transient failures
|
|
391
|
+
*/
|
|
392
|
+
async request(method, endpoint, options, retryAttempt = 0) {
|
|
178
393
|
const url = this.buildUrl(endpoint) + (options?.params ? this.buildQueryString(options.params) : "");
|
|
179
394
|
const isFormData = options?.body instanceof FormData;
|
|
180
395
|
const headers = this.buildHeaders(options?.headers, isFormData);
|
|
181
396
|
const controller = new AbortController();
|
|
182
397
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
398
|
+
const startTime = Date.now();
|
|
399
|
+
if (this.debug) {
|
|
400
|
+
this.logger.log(`[Marvin] ${method} ${url}`);
|
|
401
|
+
}
|
|
183
402
|
try {
|
|
184
403
|
const response = await fetch(url, {
|
|
185
404
|
method,
|
|
@@ -189,11 +408,54 @@ var init_HttpClient = __esm({
|
|
|
189
408
|
signal: controller.signal
|
|
190
409
|
});
|
|
191
410
|
clearTimeout(timeoutId);
|
|
411
|
+
const duration = Date.now() - startTime;
|
|
412
|
+
if (this.debug) {
|
|
413
|
+
this.logger.log(
|
|
414
|
+
`[Marvin] ${response.status} ${response.statusText} (${duration}ms)`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
192
417
|
if (response.status === 401 && this.auth.handleUnauthorized) {
|
|
193
418
|
await this.auth.handleUnauthorized();
|
|
194
419
|
}
|
|
195
420
|
if (!response.ok) {
|
|
196
421
|
const errorText = await response.text().catch(() => "");
|
|
422
|
+
if (this.debug) {
|
|
423
|
+
this.logger.error(
|
|
424
|
+
`[Marvin] Error response: ${response.status} ${response.statusText}`,
|
|
425
|
+
errorText ? `Body: ${errorText}` : ""
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const isRetryable = this.retryConfig.retryableStatuses.includes(response.status);
|
|
429
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
430
|
+
if (isRetryable && canRetry) {
|
|
431
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
432
|
+
if (this.debug) {
|
|
433
|
+
this.logger.warn(
|
|
434
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (status: ${response.status})`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
await this.sleep(delay);
|
|
438
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
439
|
+
}
|
|
440
|
+
if (response.status === 404) {
|
|
441
|
+
throw new MarvinNotFoundError(
|
|
442
|
+
`Resource not found: ${endpoint}`,
|
|
443
|
+
endpoint
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (response.status === 401 || response.status === 403) {
|
|
447
|
+
throw new MarvinAuthError(
|
|
448
|
+
`Authentication failed: ${response.statusText}`,
|
|
449
|
+
response.status
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (response.status >= 500) {
|
|
453
|
+
throw new MarvinServerError(
|
|
454
|
+
`Server error: ${response.status} ${response.statusText}`,
|
|
455
|
+
response.status,
|
|
456
|
+
endpoint
|
|
457
|
+
);
|
|
458
|
+
}
|
|
197
459
|
throw MarvinApiError.fromResponse(
|
|
198
460
|
response.status,
|
|
199
461
|
response.statusText,
|
|
@@ -205,21 +467,39 @@ var init_HttpClient = __esm({
|
|
|
205
467
|
if (!contentType || !contentType.includes("application/json")) {
|
|
206
468
|
return void 0;
|
|
207
469
|
}
|
|
208
|
-
|
|
470
|
+
const data = await response.json();
|
|
471
|
+
if (this.debug) {
|
|
472
|
+
const sanitized = this.sanitizeForLogging(data);
|
|
473
|
+
this.logger.log("[Marvin] Response:", JSON.stringify(sanitized, null, 2));
|
|
474
|
+
}
|
|
475
|
+
return data;
|
|
209
476
|
} catch (error) {
|
|
210
477
|
clearTimeout(timeoutId);
|
|
211
|
-
if (error instanceof MarvinApiError) {
|
|
478
|
+
if (error instanceof MarvinApiError || error instanceof MarvinNotFoundError || error instanceof MarvinAuthError || error instanceof MarvinServerError) {
|
|
212
479
|
throw error;
|
|
213
480
|
}
|
|
214
481
|
if (error instanceof Error) {
|
|
482
|
+
const canRetry = retryAttempt < this.retryConfig.maxRetries;
|
|
483
|
+
if (canRetry && error.name !== "AbortError") {
|
|
484
|
+
const delay = this.calculateRetryDelay(retryAttempt);
|
|
485
|
+
if (this.debug) {
|
|
486
|
+
this.logger.warn(
|
|
487
|
+
`[Marvin] Retry ${retryAttempt + 1}/${this.retryConfig.maxRetries} after ${delay}ms (network error)`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
await this.sleep(delay);
|
|
491
|
+
return this.request(method, endpoint, options, retryAttempt + 1);
|
|
492
|
+
}
|
|
215
493
|
if (error.name === "AbortError") {
|
|
216
|
-
throw new
|
|
494
|
+
throw new MarvinNetworkError(
|
|
217
495
|
`Request timeout after ${this.timeout}ms`,
|
|
218
|
-
|
|
219
|
-
endpoint
|
|
496
|
+
error
|
|
220
497
|
);
|
|
221
498
|
}
|
|
222
|
-
throw new
|
|
499
|
+
throw new MarvinNetworkError(
|
|
500
|
+
`Network error: ${error.message}`,
|
|
501
|
+
error
|
|
502
|
+
);
|
|
223
503
|
}
|
|
224
504
|
throw error;
|
|
225
505
|
}
|
|
@@ -290,10 +570,27 @@ var init_AuthStrategy = __esm({
|
|
|
290
570
|
}
|
|
291
571
|
sessionToken;
|
|
292
572
|
onUnauthorized;
|
|
573
|
+
csrfToken;
|
|
574
|
+
/**
|
|
575
|
+
* Set CSRF token for cross-site request forgery protection
|
|
576
|
+
* The token should be obtained from the server after login
|
|
577
|
+
*/
|
|
578
|
+
setCsrfToken(token) {
|
|
579
|
+
this.csrfToken = token;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get current CSRF token
|
|
583
|
+
*/
|
|
584
|
+
getCsrfToken() {
|
|
585
|
+
return this.csrfToken;
|
|
586
|
+
}
|
|
293
587
|
injectAuth(headers) {
|
|
294
588
|
if (this.sessionToken) {
|
|
295
589
|
headers.set("Authorization", `Bearer ${this.sessionToken}`);
|
|
296
590
|
}
|
|
591
|
+
if (this.csrfToken) {
|
|
592
|
+
headers.set("X-CSRF-Token", this.csrfToken);
|
|
593
|
+
}
|
|
297
594
|
}
|
|
298
595
|
async handleUnauthorized() {
|
|
299
596
|
if (this.onUnauthorized) {
|
|
@@ -301,7 +598,9 @@ var init_AuthStrategy = __esm({
|
|
|
301
598
|
}
|
|
302
599
|
}
|
|
303
600
|
getDescription() {
|
|
304
|
-
|
|
601
|
+
const auth = this.sessionToken ? "Session authentication (explicit token)" : "Session authentication (cookies)";
|
|
602
|
+
const csrf = this.csrfToken ? " with CSRF protection" : "";
|
|
603
|
+
return auth + csrf;
|
|
305
604
|
}
|
|
306
605
|
};
|
|
307
606
|
NoAuth = class {
|
|
@@ -361,11 +660,17 @@ var init_cache = __esm({
|
|
|
361
660
|
"src/core/cache.ts"() {
|
|
362
661
|
"use strict";
|
|
363
662
|
MarvinCache = class {
|
|
364
|
-
constructor(duration = 5 * 60 * 1e3) {
|
|
663
|
+
constructor(duration = 5 * 60 * 1e3, maxEntries = 1e3) {
|
|
365
664
|
this.duration = duration;
|
|
665
|
+
this.maxEntries = maxEntries;
|
|
366
666
|
}
|
|
367
667
|
duration;
|
|
668
|
+
maxEntries;
|
|
368
669
|
cache = /* @__PURE__ */ new Map();
|
|
670
|
+
/**
|
|
671
|
+
* Get cached data by key
|
|
672
|
+
* Returns undefined if not found or expired
|
|
673
|
+
*/
|
|
369
674
|
get(key) {
|
|
370
675
|
const entry = this.cache.get(key);
|
|
371
676
|
if (!entry) return void 0;
|
|
@@ -376,25 +681,149 @@ var init_cache = __esm({
|
|
|
376
681
|
}
|
|
377
682
|
return entry.data;
|
|
378
683
|
}
|
|
379
|
-
|
|
684
|
+
/**
|
|
685
|
+
* Set cached data with optional tags for invalidation
|
|
686
|
+
*/
|
|
687
|
+
set(key, data, tags) {
|
|
688
|
+
if (this.cache.size >= this.maxEntries) {
|
|
689
|
+
const oldestKey = this.cache.keys().next().value;
|
|
690
|
+
if (oldestKey) {
|
|
691
|
+
this.cache.delete(oldestKey);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
380
694
|
this.cache.set(key, {
|
|
381
695
|
data,
|
|
382
|
-
timestamp: Date.now()
|
|
696
|
+
timestamp: Date.now(),
|
|
697
|
+
tags
|
|
383
698
|
});
|
|
384
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Check if key exists and is not expired
|
|
702
|
+
*/
|
|
385
703
|
has(key) {
|
|
386
704
|
return this.get(key) !== void 0;
|
|
387
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Clear all cached entries
|
|
708
|
+
*/
|
|
388
709
|
clear() {
|
|
389
710
|
this.cache.clear();
|
|
390
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Delete a specific cache entry
|
|
714
|
+
*/
|
|
391
715
|
delete(key) {
|
|
392
716
|
this.cache.delete(key);
|
|
393
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Invalidate all cache entries with a specific tag
|
|
720
|
+
* Useful for invalidating related data after mutations
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* ```typescript
|
|
724
|
+
* // Cache entries with tags
|
|
725
|
+
* cache.set('entry:1', data, ['entries', 'entry:1']);
|
|
726
|
+
* cache.set('entries:list', data, ['entries']);
|
|
727
|
+
*
|
|
728
|
+
* // Invalidate all entries
|
|
729
|
+
* cache.invalidateByTag('entries');
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
invalidateByTag(tag) {
|
|
733
|
+
let count = 0;
|
|
734
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
735
|
+
if (entry.tags?.includes(tag)) {
|
|
736
|
+
this.cache.delete(key);
|
|
737
|
+
count++;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return count;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Get cache statistics
|
|
744
|
+
*/
|
|
745
|
+
getStats() {
|
|
746
|
+
let oldest = null;
|
|
747
|
+
let newest = null;
|
|
748
|
+
for (const entry of this.cache.values()) {
|
|
749
|
+
if (oldest === null || entry.timestamp < oldest) {
|
|
750
|
+
oldest = entry.timestamp;
|
|
751
|
+
}
|
|
752
|
+
if (newest === null || entry.timestamp > newest) {
|
|
753
|
+
newest = entry.timestamp;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
size: this.cache.size,
|
|
758
|
+
maxEntries: this.maxEntries,
|
|
759
|
+
oldestEntry: oldest,
|
|
760
|
+
newestEntry: newest
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Remove expired entries (garbage collection)
|
|
765
|
+
* Returns number of entries removed
|
|
766
|
+
*/
|
|
767
|
+
prune() {
|
|
768
|
+
let count = 0;
|
|
769
|
+
const now = Date.now();
|
|
770
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
771
|
+
const age = now - entry.timestamp;
|
|
772
|
+
if (age > this.duration) {
|
|
773
|
+
this.cache.delete(key);
|
|
774
|
+
count++;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return count;
|
|
778
|
+
}
|
|
394
779
|
};
|
|
395
780
|
}
|
|
396
781
|
});
|
|
397
782
|
|
|
783
|
+
// src/core/validation.ts
|
|
784
|
+
function validateEmail(email, fieldName = "Email") {
|
|
785
|
+
if (!email || typeof email !== "string") {
|
|
786
|
+
throw new MarvinValidationError(`${fieldName} is required and must be a string`);
|
|
787
|
+
}
|
|
788
|
+
const trimmed = email.trim();
|
|
789
|
+
if (trimmed.length === 0) {
|
|
790
|
+
throw new MarvinValidationError(`${fieldName} cannot be empty`);
|
|
791
|
+
}
|
|
792
|
+
if (trimmed.length < 5) {
|
|
793
|
+
throw new MarvinValidationError(`${fieldName} is too short`);
|
|
794
|
+
}
|
|
795
|
+
if (trimmed.length > 254) {
|
|
796
|
+
throw new MarvinValidationError(`${fieldName} exceeds maximum length of 254 characters`);
|
|
797
|
+
}
|
|
798
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
799
|
+
throw new MarvinValidationError(
|
|
800
|
+
`${fieldName} format is invalid. Expected format: user@example.com`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const [localPart, domain] = trimmed.split("@");
|
|
804
|
+
if (localPart.length > 64) {
|
|
805
|
+
throw new MarvinValidationError(`${fieldName} local part exceeds 64 characters`);
|
|
806
|
+
}
|
|
807
|
+
if (domain.length > 253) {
|
|
808
|
+
throw new MarvinValidationError(`${fieldName} domain exceeds 253 characters`);
|
|
809
|
+
}
|
|
810
|
+
if (trimmed.includes("..")) {
|
|
811
|
+
throw new MarvinValidationError(`${fieldName} cannot contain consecutive dots`);
|
|
812
|
+
}
|
|
813
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
814
|
+
throw new MarvinValidationError(`${fieldName} local part cannot start or end with a dot`);
|
|
815
|
+
}
|
|
816
|
+
return trimmed;
|
|
817
|
+
}
|
|
818
|
+
var EMAIL_REGEX;
|
|
819
|
+
var init_validation = __esm({
|
|
820
|
+
"src/core/validation.ts"() {
|
|
821
|
+
"use strict";
|
|
822
|
+
init_errors();
|
|
823
|
+
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])?)*$/;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
|
|
398
827
|
// src/core/index.ts
|
|
399
828
|
var init_core = __esm({
|
|
400
829
|
"src/core/index.ts"() {
|
|
@@ -404,6 +833,7 @@ var init_core = __esm({
|
|
|
404
833
|
init_errors();
|
|
405
834
|
init_pagination();
|
|
406
835
|
init_cache();
|
|
836
|
+
init_validation();
|
|
407
837
|
}
|
|
408
838
|
});
|
|
409
839
|
|
|
@@ -418,7 +848,9 @@ var init_http2 = __esm({
|
|
|
418
848
|
super({
|
|
419
849
|
baseUrl: config.apiUrl,
|
|
420
850
|
auth: new BearerTokenAuth(config.siteClientToken),
|
|
421
|
-
credentials: "same-origin"
|
|
851
|
+
credentials: "same-origin",
|
|
852
|
+
debug: config.debug,
|
|
853
|
+
logger: config.logger
|
|
422
854
|
});
|
|
423
855
|
}
|
|
424
856
|
/**
|
|
@@ -555,6 +987,7 @@ var init_entries = __esm({
|
|
|
555
987
|
"src/entries/entries.ts"() {
|
|
556
988
|
"use strict";
|
|
557
989
|
init_entry();
|
|
990
|
+
init_errors();
|
|
558
991
|
EntriesModule = class {
|
|
559
992
|
constructor(http, workspaceSlug) {
|
|
560
993
|
this.http = http;
|
|
@@ -579,11 +1012,19 @@ var init_entries = __esm({
|
|
|
579
1012
|
}
|
|
580
1013
|
/**
|
|
581
1014
|
* Get a single entry by slug
|
|
1015
|
+
* Returns null if entry is not found (404)
|
|
582
1016
|
*/
|
|
583
1017
|
async get(slug) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1018
|
+
try {
|
|
1019
|
+
const endpoint = `/api/publish/${this.workspaceSlug}/entries/${slug}`;
|
|
1020
|
+
const data = await this.http.fetch(endpoint);
|
|
1021
|
+
return new Entry(data, this.http, this.workspaceSlug);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (error instanceof MarvinNotFoundError) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
587
1028
|
}
|
|
588
1029
|
/**
|
|
589
1030
|
* Convenience: Get all pages
|
|
@@ -689,6 +1130,7 @@ var init_collections = __esm({
|
|
|
689
1130
|
"src/collections/collections.ts"() {
|
|
690
1131
|
"use strict";
|
|
691
1132
|
init_collection();
|
|
1133
|
+
init_errors();
|
|
692
1134
|
CollectionsModule = class {
|
|
693
1135
|
constructor(http, workspaceSlug) {
|
|
694
1136
|
this.http = http;
|
|
@@ -706,19 +1148,63 @@ var init_collections = __esm({
|
|
|
706
1148
|
}
|
|
707
1149
|
/**
|
|
708
1150
|
* Get a single collection by slug
|
|
1151
|
+
* Returns null if collection is not found (404)
|
|
709
1152
|
*/
|
|
710
1153
|
async get(slug) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1154
|
+
try {
|
|
1155
|
+
const endpoint = `/api/publish/${this.workspaceSlug}/collections/${slug}`;
|
|
1156
|
+
const data = await this.http.fetch(endpoint);
|
|
1157
|
+
return new Collection(data, this.http, this.workspaceSlug);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
if (error instanceof MarvinNotFoundError) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
throw error;
|
|
1163
|
+
}
|
|
714
1164
|
}
|
|
715
1165
|
/**
|
|
716
1166
|
* Get entries in a collection
|
|
1167
|
+
* Returns empty array if collection is not found
|
|
717
1168
|
*/
|
|
718
1169
|
async entries(slug) {
|
|
719
1170
|
const collection = await this.get(slug);
|
|
1171
|
+
if (!collection) {
|
|
1172
|
+
return [];
|
|
1173
|
+
}
|
|
720
1174
|
return collection.entries();
|
|
721
1175
|
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Try multiple collection slugs in fallback order
|
|
1178
|
+
* Returns the first collection that exists and has entries
|
|
1179
|
+
*
|
|
1180
|
+
* @param slugs - Collection slugs to try in order
|
|
1181
|
+
* @param options - Options for fallback behavior
|
|
1182
|
+
* @returns Entries from the first matching collection, or empty array
|
|
1183
|
+
*
|
|
1184
|
+
* @example
|
|
1185
|
+
* ```ts
|
|
1186
|
+
* // Try bench-notes, then journal, then blog
|
|
1187
|
+
* const entries = await collections.fallback(['bench-notes', 'journal', 'blog']);
|
|
1188
|
+
* ```
|
|
1189
|
+
*/
|
|
1190
|
+
async fallback(slugs, options = {}) {
|
|
1191
|
+
const { requireEntries = true } = options;
|
|
1192
|
+
for (const slug of slugs) {
|
|
1193
|
+
try {
|
|
1194
|
+
const collection = await this.get(slug);
|
|
1195
|
+
if (!collection) {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const entries = await collection.entries();
|
|
1199
|
+
if (!requireEntries || entries.length > 0) {
|
|
1200
|
+
return entries;
|
|
1201
|
+
}
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return [];
|
|
1207
|
+
}
|
|
722
1208
|
};
|
|
723
1209
|
}
|
|
724
1210
|
});
|
|
@@ -890,6 +1376,26 @@ var init_resources = __esm({
|
|
|
890
1376
|
}
|
|
891
1377
|
});
|
|
892
1378
|
|
|
1379
|
+
// src/renderers/renderers.ts
|
|
1380
|
+
var RenderersModule;
|
|
1381
|
+
var init_renderers = __esm({
|
|
1382
|
+
"src/renderers/renderers.ts"() {
|
|
1383
|
+
"use strict";
|
|
1384
|
+
RenderersModule = class {
|
|
1385
|
+
constructor(http, workspaceSlug) {
|
|
1386
|
+
this.http = http;
|
|
1387
|
+
this.workspaceSlug = workspaceSlug;
|
|
1388
|
+
}
|
|
1389
|
+
http;
|
|
1390
|
+
workspaceSlug;
|
|
1391
|
+
async list() {
|
|
1392
|
+
const endpoint = `/api/publish/${this.workspaceSlug}/entry-types`;
|
|
1393
|
+
return this.http.fetch(endpoint);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
893
1399
|
// src/workspaces/workspace.ts
|
|
894
1400
|
var Workspace;
|
|
895
1401
|
var init_workspace = __esm({
|
|
@@ -899,6 +1405,7 @@ var init_workspace = __esm({
|
|
|
899
1405
|
init_collections();
|
|
900
1406
|
init_assets();
|
|
901
1407
|
init_resources();
|
|
1408
|
+
init_renderers();
|
|
902
1409
|
Workspace = class {
|
|
903
1410
|
constructor(http, slug) {
|
|
904
1411
|
this.http = http;
|
|
@@ -907,6 +1414,7 @@ var init_workspace = __esm({
|
|
|
907
1414
|
this.collections = new CollectionsModule(http, slug);
|
|
908
1415
|
this.assets = new AssetsModule(http, slug);
|
|
909
1416
|
this.resources = new ResourcesModule(http, slug);
|
|
1417
|
+
this.renderers = new RenderersModule(http, slug);
|
|
910
1418
|
}
|
|
911
1419
|
http;
|
|
912
1420
|
_site = null;
|
|
@@ -915,6 +1423,7 @@ var init_workspace = __esm({
|
|
|
915
1423
|
collections;
|
|
916
1424
|
assets;
|
|
917
1425
|
resources;
|
|
1426
|
+
renderers;
|
|
918
1427
|
/**
|
|
919
1428
|
* Get workspace slug
|
|
920
1429
|
*/
|
|
@@ -1042,6 +1551,12 @@ var init_marvin = __esm({
|
|
|
1042
1551
|
get resources() {
|
|
1043
1552
|
return this.workspace.resources;
|
|
1044
1553
|
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Get renderers module
|
|
1556
|
+
*/
|
|
1557
|
+
get renderers() {
|
|
1558
|
+
return this.workspace.renderers;
|
|
1559
|
+
}
|
|
1045
1560
|
// ======================
|
|
1046
1561
|
// Convenience Methods
|
|
1047
1562
|
// ======================
|
|
@@ -1081,6 +1596,23 @@ var init_marvin = __esm({
|
|
|
1081
1596
|
async resource(slug) {
|
|
1082
1597
|
return this.workspace.resources.get(slug);
|
|
1083
1598
|
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Try multiple collection slugs in fallback order
|
|
1601
|
+
* Returns entries from the first collection that exists and has content
|
|
1602
|
+
*
|
|
1603
|
+
* @param slugs - Collection slugs to try in order
|
|
1604
|
+
* @param options - Options for fallback behavior
|
|
1605
|
+
* @returns Entries from the first matching collection, or empty array
|
|
1606
|
+
*
|
|
1607
|
+
* @example
|
|
1608
|
+
* ```ts
|
|
1609
|
+
* // Try bench-notes, then journal, then blog
|
|
1610
|
+
* const entries = await client.collectionFallback(['bench-notes', 'journal', 'blog']);
|
|
1611
|
+
* ```
|
|
1612
|
+
*/
|
|
1613
|
+
async collectionFallback(slugs, options) {
|
|
1614
|
+
return this.workspace.collections.fallback(slugs, options);
|
|
1615
|
+
}
|
|
1084
1616
|
// ======================
|
|
1085
1617
|
// Backwards-Compatible Publishing API
|
|
1086
1618
|
// ======================
|
|
@@ -1104,6 +1636,9 @@ var init_marvin = __esm({
|
|
|
1104
1636
|
*/
|
|
1105
1637
|
async getEntry(slug) {
|
|
1106
1638
|
const entry = await this.workspace.entries.get(slug);
|
|
1639
|
+
if (!entry) {
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1107
1642
|
return entry.toJSON();
|
|
1108
1643
|
}
|
|
1109
1644
|
/**
|
|
@@ -1119,6 +1654,9 @@ var init_marvin = __esm({
|
|
|
1119
1654
|
*/
|
|
1120
1655
|
async getCollection(slug) {
|
|
1121
1656
|
const collection = await this.workspace.collections.get(slug);
|
|
1657
|
+
if (!collection) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1122
1660
|
return collection.toJSON();
|
|
1123
1661
|
}
|
|
1124
1662
|
/**
|
|
@@ -1175,6 +1713,9 @@ __export(index_exports, {
|
|
|
1175
1713
|
MarvinClient: () => MarvinClient,
|
|
1176
1714
|
MarvinConfigError: () => MarvinConfigError,
|
|
1177
1715
|
MarvinError: () => MarvinError,
|
|
1716
|
+
MarvinNetworkError: () => MarvinNetworkError,
|
|
1717
|
+
MarvinNotFoundError: () => MarvinNotFoundError,
|
|
1718
|
+
MarvinServerError: () => MarvinServerError,
|
|
1178
1719
|
MarvinValidationError: () => MarvinValidationError,
|
|
1179
1720
|
NoAuth: () => NoAuth,
|
|
1180
1721
|
Resource: () => Resource,
|
|
@@ -1209,12 +1750,14 @@ var AuthClient = class extends HttpClient {
|
|
|
1209
1750
|
* Returns the created user object
|
|
1210
1751
|
*/
|
|
1211
1752
|
async register(data) {
|
|
1753
|
+
validateEmail(data.email, "Email");
|
|
1212
1754
|
return this.post("/api/users/register", data);
|
|
1213
1755
|
}
|
|
1214
1756
|
/**
|
|
1215
1757
|
* Request password reset (sends email with token)
|
|
1216
1758
|
*/
|
|
1217
1759
|
async forgotPassword(data) {
|
|
1760
|
+
validateEmail(data.email, "Email");
|
|
1218
1761
|
return this.post("/api/users/forgot-password", data);
|
|
1219
1762
|
}
|
|
1220
1763
|
/**
|
|
@@ -1271,6 +1814,9 @@ function createMarvinClient(config) {
|
|
|
1271
1814
|
MarvinClient,
|
|
1272
1815
|
MarvinConfigError,
|
|
1273
1816
|
MarvinError,
|
|
1817
|
+
MarvinNetworkError,
|
|
1818
|
+
MarvinNotFoundError,
|
|
1819
|
+
MarvinServerError,
|
|
1274
1820
|
MarvinValidationError,
|
|
1275
1821
|
NoAuth,
|
|
1276
1822
|
Resource,
|