@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.d.cts CHANGED
@@ -51,11 +51,13 @@ declare function listContentAll(client: BetterCMSDeliveryClient, opts?: ListCont
51
51
  declare class BetterCMSDeliveryClient {
52
52
  readonly workspace: string;
53
53
  readonly baseUrl: string;
54
+ protected readonly timeout: number;
54
55
  private readonly apiKey?;
55
56
  constructor(opts: {
56
57
  workspace: string;
57
58
  apiKey?: string;
58
59
  baseUrl: string;
60
+ timeout?: number;
59
61
  });
60
62
  getContent(slug: string): Promise<_bettercms_ai_types.Content>;
61
63
  listContent(opts?: Parameters<typeof listContent>[1]): Promise<_bettercms_ai_types.PaginatedResult<_bettercms_ai_types.Content>>;
@@ -71,8 +73,13 @@ declare class BetterCMSDeliveryClient {
71
73
  /** Build request headers, including auth if provided. */
72
74
  headers(): Record<string, string>;
73
75
  /**
74
- * Generic fetch helper — wraps a request with network-error handling
75
- * and throws a typed BetterCMSError on non-OK responses.
76
+ * Generic fetch helper — retries 429/503 honouring Retry-After, applies a
77
+ * per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
78
+ *
79
+ * This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
80
+ * 429 from the delivery limiter threw straight through and killed the caller. In a
81
+ * static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
82
+ * never going to be enough against a 60-second fixed window.
76
83
  */
77
84
  fetchJSON<T>(url: string): Promise<T>;
78
85
  }
@@ -139,7 +146,7 @@ interface WebhookListResponse {
139
146
  *
140
147
  * Extends BetterCMSDeliveryClient but overrides:
141
148
  * - `headers()` — uses Bearer token auth instead of API key
142
- * - `fetchJSON()` — adds retry on 429/503 and 10s timeout per request
149
+ * - `fetchJSON()` — adds Retry-After-aware retry on 429/503 (see ./retry.ts)
143
150
  *
144
151
  * @see openspec/changes/flo-6-backend-sdk-admin/specs/sdk-admin-client/spec.md
145
152
  */
@@ -158,13 +165,12 @@ interface BetterCMSAdminOptions {
158
165
  * fetchJSON() for Bearer auth + retry/timeout.
159
166
  */
160
167
  declare class BetterCMSAdminClient extends BetterCMSDeliveryClient {
161
- readonly timeout: number;
162
168
  private readonly _token;
163
169
  constructor(options: BetterCMSAdminOptions);
164
170
  headers(): Record<string, string>;
165
171
  /**
166
- * Admin fetchJSON with retry (429, 503) and per-request timeout (AbortController).
167
- * Each retry gets a fresh timeout.
172
+ * Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
173
+ * timeout (AbortController). Each retry gets a fresh timeout.
168
174
  */
169
175
  fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
170
176
  /** Build the admin API URL. */
@@ -741,7 +747,11 @@ declare class BetterCMSManagementClient extends BetterCMSDeliveryClient {
741
747
  headers(): Record<string, string>;
742
748
  /** Management API URL: baseUrl + path (path is workspace-scoped via the key). */
743
749
  url(path: string): string;
744
- /** fetchJSON with retry (429, 503) + per-request timeout (mirrors the admin client). */
750
+ /**
751
+ * fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
752
+ * (mirrors the admin client). See ./retry.ts for why the delay must be able
753
+ * to outlast the backend's 60-second rate-limit window.
754
+ */
745
755
  fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
746
756
  listModels(): Promise<ManagedContentModel[]>;
747
757
  getModel(id: string): Promise<ManagedContentModel>;
@@ -864,6 +874,12 @@ interface BetterCMSSiteOptions {
864
874
  apiKey?: string;
865
875
  /** Base URL of the Delivery API. Defaults to https://api.bettercms.ai/v1. */
866
876
  baseUrl?: string;
877
+ /**
878
+ * Per-attempt request timeout in ms. Defaults to 10000. Each of the 3 retries gets
879
+ * a fresh one. Raise it for a slow delivery origin rather than losing the timeout —
880
+ * without one, a hung socket stalls a build indefinitely.
881
+ */
882
+ timeout?: number;
867
883
  }
868
884
  /**
869
885
  * BetterCMS factory — returns a typed client for the Delivery API.
@@ -931,6 +947,21 @@ interface CreateClientOptions {
931
947
  stega?: boolean;
932
948
  /** Per-attempt timeout in ms. Defaults to 10000. */
933
949
  timeout?: number;
950
+ /**
951
+ * Collapse identical GETs to one network round-trip for the life of this client.
952
+ * Default false.
953
+ *
954
+ * Only correct where the content is a POINT-IN-TIME SNAPSHOT — i.e. a static build,
955
+ * where every route renders from the same published dataset. `@bettercms-ai/astro`
956
+ * turns this on automatically when Astro's `command === "build"` and never for `dev`,
957
+ * SSR or the drafts perspective, where a repeat read must see the current row.
958
+ *
959
+ * A static build without it re-fetches the same nav/settings/collection once PER ROUTE.
960
+ * That is what made a large site exhaust the delivery rate limit: hundreds of identical
961
+ * requests, and `getPage()` re-scanning every page of the paginated list each time it is
962
+ * called.
963
+ */
964
+ cache?: boolean;
934
965
  }
935
966
  interface ListEntriesOptions {
936
967
  /** Filter by content-model slug. */
@@ -1048,6 +1079,24 @@ interface RichTextValue {
1048
1079
  */
1049
1080
  readonly doc?: DocValue;
1050
1081
  }
1082
+ /** The marker on a Portable Text envelope. `format` is a plain string, never a union — see below. */
1083
+ declare const PORTABLE_TEXT_FORMAT = "portable-text-1";
1084
+ /**
1085
+ * Structured blocks, when the field stores Portable Text.
1086
+ *
1087
+ * ── The compatibility guarantee, stated once ────────────────────────────────────────────
1088
+ * Portable Text did NOT change this envelope. `format`, `value` and `html` are the same three
1089
+ * keys they always were, so `isRichText`, `plain()` and `rich()` keep working untouched — and
1090
+ * they have to, because sites compiled months ago read `.html` directly and cannot be
1091
+ * rebuilt. `bettercms-starter-modern/lib/cms.ts` even carries its OWN copy of `rich()`, baked
1092
+ * into deployments nobody can reach. Storing a bare array instead would have made
1093
+ * `isRichText` return false (it rejects arrays), every shipped `rich()` call return "", and
1094
+ * every one of those pages go blank at 200 OK.
1095
+ *
1096
+ * `portableText()` is therefore ADDITIVE: `html` remains the always-present field, and this is
1097
+ * the opt-in for consumers that want to render structure with `@portabletext/react` instead.
1098
+ */
1099
+ declare function portableText(value: TextOrRich): readonly unknown[] | null;
1051
1100
  /**
1052
1101
  * A field that may arrive as either shape. Type author-editable text fields with this and the
1053
1102
  * compiler routes you through `plain()`/`rich()`, so a type switch in the CMS can never reach
@@ -1362,4 +1411,4 @@ declare function inviteMember(client: BetterCMSAdminClient, workspaceId: string,
1362
1411
  declare function updateMemberRole(client: BetterCMSAdminClient, workspaceId: string, memberId: string, role: MemberRole): Promise<Member>;
1363
1412
  declare function removeMember(client: BetterCMSAdminClient, workspaceId: string, memberId: string): Promise<void>;
1364
1413
 
1365
- export { type AddPageFieldsInput, type ApiKeyUsage, type ApiKeyWithRaw, type AuthResult, BetterCMSError as BCMSClientError, ErrorCodes as BCMSErrorCodes, BetterCMS, BetterCMSAdminClient, type BetterCMSAdminOptions, BetterCMSDeliveryClient, BetterCMSError, BetterCMSManagementClient, type BetterCMSManagementOptions, type BetterCMSReadClient, type BetterCMSSiteOptions, type CommandManagedLayoutInput, type CreateApiKeyInput, type CreateClientOptions, type CreateEntryInput, type CreateFormInput, type CreateManagedPageInput, type CreateModelInput, type CreatePageInput, type CreateWorkspaceInput, type DeliveryForm, type DeliveryFormField, type DeliveryFormFieldType, ErrorCodes, type ExtractionCandidate, type FormFieldInput, type FormListOptions, type FormSubmitError, type FormValue, type FormValues, type GetEntryOptions, type GetManagedLayoutOptions, type ListContentAllOptions, type ListContentOptions, type ListEntriesFilter, type ListEntriesOptions, type ListPagesOptions, type ManagedComponent, type ManagedComponentInput, type ManagedContentEntry, type ManagedContentModel, type ManagedForm, type ManagedFormInput, type ManagedLayoutClient, type ManagedLayoutDocument, type ManagedPage, type ManagementClient, type MediaListOptions, type MediaMetadata, type Member, type MemberRole, type PageListOptions, type PendingInvite, type ResolvedSeo, type RichTextValue, type SearchHit, type SearchOptions, type SeoInput, type SeoLocation, type SeoMeta, type SeoMetaInput, type SetPageContentInput, type StegaPayload, type SubmissionListOptions, type SubmitFormOptions, type SubmitFormResult, type TextOrRich, type UpdateApiKeyInput, type UpdateEntryInput, type UpdateFormInput, type UpdateModelInput, type UpdatePageInput, type UpdateWorkspaceInput, type UploadAssetInput, type UploadedAsset, type WriteContentInput, addModelFields, addPageFields, asStringArray, commandManagedLayout, createApiKey, createClient, createEntry, createForm, createManagedPage, createModel, createPage, createWorkspace, decodeStega, deleteForm, deleteMedia, deletePage, deleteSubmission, deleteWorkspace, encodeStega, formInitialValues, getApiKeyUsage, getEntry, getForm, getManagedLayout, getManagedPage, getMedia, getMember, getModel, getPage, getSubmission, getWorkspace, inviteMember, isMultiValueField, isRichText, listApiKeys, listEntries, listForms, listManagedPages, listMedia, listMembers, listModels, listPages, listSubmissions, listWorkspaces, plain, publishPage, regenerateApiKey, removeMember, resolveSeo, revokeApiKey, rich, search, setPageContent, shouldShowField, signIn, signInWithGithub, signInWithGoogle, signUp, stripStega, submitForm, toggleOption, updateApiKey, updateEntry, updateForm, updateMemberRole, updateModel, updatePage, updateWorkspace, uploadMedia };
1414
+ export { type AddPageFieldsInput, type ApiKeyUsage, type ApiKeyWithRaw, type AuthResult, BetterCMSError as BCMSClientError, ErrorCodes as BCMSErrorCodes, BetterCMS, BetterCMSAdminClient, type BetterCMSAdminOptions, BetterCMSDeliveryClient, BetterCMSError, BetterCMSManagementClient, type BetterCMSManagementOptions, type BetterCMSReadClient, type BetterCMSSiteOptions, type CommandManagedLayoutInput, type CreateApiKeyInput, type CreateClientOptions, type CreateEntryInput, type CreateFormInput, type CreateManagedPageInput, type CreateModelInput, type CreatePageInput, type CreateWorkspaceInput, type DeliveryForm, type DeliveryFormField, type DeliveryFormFieldType, ErrorCodes, type ExtractionCandidate, type FormFieldInput, type FormListOptions, type FormSubmitError, type FormValue, type FormValues, type GetEntryOptions, type GetManagedLayoutOptions, type ListContentAllOptions, type ListContentOptions, type ListEntriesFilter, type ListEntriesOptions, type ListPagesOptions, type ManagedComponent, type ManagedComponentInput, type ManagedContentEntry, type ManagedContentModel, type ManagedForm, type ManagedFormInput, type ManagedLayoutClient, type ManagedLayoutDocument, type ManagedPage, type ManagementClient, type MediaListOptions, type MediaMetadata, type Member, type MemberRole, PORTABLE_TEXT_FORMAT, type PageListOptions, type PendingInvite, type ResolvedSeo, type RichTextValue, type SearchHit, type SearchOptions, type SeoInput, type SeoLocation, type SeoMeta, type SeoMetaInput, type SetPageContentInput, type StegaPayload, type SubmissionListOptions, type SubmitFormOptions, type SubmitFormResult, type TextOrRich, type UpdateApiKeyInput, type UpdateEntryInput, type UpdateFormInput, type UpdateModelInput, type UpdatePageInput, type UpdateWorkspaceInput, type UploadAssetInput, type UploadedAsset, type WriteContentInput, addModelFields, addPageFields, asStringArray, commandManagedLayout, createApiKey, createClient, createEntry, createForm, createManagedPage, createModel, createPage, createWorkspace, decodeStega, deleteForm, deleteMedia, deletePage, deleteSubmission, deleteWorkspace, encodeStega, formInitialValues, getApiKeyUsage, getEntry, getForm, getManagedLayout, getManagedPage, getMedia, getMember, getModel, getPage, getSubmission, getWorkspace, inviteMember, isMultiValueField, isRichText, listApiKeys, listEntries, listForms, listManagedPages, listMedia, listMembers, listModels, listPages, listSubmissions, listWorkspaces, plain, portableText, publishPage, regenerateApiKey, removeMember, resolveSeo, revokeApiKey, rich, search, setPageContent, shouldShowField, signIn, signInWithGithub, signInWithGoogle, signUp, stripStega, submitForm, toggleOption, updateApiKey, updateEntry, updateForm, updateMemberRole, updateModel, updatePage, updateWorkspace, uploadMedia };
package/dist/index.d.ts CHANGED
@@ -51,11 +51,13 @@ declare function listContentAll(client: BetterCMSDeliveryClient, opts?: ListCont
51
51
  declare class BetterCMSDeliveryClient {
52
52
  readonly workspace: string;
53
53
  readonly baseUrl: string;
54
+ protected readonly timeout: number;
54
55
  private readonly apiKey?;
55
56
  constructor(opts: {
56
57
  workspace: string;
57
58
  apiKey?: string;
58
59
  baseUrl: string;
60
+ timeout?: number;
59
61
  });
60
62
  getContent(slug: string): Promise<_bettercms_ai_types.Content>;
61
63
  listContent(opts?: Parameters<typeof listContent>[1]): Promise<_bettercms_ai_types.PaginatedResult<_bettercms_ai_types.Content>>;
@@ -71,8 +73,13 @@ declare class BetterCMSDeliveryClient {
71
73
  /** Build request headers, including auth if provided. */
72
74
  headers(): Record<string, string>;
73
75
  /**
74
- * Generic fetch helper — wraps a request with network-error handling
75
- * and throws a typed BetterCMSError on non-OK responses.
76
+ * Generic fetch helper — retries 429/503 honouring Retry-After, applies a
77
+ * per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
78
+ *
79
+ * This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
80
+ * 429 from the delivery limiter threw straight through and killed the caller. In a
81
+ * static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
82
+ * never going to be enough against a 60-second fixed window.
76
83
  */
77
84
  fetchJSON<T>(url: string): Promise<T>;
78
85
  }
@@ -139,7 +146,7 @@ interface WebhookListResponse {
139
146
  *
140
147
  * Extends BetterCMSDeliveryClient but overrides:
141
148
  * - `headers()` — uses Bearer token auth instead of API key
142
- * - `fetchJSON()` — adds retry on 429/503 and 10s timeout per request
149
+ * - `fetchJSON()` — adds Retry-After-aware retry on 429/503 (see ./retry.ts)
143
150
  *
144
151
  * @see openspec/changes/flo-6-backend-sdk-admin/specs/sdk-admin-client/spec.md
145
152
  */
@@ -158,13 +165,12 @@ interface BetterCMSAdminOptions {
158
165
  * fetchJSON() for Bearer auth + retry/timeout.
159
166
  */
160
167
  declare class BetterCMSAdminClient extends BetterCMSDeliveryClient {
161
- readonly timeout: number;
162
168
  private readonly _token;
163
169
  constructor(options: BetterCMSAdminOptions);
164
170
  headers(): Record<string, string>;
165
171
  /**
166
- * Admin fetchJSON with retry (429, 503) and per-request timeout (AbortController).
167
- * Each retry gets a fresh timeout.
172
+ * Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
173
+ * timeout (AbortController). Each retry gets a fresh timeout.
168
174
  */
169
175
  fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
170
176
  /** Build the admin API URL. */
@@ -741,7 +747,11 @@ declare class BetterCMSManagementClient extends BetterCMSDeliveryClient {
741
747
  headers(): Record<string, string>;
742
748
  /** Management API URL: baseUrl + path (path is workspace-scoped via the key). */
743
749
  url(path: string): string;
744
- /** fetchJSON with retry (429, 503) + per-request timeout (mirrors the admin client). */
750
+ /**
751
+ * fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
752
+ * (mirrors the admin client). See ./retry.ts for why the delay must be able
753
+ * to outlast the backend's 60-second rate-limit window.
754
+ */
745
755
  fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
746
756
  listModels(): Promise<ManagedContentModel[]>;
747
757
  getModel(id: string): Promise<ManagedContentModel>;
@@ -864,6 +874,12 @@ interface BetterCMSSiteOptions {
864
874
  apiKey?: string;
865
875
  /** Base URL of the Delivery API. Defaults to https://api.bettercms.ai/v1. */
866
876
  baseUrl?: string;
877
+ /**
878
+ * Per-attempt request timeout in ms. Defaults to 10000. Each of the 3 retries gets
879
+ * a fresh one. Raise it for a slow delivery origin rather than losing the timeout —
880
+ * without one, a hung socket stalls a build indefinitely.
881
+ */
882
+ timeout?: number;
867
883
  }
868
884
  /**
869
885
  * BetterCMS factory — returns a typed client for the Delivery API.
@@ -931,6 +947,21 @@ interface CreateClientOptions {
931
947
  stega?: boolean;
932
948
  /** Per-attempt timeout in ms. Defaults to 10000. */
933
949
  timeout?: number;
950
+ /**
951
+ * Collapse identical GETs to one network round-trip for the life of this client.
952
+ * Default false.
953
+ *
954
+ * Only correct where the content is a POINT-IN-TIME SNAPSHOT — i.e. a static build,
955
+ * where every route renders from the same published dataset. `@bettercms-ai/astro`
956
+ * turns this on automatically when Astro's `command === "build"` and never for `dev`,
957
+ * SSR or the drafts perspective, where a repeat read must see the current row.
958
+ *
959
+ * A static build without it re-fetches the same nav/settings/collection once PER ROUTE.
960
+ * That is what made a large site exhaust the delivery rate limit: hundreds of identical
961
+ * requests, and `getPage()` re-scanning every page of the paginated list each time it is
962
+ * called.
963
+ */
964
+ cache?: boolean;
934
965
  }
935
966
  interface ListEntriesOptions {
936
967
  /** Filter by content-model slug. */
@@ -1048,6 +1079,24 @@ interface RichTextValue {
1048
1079
  */
1049
1080
  readonly doc?: DocValue;
1050
1081
  }
1082
+ /** The marker on a Portable Text envelope. `format` is a plain string, never a union — see below. */
1083
+ declare const PORTABLE_TEXT_FORMAT = "portable-text-1";
1084
+ /**
1085
+ * Structured blocks, when the field stores Portable Text.
1086
+ *
1087
+ * ── The compatibility guarantee, stated once ────────────────────────────────────────────
1088
+ * Portable Text did NOT change this envelope. `format`, `value` and `html` are the same three
1089
+ * keys they always were, so `isRichText`, `plain()` and `rich()` keep working untouched — and
1090
+ * they have to, because sites compiled months ago read `.html` directly and cannot be
1091
+ * rebuilt. `bettercms-starter-modern/lib/cms.ts` even carries its OWN copy of `rich()`, baked
1092
+ * into deployments nobody can reach. Storing a bare array instead would have made
1093
+ * `isRichText` return false (it rejects arrays), every shipped `rich()` call return "", and
1094
+ * every one of those pages go blank at 200 OK.
1095
+ *
1096
+ * `portableText()` is therefore ADDITIVE: `html` remains the always-present field, and this is
1097
+ * the opt-in for consumers that want to render structure with `@portabletext/react` instead.
1098
+ */
1099
+ declare function portableText(value: TextOrRich): readonly unknown[] | null;
1051
1100
  /**
1052
1101
  * A field that may arrive as either shape. Type author-editable text fields with this and the
1053
1102
  * compiler routes you through `plain()`/`rich()`, so a type switch in the CMS can never reach
@@ -1362,4 +1411,4 @@ declare function inviteMember(client: BetterCMSAdminClient, workspaceId: string,
1362
1411
  declare function updateMemberRole(client: BetterCMSAdminClient, workspaceId: string, memberId: string, role: MemberRole): Promise<Member>;
1363
1412
  declare function removeMember(client: BetterCMSAdminClient, workspaceId: string, memberId: string): Promise<void>;
1364
1413
 
1365
- export { type AddPageFieldsInput, type ApiKeyUsage, type ApiKeyWithRaw, type AuthResult, BetterCMSError as BCMSClientError, ErrorCodes as BCMSErrorCodes, BetterCMS, BetterCMSAdminClient, type BetterCMSAdminOptions, BetterCMSDeliveryClient, BetterCMSError, BetterCMSManagementClient, type BetterCMSManagementOptions, type BetterCMSReadClient, type BetterCMSSiteOptions, type CommandManagedLayoutInput, type CreateApiKeyInput, type CreateClientOptions, type CreateEntryInput, type CreateFormInput, type CreateManagedPageInput, type CreateModelInput, type CreatePageInput, type CreateWorkspaceInput, type DeliveryForm, type DeliveryFormField, type DeliveryFormFieldType, ErrorCodes, type ExtractionCandidate, type FormFieldInput, type FormListOptions, type FormSubmitError, type FormValue, type FormValues, type GetEntryOptions, type GetManagedLayoutOptions, type ListContentAllOptions, type ListContentOptions, type ListEntriesFilter, type ListEntriesOptions, type ListPagesOptions, type ManagedComponent, type ManagedComponentInput, type ManagedContentEntry, type ManagedContentModel, type ManagedForm, type ManagedFormInput, type ManagedLayoutClient, type ManagedLayoutDocument, type ManagedPage, type ManagementClient, type MediaListOptions, type MediaMetadata, type Member, type MemberRole, type PageListOptions, type PendingInvite, type ResolvedSeo, type RichTextValue, type SearchHit, type SearchOptions, type SeoInput, type SeoLocation, type SeoMeta, type SeoMetaInput, type SetPageContentInput, type StegaPayload, type SubmissionListOptions, type SubmitFormOptions, type SubmitFormResult, type TextOrRich, type UpdateApiKeyInput, type UpdateEntryInput, type UpdateFormInput, type UpdateModelInput, type UpdatePageInput, type UpdateWorkspaceInput, type UploadAssetInput, type UploadedAsset, type WriteContentInput, addModelFields, addPageFields, asStringArray, commandManagedLayout, createApiKey, createClient, createEntry, createForm, createManagedPage, createModel, createPage, createWorkspace, decodeStega, deleteForm, deleteMedia, deletePage, deleteSubmission, deleteWorkspace, encodeStega, formInitialValues, getApiKeyUsage, getEntry, getForm, getManagedLayout, getManagedPage, getMedia, getMember, getModel, getPage, getSubmission, getWorkspace, inviteMember, isMultiValueField, isRichText, listApiKeys, listEntries, listForms, listManagedPages, listMedia, listMembers, listModels, listPages, listSubmissions, listWorkspaces, plain, publishPage, regenerateApiKey, removeMember, resolveSeo, revokeApiKey, rich, search, setPageContent, shouldShowField, signIn, signInWithGithub, signInWithGoogle, signUp, stripStega, submitForm, toggleOption, updateApiKey, updateEntry, updateForm, updateMemberRole, updateModel, updatePage, updateWorkspace, uploadMedia };
1414
+ export { type AddPageFieldsInput, type ApiKeyUsage, type ApiKeyWithRaw, type AuthResult, BetterCMSError as BCMSClientError, ErrorCodes as BCMSErrorCodes, BetterCMS, BetterCMSAdminClient, type BetterCMSAdminOptions, BetterCMSDeliveryClient, BetterCMSError, BetterCMSManagementClient, type BetterCMSManagementOptions, type BetterCMSReadClient, type BetterCMSSiteOptions, type CommandManagedLayoutInput, type CreateApiKeyInput, type CreateClientOptions, type CreateEntryInput, type CreateFormInput, type CreateManagedPageInput, type CreateModelInput, type CreatePageInput, type CreateWorkspaceInput, type DeliveryForm, type DeliveryFormField, type DeliveryFormFieldType, ErrorCodes, type ExtractionCandidate, type FormFieldInput, type FormListOptions, type FormSubmitError, type FormValue, type FormValues, type GetEntryOptions, type GetManagedLayoutOptions, type ListContentAllOptions, type ListContentOptions, type ListEntriesFilter, type ListEntriesOptions, type ListPagesOptions, type ManagedComponent, type ManagedComponentInput, type ManagedContentEntry, type ManagedContentModel, type ManagedForm, type ManagedFormInput, type ManagedLayoutClient, type ManagedLayoutDocument, type ManagedPage, type ManagementClient, type MediaListOptions, type MediaMetadata, type Member, type MemberRole, PORTABLE_TEXT_FORMAT, type PageListOptions, type PendingInvite, type ResolvedSeo, type RichTextValue, type SearchHit, type SearchOptions, type SeoInput, type SeoLocation, type SeoMeta, type SeoMetaInput, type SetPageContentInput, type StegaPayload, type SubmissionListOptions, type SubmitFormOptions, type SubmitFormResult, type TextOrRich, type UpdateApiKeyInput, type UpdateEntryInput, type UpdateFormInput, type UpdateModelInput, type UpdatePageInput, type UpdateWorkspaceInput, type UploadAssetInput, type UploadedAsset, type WriteContentInput, addModelFields, addPageFields, asStringArray, commandManagedLayout, createApiKey, createClient, createEntry, createForm, createManagedPage, createModel, createPage, createWorkspace, decodeStega, deleteForm, deleteMedia, deletePage, deleteSubmission, deleteWorkspace, encodeStega, formInitialValues, getApiKeyUsage, getEntry, getForm, getManagedLayout, getManagedPage, getMedia, getMember, getModel, getPage, getSubmission, getWorkspace, inviteMember, isMultiValueField, isRichText, listApiKeys, listEntries, listForms, listManagedPages, listMedia, listMembers, listModels, listPages, listSubmissions, listWorkspaces, plain, portableText, publishPage, regenerateApiKey, removeMember, resolveSeo, revokeApiKey, rich, search, setPageContent, shouldShowField, signIn, signInWithGithub, signInWithGoogle, signUp, stripStega, submitForm, toggleOption, updateApiKey, updateEntry, updateForm, updateMemberRole, updateModel, updatePage, updateWorkspace, uploadMedia };
package/dist/index.js CHANGED
@@ -89,6 +89,35 @@ var BetterCMSError = class _BetterCMSError extends Error {
89
89
  }
90
90
  };
91
91
 
92
+ // src/retry.ts
93
+ var RETRY_STATUSES = [429, 503];
94
+ var MAX_RETRIES = 3;
95
+ var MAX_DELAY_MS = 65e3;
96
+ var BASE_DELAY_MS = 1e3;
97
+ var JITTER_MS = 250;
98
+ function isRetryableStatus(status) {
99
+ return RETRY_STATUSES.includes(status);
100
+ }
101
+ function retryAfterMs(res) {
102
+ const raw = res.headers?.get?.("retry-after");
103
+ if (!raw) return null;
104
+ const seconds = Number(raw);
105
+ if (Number.isFinite(seconds)) {
106
+ return Math.min(Math.max(seconds, 0) * 1e3, MAX_DELAY_MS);
107
+ }
108
+ const at = Date.parse(raw);
109
+ if (!Number.isNaN(at)) {
110
+ return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);
111
+ }
112
+ return null;
113
+ }
114
+ function backoffMs(attempt, res) {
115
+ const fromServer = res ? retryAfterMs(res) : null;
116
+ const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);
117
+ return base + Math.random() * JITTER_MS;
118
+ }
119
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
120
+
92
121
  // src/methods/getContent.ts
93
122
  async function getContent(client, slug) {
94
123
  const data = await client.fetchJSON(client.url(slug));
@@ -153,14 +182,17 @@ function listContentAll(client, opts) {
153
182
  }
154
183
 
155
184
  // src/delivery-client.ts
185
+ var DEFAULT_TIMEOUT = 1e4;
156
186
  var BetterCMSDeliveryClient = class {
157
187
  workspace;
158
188
  baseUrl;
189
+ timeout;
159
190
  apiKey;
160
191
  constructor(opts) {
161
192
  this.workspace = opts.workspace;
162
193
  this.baseUrl = opts.baseUrl;
163
194
  this.apiKey = opts.apiKey;
195
+ this.timeout = opts.timeout ?? DEFAULT_TIMEOUT;
164
196
  }
165
197
  getContent(slug) {
166
198
  return getContent(this, slug);
@@ -193,24 +225,46 @@ var BetterCMSDeliveryClient = class {
193
225
  return headers;
194
226
  }
195
227
  /**
196
- * Generic fetch helper — wraps a request with network-error handling
197
- * and throws a typed BetterCMSError on non-OK responses.
228
+ * Generic fetch helper — retries 429/503 honouring Retry-After, applies a
229
+ * per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
230
+ *
231
+ * This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
232
+ * 429 from the delivery limiter threw straight through and killed the caller. In a
233
+ * static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
234
+ * never going to be enough against a 60-second fixed window.
198
235
  */
199
236
  async fetchJSON(url) {
200
- let res;
201
- try {
202
- res = await globalThis.fetch(url, { headers: this.headers() });
203
- } catch (err) {
204
- throw new BetterCMSError(
205
- `Network error: ${err instanceof Error ? err.message : String(err)}`,
206
- 0,
207
- "NETWORK_ERROR"
208
- );
209
- }
210
- if (!res.ok) {
237
+ for (let attempt = 0; ; attempt++) {
238
+ const controller = new AbortController();
239
+ const timer = setTimeout(() => controller.abort(), this.timeout);
240
+ let res;
241
+ try {
242
+ res = await globalThis.fetch(url, {
243
+ headers: this.headers(),
244
+ signal: controller.signal
245
+ });
246
+ } catch (err) {
247
+ const timedOut = err instanceof Error && err.name === "AbortError";
248
+ if (attempt < MAX_RETRIES) {
249
+ await sleep(backoffMs(attempt));
250
+ continue;
251
+ }
252
+ if (timedOut) throw new BetterCMSError("Request timeout", 408, "NETWORK_ERROR");
253
+ throw new BetterCMSError(
254
+ `Network error: ${err instanceof Error ? err.message : String(err)}`,
255
+ 0,
256
+ "NETWORK_ERROR"
257
+ );
258
+ } finally {
259
+ clearTimeout(timer);
260
+ }
261
+ if (res.ok) return await res.json();
262
+ if (attempt < MAX_RETRIES && isRetryableStatus(res.status)) {
263
+ await sleep(backoffMs(attempt, res));
264
+ continue;
265
+ }
211
266
  throw await BetterCMSError.from(res);
212
267
  }
213
- return await res.json();
214
268
  }
215
269
  };
216
270
 
@@ -291,19 +345,15 @@ function addWebhookMethods(client) {
291
345
 
292
346
  // src/admin-client.ts
293
347
  var DEFAULT_BASE_URL = "https://api.bettercms.ai/v1";
294
- var DEFAULT_TIMEOUT = 1e4;
295
- var RETRY_STATUSES = [429, 503];
296
- var RETRY_DELAYS = [1e3, 2e3, 4e3];
297
348
  var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
298
- timeout;
299
349
  _token;
300
350
  constructor(options) {
301
351
  super({
302
352
  workspace: "",
303
- baseUrl: options.baseUrl ?? DEFAULT_BASE_URL
353
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
354
+ timeout: options.timeout
304
355
  });
305
356
  this._token = options.token;
306
- this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
307
357
  }
308
358
  headers() {
309
359
  return {
@@ -313,12 +363,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
313
363
  };
314
364
  }
315
365
  /**
316
- * Admin fetchJSON with retry (429, 503) and per-request timeout (AbortController).
317
- * Each retry gets a fresh timeout.
366
+ * Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
367
+ * timeout (AbortController). Each retry gets a fresh timeout.
318
368
  */
319
369
  async fetchJSON(url, init) {
320
370
  let lastError;
321
- for (let attempt = 0; attempt <= 3; attempt++) {
371
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
322
372
  const controller = new AbortController();
323
373
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
324
374
  let nonRetryable = false;
@@ -332,12 +382,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
332
382
  if (res.ok) {
333
383
  return res.json();
334
384
  }
335
- const shouldRetry = attempt < 3 && RETRY_STATUSES.includes(res.status);
385
+ const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
336
386
  if (!shouldRetry) {
337
387
  nonRetryable = true;
338
388
  throw await BetterCMSError.from(res);
339
389
  }
340
- await sleep(RETRY_DELAYS[attempt] ?? 0);
390
+ await sleep(backoffMs(attempt, res));
341
391
  } catch (err) {
342
392
  clearTimeout(timeoutId);
343
393
  if (err instanceof Error && err.name === "AbortError") {
@@ -347,8 +397,8 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
347
397
  throw err;
348
398
  }
349
399
  lastError = err;
350
- if (attempt < 3) {
351
- await sleep(RETRY_DELAYS[attempt] ?? 0);
400
+ if (attempt < MAX_RETRIES) {
401
+ await sleep(backoffMs(attempt));
352
402
  continue;
353
403
  }
354
404
  throw err;
@@ -365,9 +415,6 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
365
415
  return addWebhookMethods(this);
366
416
  }
367
417
  };
368
- function sleep(ms) {
369
- return new Promise((resolve) => setTimeout(resolve, ms));
370
- }
371
418
 
372
419
  // src/methods/management/content.ts
373
420
  async function listModels(client) {
@@ -508,7 +555,14 @@ var MIME_BY_EXT = {
508
555
  avif: "image/avif",
509
556
  mp4: "video/mp4",
510
557
  webm: "video/webm",
511
- pdf: "application/pdf"
558
+ pdf: "application/pdf",
559
+ // mp3/ogg were server-allowed all along; the SDK sent application/octet-stream for them,
560
+ // which the allowlist then refused — so an agent could never upload audio.
561
+ mp3: "audio/mpeg",
562
+ ogg: "audio/ogg",
563
+ txt: "text/plain",
564
+ md: "text/markdown",
565
+ markdown: "text/markdown"
512
566
  };
513
567
  function basenameOf(p) {
514
568
  const clean = p.split(/[?#]/)[0] ?? p;
@@ -737,8 +791,6 @@ async function commandManagedLayout(client, input) {
737
791
  // src/management-client.ts
738
792
  var DEFAULT_BASE_URL2 = "https://api.bettercms.ai/v1";
739
793
  var DEFAULT_TIMEOUT2 = 1e4;
740
- var RETRY_STATUSES2 = [429, 503];
741
- var RETRY_DELAYS2 = [1e3, 2e3, 4e3];
742
794
  var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
743
795
  timeout;
744
796
  _apiKey;
@@ -760,10 +812,14 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
760
812
  url(path) {
761
813
  return `${this.baseUrl}${path}`;
762
814
  }
763
- /** fetchJSON with retry (429, 503) + per-request timeout (mirrors the admin client). */
815
+ /**
816
+ * fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
817
+ * (mirrors the admin client). See ./retry.ts for why the delay must be able
818
+ * to outlast the backend's 60-second rate-limit window.
819
+ */
764
820
  async fetchJSON(url, init) {
765
821
  let lastError;
766
- for (let attempt = 0; attempt <= 3; attempt++) {
822
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
767
823
  const controller = new AbortController();
768
824
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
769
825
  let nonRetryable = false;
@@ -777,12 +833,12 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
777
833
  if (res.ok) {
778
834
  return res.json();
779
835
  }
780
- const shouldRetry = attempt < 3 && RETRY_STATUSES2.includes(res.status);
836
+ const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
781
837
  if (!shouldRetry) {
782
838
  nonRetryable = true;
783
839
  throw await BetterCMSError.from(res);
784
840
  }
785
- await sleep2(RETRY_DELAYS2[attempt] ?? 0);
841
+ await sleep(backoffMs(attempt, res));
786
842
  } catch (err) {
787
843
  clearTimeout(timeoutId);
788
844
  if (err instanceof Error && err.name === "AbortError") {
@@ -790,8 +846,8 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
790
846
  }
791
847
  if (nonRetryable) throw err;
792
848
  lastError = err;
793
- if (attempt < 3) {
794
- await sleep2(RETRY_DELAYS2[attempt] ?? 0);
849
+ if (attempt < MAX_RETRIES) {
850
+ await sleep(backoffMs(attempt));
795
851
  continue;
796
852
  }
797
853
  throw err;
@@ -923,9 +979,6 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
923
979
  return generateSeoMeta(this, input);
924
980
  }
925
981
  };
926
- function sleep2(ms) {
927
- return new Promise((resolve) => setTimeout(resolve, ms));
928
- }
929
982
 
930
983
  // src/auth.ts
931
984
  var AUTH_BASE_URL = "https://api.bettercms.ai/api/v1/auth";
@@ -1016,7 +1069,8 @@ var BetterCMS = {
1016
1069
  return new BetterCMSDeliveryClient({
1017
1070
  workspace: options.workspace,
1018
1071
  apiKey: options.apiKey,
1019
- baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3
1072
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3,
1073
+ timeout: options.timeout
1020
1074
  });
1021
1075
  },
1022
1076
  /**
@@ -1048,10 +1102,7 @@ var BetterCMS = {
1048
1102
  };
1049
1103
 
1050
1104
  // src/read-client.ts
1051
- var RETRY_STATUSES3 = [429, 503];
1052
- var RETRY_DELAYS3 = [250, 750, 1500];
1053
1105
  var DEFAULT_TIMEOUT3 = 1e4;
1054
- var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1055
1106
  function qs(params2) {
1056
1107
  const sp = new URLSearchParams();
1057
1108
  for (const [k, v] of Object.entries(params2)) {
@@ -1068,6 +1119,27 @@ function unwrap(json) {
1068
1119
  }
1069
1120
  return json;
1070
1121
  }
1122
+ function createRequestCache() {
1123
+ const inflight = /* @__PURE__ */ new Map();
1124
+ const settled = /* @__PURE__ */ new Map();
1125
+ const copy = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
1126
+ return async function collapse(url, fetcher) {
1127
+ if (settled.has(url)) return copy(settled.get(url));
1128
+ const pending = inflight.get(url);
1129
+ if (pending) return copy(await pending);
1130
+ const run = (async () => {
1131
+ try {
1132
+ const value = await fetcher();
1133
+ settled.set(url, value);
1134
+ return value;
1135
+ } finally {
1136
+ inflight.delete(url);
1137
+ }
1138
+ })();
1139
+ inflight.set(url, run);
1140
+ return copy(await run);
1141
+ };
1142
+ }
1071
1143
  function createClient(options) {
1072
1144
  const apiUrl = options.apiUrl.replace(/\/+$/, "");
1073
1145
  const { workspace, apiKey, perspective = "published", previewToken } = options;
@@ -1079,9 +1151,11 @@ function createClient(options) {
1079
1151
  if (apiKey) h["X-API-Key"] = apiKey;
1080
1152
  return h;
1081
1153
  };
1082
- async function fetchJSON(url) {
1154
+ const collapse = options.cache && perspective === "published" ? createRequestCache() : void 0;
1155
+ const fetchJSON = (url) => collapse ? collapse(url, () => request(url)) : request(url);
1156
+ async function request(url) {
1083
1157
  let lastError;
1084
- for (let attempt = 0; attempt <= RETRY_DELAYS3.length; attempt++) {
1158
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
1085
1159
  const controller = new AbortController();
1086
1160
  const timer = setTimeout(() => controller.abort(), timeout);
1087
1161
  try {
@@ -1091,9 +1165,9 @@ function createClient(options) {
1091
1165
  });
1092
1166
  clearTimeout(timer);
1093
1167
  if (res.ok) return await res.json();
1094
- const retryable = attempt < RETRY_DELAYS3.length && RETRY_STATUSES3.includes(res.status);
1168
+ const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
1095
1169
  if (!retryable) throw await BetterCMSError.from(res);
1096
- await sleep3(RETRY_DELAYS3[attempt] ?? 0);
1170
+ await sleep(backoffMs(attempt, res));
1097
1171
  } catch (err) {
1098
1172
  clearTimeout(timer);
1099
1173
  if (err instanceof Error && err.name === "AbortError") {
@@ -1101,8 +1175,8 @@ function createClient(options) {
1101
1175
  }
1102
1176
  if (err instanceof BetterCMSError) throw err;
1103
1177
  lastError = err;
1104
- if (attempt < RETRY_DELAYS3.length) {
1105
- await sleep3(RETRY_DELAYS3[attempt] ?? 0);
1178
+ if (attempt < MAX_RETRIES) {
1179
+ await sleep(backoffMs(attempt));
1106
1180
  continue;
1107
1181
  }
1108
1182
  throw new BetterCMSError(
@@ -1224,6 +1298,12 @@ function stripStega(value) {
1224
1298
  }
1225
1299
 
1226
1300
  // src/richtext.ts
1301
+ var PORTABLE_TEXT_FORMAT = "portable-text-1";
1302
+ function portableText(value) {
1303
+ if (!isRichText(value)) return null;
1304
+ const v = value;
1305
+ return v.format === PORTABLE_TEXT_FORMAT && Array.isArray(v.value) ? v.value : null;
1306
+ }
1227
1307
  function isRichText(value) {
1228
1308
  return typeof value === "object" && value !== null && !Array.isArray(value) && ("html" in value || "format" in value);
1229
1309
  }
@@ -1611,6 +1691,7 @@ export {
1611
1691
  BetterCMSError,
1612
1692
  BetterCMSManagementClient,
1613
1693
  ErrorCodes,
1694
+ PORTABLE_TEXT_FORMAT,
1614
1695
  addModelFields,
1615
1696
  addPageFields,
1616
1697
  asStringArray,
@@ -1660,6 +1741,7 @@ export {
1660
1741
  listSubmissions,
1661
1742
  listWorkspaces,
1662
1743
  plain,
1744
+ portableText,
1663
1745
  publishPage,
1664
1746
  regenerateApiKey,
1665
1747
  removeMember,