@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 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 — wraps a request with network-error handling
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 res;
317
- try {
318
- res = await globalThis.fetch(url, { headers: this.headers() });
319
- } catch (err) {
320
- throw new BetterCMSError(
321
- `Network error: ${err instanceof Error ? err.message : String(err)}`,
322
- 0,
323
- "NETWORK_ERROR"
324
- );
325
- }
326
- if (!res.ok) {
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 timeout (AbortController).
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 <= 3; 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 < 3 && RETRY_STATUSES.includes(res.status);
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(RETRY_DELAYS[attempt] ?? 0);
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 < 3) {
467
- await sleep(RETRY_DELAYS[attempt] ?? 0);
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
- /** fetchJSON with retry (429, 503) + per-request timeout (mirrors the admin client). */
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 <= 3; 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 < 3 && RETRY_STATUSES2.includes(res.status);
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 sleep2(RETRY_DELAYS2[attempt] ?? 0);
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 < 3) {
910
- await sleep2(RETRY_DELAYS2[attempt] ?? 0);
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
- async function fetchJSON(url) {
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 <= RETRY_DELAYS3.length; 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 < RETRY_DELAYS3.length && RETRY_STATUSES3.includes(res.status);
1286
+ const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
1211
1287
  if (!retryable) throw await BetterCMSError.from(res);
1212
- await sleep3(RETRY_DELAYS3[attempt] ?? 0);
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 < RETRY_DELAYS3.length) {
1221
- await sleep3(RETRY_DELAYS3[attempt] ?? 0);
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,