@bettercms-ai/sdk 1.13.1 → 1.14.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/dist/index.cjs +136 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -8
- package/dist/index.d.ts +57 -8
- package/dist/index.js +134 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
BetterCMSError: () => BetterCMSError,
|
|
39
39
|
BetterCMSManagementClient: () => BetterCMSManagementClient,
|
|
40
40
|
ErrorCodes: () => ErrorCodes,
|
|
41
|
+
PORTABLE_TEXT_FORMAT: () => PORTABLE_TEXT_FORMAT,
|
|
41
42
|
addModelFields: () => addModelFields,
|
|
42
43
|
addPageFields: () => addPageFields,
|
|
43
44
|
asStringArray: () => asStringArray,
|
|
@@ -87,6 +88,7 @@ __export(index_exports, {
|
|
|
87
88
|
listSubmissions: () => listSubmissions,
|
|
88
89
|
listWorkspaces: () => listWorkspaces,
|
|
89
90
|
plain: () => plain,
|
|
91
|
+
portableText: () => portableText,
|
|
90
92
|
publishPage: () => publishPage,
|
|
91
93
|
regenerateApiKey: () => regenerateApiKey,
|
|
92
94
|
removeMember: () => removeMember,
|
|
@@ -205,6 +207,35 @@ var BetterCMSError = class _BetterCMSError extends Error {
|
|
|
205
207
|
}
|
|
206
208
|
};
|
|
207
209
|
|
|
210
|
+
// src/retry.ts
|
|
211
|
+
var RETRY_STATUSES = [429, 503];
|
|
212
|
+
var MAX_RETRIES = 3;
|
|
213
|
+
var MAX_DELAY_MS = 65e3;
|
|
214
|
+
var BASE_DELAY_MS = 1e3;
|
|
215
|
+
var JITTER_MS = 250;
|
|
216
|
+
function isRetryableStatus(status) {
|
|
217
|
+
return RETRY_STATUSES.includes(status);
|
|
218
|
+
}
|
|
219
|
+
function retryAfterMs(res) {
|
|
220
|
+
const raw = res.headers?.get?.("retry-after");
|
|
221
|
+
if (!raw) return null;
|
|
222
|
+
const seconds = Number(raw);
|
|
223
|
+
if (Number.isFinite(seconds)) {
|
|
224
|
+
return Math.min(Math.max(seconds, 0) * 1e3, MAX_DELAY_MS);
|
|
225
|
+
}
|
|
226
|
+
const at = Date.parse(raw);
|
|
227
|
+
if (!Number.isNaN(at)) {
|
|
228
|
+
return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function backoffMs(attempt, res) {
|
|
233
|
+
const fromServer = res ? retryAfterMs(res) : null;
|
|
234
|
+
const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);
|
|
235
|
+
return base + Math.random() * JITTER_MS;
|
|
236
|
+
}
|
|
237
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
238
|
+
|
|
208
239
|
// src/methods/getContent.ts
|
|
209
240
|
async function getContent(client, slug) {
|
|
210
241
|
const data = await client.fetchJSON(client.url(slug));
|
|
@@ -269,14 +300,17 @@ function listContentAll(client, opts) {
|
|
|
269
300
|
}
|
|
270
301
|
|
|
271
302
|
// src/delivery-client.ts
|
|
303
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
272
304
|
var BetterCMSDeliveryClient = class {
|
|
273
305
|
workspace;
|
|
274
306
|
baseUrl;
|
|
307
|
+
timeout;
|
|
275
308
|
apiKey;
|
|
276
309
|
constructor(opts) {
|
|
277
310
|
this.workspace = opts.workspace;
|
|
278
311
|
this.baseUrl = opts.baseUrl;
|
|
279
312
|
this.apiKey = opts.apiKey;
|
|
313
|
+
this.timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
280
314
|
}
|
|
281
315
|
getContent(slug) {
|
|
282
316
|
return getContent(this, slug);
|
|
@@ -309,24 +343,46 @@ var BetterCMSDeliveryClient = class {
|
|
|
309
343
|
return headers;
|
|
310
344
|
}
|
|
311
345
|
/**
|
|
312
|
-
* Generic fetch helper —
|
|
313
|
-
* and throws a typed BetterCMSError on non-OK responses.
|
|
346
|
+
* Generic fetch helper — retries 429/503 honouring Retry-After, applies a
|
|
347
|
+
* per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
|
|
348
|
+
*
|
|
349
|
+
* This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
|
|
350
|
+
* 429 from the delivery limiter threw straight through and killed the caller. In a
|
|
351
|
+
* static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
|
|
352
|
+
* never going to be enough against a 60-second fixed window.
|
|
314
353
|
*/
|
|
315
354
|
async fetchJSON(url) {
|
|
316
|
-
let
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
355
|
+
for (let attempt = 0; ; attempt++) {
|
|
356
|
+
const controller = new AbortController();
|
|
357
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
358
|
+
let res;
|
|
359
|
+
try {
|
|
360
|
+
res = await globalThis.fetch(url, {
|
|
361
|
+
headers: this.headers(),
|
|
362
|
+
signal: controller.signal
|
|
363
|
+
});
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const timedOut = err instanceof Error && err.name === "AbortError";
|
|
366
|
+
if (attempt < MAX_RETRIES) {
|
|
367
|
+
await sleep(backoffMs(attempt));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (timedOut) throw new BetterCMSError("Request timeout", 408, "NETWORK_ERROR");
|
|
371
|
+
throw new BetterCMSError(
|
|
372
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
373
|
+
0,
|
|
374
|
+
"NETWORK_ERROR"
|
|
375
|
+
);
|
|
376
|
+
} finally {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
}
|
|
379
|
+
if (res.ok) return await res.json();
|
|
380
|
+
if (attempt < MAX_RETRIES && isRetryableStatus(res.status)) {
|
|
381
|
+
await sleep(backoffMs(attempt, res));
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
327
384
|
throw await BetterCMSError.from(res);
|
|
328
385
|
}
|
|
329
|
-
return await res.json();
|
|
330
386
|
}
|
|
331
387
|
};
|
|
332
388
|
|
|
@@ -407,19 +463,15 @@ function addWebhookMethods(client) {
|
|
|
407
463
|
|
|
408
464
|
// src/admin-client.ts
|
|
409
465
|
var DEFAULT_BASE_URL = "https://api.bettercms.ai/v1";
|
|
410
|
-
var DEFAULT_TIMEOUT = 1e4;
|
|
411
|
-
var RETRY_STATUSES = [429, 503];
|
|
412
|
-
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
413
466
|
var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
414
|
-
timeout;
|
|
415
467
|
_token;
|
|
416
468
|
constructor(options) {
|
|
417
469
|
super({
|
|
418
470
|
workspace: "",
|
|
419
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL
|
|
471
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
472
|
+
timeout: options.timeout
|
|
420
473
|
});
|
|
421
474
|
this._token = options.token;
|
|
422
|
-
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
423
475
|
}
|
|
424
476
|
headers() {
|
|
425
477
|
return {
|
|
@@ -429,12 +481,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
429
481
|
};
|
|
430
482
|
}
|
|
431
483
|
/**
|
|
432
|
-
* Admin fetchJSON with retry (429, 503) and per-request
|
|
433
|
-
* Each retry gets a fresh timeout.
|
|
484
|
+
* Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
|
|
485
|
+
* timeout (AbortController). Each retry gets a fresh timeout.
|
|
434
486
|
*/
|
|
435
487
|
async fetchJSON(url, init) {
|
|
436
488
|
let lastError;
|
|
437
|
-
for (let attempt = 0; attempt <=
|
|
489
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
438
490
|
const controller = new AbortController();
|
|
439
491
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
440
492
|
let nonRetryable = false;
|
|
@@ -448,12 +500,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
448
500
|
if (res.ok) {
|
|
449
501
|
return res.json();
|
|
450
502
|
}
|
|
451
|
-
const shouldRetry = attempt <
|
|
503
|
+
const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
452
504
|
if (!shouldRetry) {
|
|
453
505
|
nonRetryable = true;
|
|
454
506
|
throw await BetterCMSError.from(res);
|
|
455
507
|
}
|
|
456
|
-
await sleep(
|
|
508
|
+
await sleep(backoffMs(attempt, res));
|
|
457
509
|
} catch (err) {
|
|
458
510
|
clearTimeout(timeoutId);
|
|
459
511
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -463,8 +515,8 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
463
515
|
throw err;
|
|
464
516
|
}
|
|
465
517
|
lastError = err;
|
|
466
|
-
if (attempt <
|
|
467
|
-
await sleep(
|
|
518
|
+
if (attempt < MAX_RETRIES) {
|
|
519
|
+
await sleep(backoffMs(attempt));
|
|
468
520
|
continue;
|
|
469
521
|
}
|
|
470
522
|
throw err;
|
|
@@ -481,9 +533,6 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
481
533
|
return addWebhookMethods(this);
|
|
482
534
|
}
|
|
483
535
|
};
|
|
484
|
-
function sleep(ms) {
|
|
485
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
486
|
-
}
|
|
487
536
|
|
|
488
537
|
// src/methods/management/content.ts
|
|
489
538
|
async function listModels(client) {
|
|
@@ -624,7 +673,14 @@ var MIME_BY_EXT = {
|
|
|
624
673
|
avif: "image/avif",
|
|
625
674
|
mp4: "video/mp4",
|
|
626
675
|
webm: "video/webm",
|
|
627
|
-
pdf: "application/pdf"
|
|
676
|
+
pdf: "application/pdf",
|
|
677
|
+
// mp3/ogg were server-allowed all along; the SDK sent application/octet-stream for them,
|
|
678
|
+
// which the allowlist then refused — so an agent could never upload audio.
|
|
679
|
+
mp3: "audio/mpeg",
|
|
680
|
+
ogg: "audio/ogg",
|
|
681
|
+
txt: "text/plain",
|
|
682
|
+
md: "text/markdown",
|
|
683
|
+
markdown: "text/markdown"
|
|
628
684
|
};
|
|
629
685
|
function basenameOf(p) {
|
|
630
686
|
const clean = p.split(/[?#]/)[0] ?? p;
|
|
@@ -853,8 +909,6 @@ async function commandManagedLayout(client, input) {
|
|
|
853
909
|
// src/management-client.ts
|
|
854
910
|
var DEFAULT_BASE_URL2 = "https://api.bettercms.ai/v1";
|
|
855
911
|
var DEFAULT_TIMEOUT2 = 1e4;
|
|
856
|
-
var RETRY_STATUSES2 = [429, 503];
|
|
857
|
-
var RETRY_DELAYS2 = [1e3, 2e3, 4e3];
|
|
858
912
|
var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
859
913
|
timeout;
|
|
860
914
|
_apiKey;
|
|
@@ -876,10 +930,14 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
876
930
|
url(path) {
|
|
877
931
|
return `${this.baseUrl}${path}`;
|
|
878
932
|
}
|
|
879
|
-
/**
|
|
933
|
+
/**
|
|
934
|
+
* fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
|
|
935
|
+
* (mirrors the admin client). See ./retry.ts for why the delay must be able
|
|
936
|
+
* to outlast the backend's 60-second rate-limit window.
|
|
937
|
+
*/
|
|
880
938
|
async fetchJSON(url, init) {
|
|
881
939
|
let lastError;
|
|
882
|
-
for (let attempt = 0; attempt <=
|
|
940
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
883
941
|
const controller = new AbortController();
|
|
884
942
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
885
943
|
let nonRetryable = false;
|
|
@@ -893,12 +951,12 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
893
951
|
if (res.ok) {
|
|
894
952
|
return res.json();
|
|
895
953
|
}
|
|
896
|
-
const shouldRetry = attempt <
|
|
954
|
+
const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
897
955
|
if (!shouldRetry) {
|
|
898
956
|
nonRetryable = true;
|
|
899
957
|
throw await BetterCMSError.from(res);
|
|
900
958
|
}
|
|
901
|
-
await
|
|
959
|
+
await sleep(backoffMs(attempt, res));
|
|
902
960
|
} catch (err) {
|
|
903
961
|
clearTimeout(timeoutId);
|
|
904
962
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -906,8 +964,8 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
906
964
|
}
|
|
907
965
|
if (nonRetryable) throw err;
|
|
908
966
|
lastError = err;
|
|
909
|
-
if (attempt <
|
|
910
|
-
await
|
|
967
|
+
if (attempt < MAX_RETRIES) {
|
|
968
|
+
await sleep(backoffMs(attempt));
|
|
911
969
|
continue;
|
|
912
970
|
}
|
|
913
971
|
throw err;
|
|
@@ -1039,9 +1097,6 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
1039
1097
|
return generateSeoMeta(this, input);
|
|
1040
1098
|
}
|
|
1041
1099
|
};
|
|
1042
|
-
function sleep2(ms) {
|
|
1043
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1044
|
-
}
|
|
1045
1100
|
|
|
1046
1101
|
// src/auth.ts
|
|
1047
1102
|
var AUTH_BASE_URL = "https://api.bettercms.ai/api/v1/auth";
|
|
@@ -1132,7 +1187,8 @@ var BetterCMS = {
|
|
|
1132
1187
|
return new BetterCMSDeliveryClient({
|
|
1133
1188
|
workspace: options.workspace,
|
|
1134
1189
|
apiKey: options.apiKey,
|
|
1135
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3
|
|
1190
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3,
|
|
1191
|
+
timeout: options.timeout
|
|
1136
1192
|
});
|
|
1137
1193
|
},
|
|
1138
1194
|
/**
|
|
@@ -1164,10 +1220,7 @@ var BetterCMS = {
|
|
|
1164
1220
|
};
|
|
1165
1221
|
|
|
1166
1222
|
// src/read-client.ts
|
|
1167
|
-
var RETRY_STATUSES3 = [429, 503];
|
|
1168
|
-
var RETRY_DELAYS3 = [250, 750, 1500];
|
|
1169
1223
|
var DEFAULT_TIMEOUT3 = 1e4;
|
|
1170
|
-
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1171
1224
|
function qs(params2) {
|
|
1172
1225
|
const sp = new URLSearchParams();
|
|
1173
1226
|
for (const [k, v] of Object.entries(params2)) {
|
|
@@ -1184,6 +1237,27 @@ function unwrap(json) {
|
|
|
1184
1237
|
}
|
|
1185
1238
|
return json;
|
|
1186
1239
|
}
|
|
1240
|
+
function createRequestCache() {
|
|
1241
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1242
|
+
const settled = /* @__PURE__ */ new Map();
|
|
1243
|
+
const copy = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
1244
|
+
return async function collapse(url, fetcher) {
|
|
1245
|
+
if (settled.has(url)) return copy(settled.get(url));
|
|
1246
|
+
const pending = inflight.get(url);
|
|
1247
|
+
if (pending) return copy(await pending);
|
|
1248
|
+
const run = (async () => {
|
|
1249
|
+
try {
|
|
1250
|
+
const value = await fetcher();
|
|
1251
|
+
settled.set(url, value);
|
|
1252
|
+
return value;
|
|
1253
|
+
} finally {
|
|
1254
|
+
inflight.delete(url);
|
|
1255
|
+
}
|
|
1256
|
+
})();
|
|
1257
|
+
inflight.set(url, run);
|
|
1258
|
+
return copy(await run);
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1187
1261
|
function createClient(options) {
|
|
1188
1262
|
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
1189
1263
|
const { workspace, apiKey, perspective = "published", previewToken } = options;
|
|
@@ -1195,9 +1269,11 @@ function createClient(options) {
|
|
|
1195
1269
|
if (apiKey) h["X-API-Key"] = apiKey;
|
|
1196
1270
|
return h;
|
|
1197
1271
|
};
|
|
1198
|
-
|
|
1272
|
+
const collapse = options.cache && perspective === "published" ? createRequestCache() : void 0;
|
|
1273
|
+
const fetchJSON = (url) => collapse ? collapse(url, () => request(url)) : request(url);
|
|
1274
|
+
async function request(url) {
|
|
1199
1275
|
let lastError;
|
|
1200
|
-
for (let attempt = 0; attempt <=
|
|
1276
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1201
1277
|
const controller = new AbortController();
|
|
1202
1278
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
1203
1279
|
try {
|
|
@@ -1207,9 +1283,9 @@ function createClient(options) {
|
|
|
1207
1283
|
});
|
|
1208
1284
|
clearTimeout(timer);
|
|
1209
1285
|
if (res.ok) return await res.json();
|
|
1210
|
-
const retryable = attempt <
|
|
1286
|
+
const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
1211
1287
|
if (!retryable) throw await BetterCMSError.from(res);
|
|
1212
|
-
await
|
|
1288
|
+
await sleep(backoffMs(attempt, res));
|
|
1213
1289
|
} catch (err) {
|
|
1214
1290
|
clearTimeout(timer);
|
|
1215
1291
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -1217,8 +1293,8 @@ function createClient(options) {
|
|
|
1217
1293
|
}
|
|
1218
1294
|
if (err instanceof BetterCMSError) throw err;
|
|
1219
1295
|
lastError = err;
|
|
1220
|
-
if (attempt <
|
|
1221
|
-
await
|
|
1296
|
+
if (attempt < MAX_RETRIES) {
|
|
1297
|
+
await sleep(backoffMs(attempt));
|
|
1222
1298
|
continue;
|
|
1223
1299
|
}
|
|
1224
1300
|
throw new BetterCMSError(
|
|
@@ -1340,6 +1416,12 @@ function stripStega(value) {
|
|
|
1340
1416
|
}
|
|
1341
1417
|
|
|
1342
1418
|
// src/richtext.ts
|
|
1419
|
+
var PORTABLE_TEXT_FORMAT = "portable-text-1";
|
|
1420
|
+
function portableText(value) {
|
|
1421
|
+
if (!isRichText(value)) return null;
|
|
1422
|
+
const v = value;
|
|
1423
|
+
return v.format === PORTABLE_TEXT_FORMAT && Array.isArray(v.value) ? v.value : null;
|
|
1424
|
+
}
|
|
1343
1425
|
function isRichText(value) {
|
|
1344
1426
|
return typeof value === "object" && value !== null && !Array.isArray(value) && ("html" in value || "format" in value);
|
|
1345
1427
|
}
|
|
@@ -1728,6 +1810,7 @@ var import_types = require("@bettercms-ai/types");
|
|
|
1728
1810
|
BetterCMSError,
|
|
1729
1811
|
BetterCMSManagementClient,
|
|
1730
1812
|
ErrorCodes,
|
|
1813
|
+
PORTABLE_TEXT_FORMAT,
|
|
1731
1814
|
addModelFields,
|
|
1732
1815
|
addPageFields,
|
|
1733
1816
|
asStringArray,
|
|
@@ -1777,6 +1860,7 @@ var import_types = require("@bettercms-ai/types");
|
|
|
1777
1860
|
listSubmissions,
|
|
1778
1861
|
listWorkspaces,
|
|
1779
1862
|
plain,
|
|
1863
|
+
portableText,
|
|
1780
1864
|
publishPage,
|
|
1781
1865
|
regenerateApiKey,
|
|
1782
1866
|
removeMember,
|