@usethrottle/catalog 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @usethrottle/catalog
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7266f03: First release: the storefront client, React hooks, server helpers and URL helpers
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Epic Design Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,269 @@
1
- # Temporary Holding Version
1
+ # @usethrottle/catalog
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The storefront SDK for the Throttle Product Catalog: a typed client for every `/v1/storefront` route, React hooks with a small stale-while-revalidate cache and a session-backed cart, Next.js server helpers, and URL helpers for shop pages.
4
+
5
+ Full guides: https://usethrottle.dev/docs/product-catalog/
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @usethrottle/catalog
11
+ # or
12
+ pnpm add @usethrottle/catalog
13
+ ```
14
+
15
+ Node 20 or later, or any evergreen browser. The only runtime dependency is `@usethrottle/errors`. `react` (18.2 or 19) is an optional peer, needed only for `@usethrottle/catalog/react`. TypeScript needs `moduleResolution` `node16`, `nodenext` or `bundler`: the package has only an `exports` map, which the older `node` setting cannot read.
16
+
17
+ ## Quickstart
18
+
19
+ Create a publishable key in the catalog dashboard (Storefront › Keys). For a script or a server, use a **server** key; for a browser, a **browser** key with your site's origin on its list.
20
+
21
+ ```ts
22
+ import { createCatalogClient } from '@usethrottle/catalog';
23
+
24
+ const catalog = createCatalogClient({
25
+ publishableKey: process.env.CATALOG_SERVER_KEY ?? '',
26
+ });
27
+
28
+ const { products } = await catalog.products.list({ limit: 5 });
29
+
30
+ for (const product of products) {
31
+ console.log(product.handle, product.title, product.priceRange?.minMinor);
32
+ }
33
+ ```
34
+
35
+ Prices are integers in minor units (cents for USD). The client talks to `https://catalog.usethrottle.dev` by default (`baseUrl` changes it). Every call takes `{ signal, timeoutMs }`; the default timeout is 10 seconds.
36
+
37
+ ## Entries
38
+
39
+ | Import | What it holds |
40
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
41
+ | `@usethrottle/catalog` | `createCatalogClient`, the error classes and `isCatalogError`, and the answer types. No DOM and no Node-only API. |
42
+ | `@usethrottle/catalog/react` | `CatalogProvider`, the read hooks, `useCart` and `useSubmitReview` (marked `"use client"`). |
43
+ | `@usethrottle/catalog/server` | `createServerCatalogClient`, `catalogTags`, `syncCatalogGeneration` and the paging helpers (`listAllProductHandles`, `staticProductParams`, `listAllCollectionHandles`, `listAllCategoryHandles`). |
44
+ | `@usethrottle/catalog/url` | `parseShopQuery`, `serializeShopQuery`, `toProductsQuery`, `parseRange` and `formatRange`. Standalone: none of the client or React code (the ESM and CommonJS builds each load one tiny shared helper file). |
45
+
46
+ Everything `/react` exports is a client reference in a server component, `SDK_VERSION` and `catalogQueryKey` included: in server code import `SDK_VERSION` from the root or `/server`, and call `catalogQueryKey` only in client code.
47
+
48
+ The client's namespaces: `products` (`list`, `get`, `related`), `filters.values`, `categories` (`tree`, `get`), `tags.list`, `collections` (`list`, `get`), `suggest`, `config`, `reviews` (`list`, `eligibility`, `submit`) and `carts` (`check`, `create`, `session`). `products.get` always asks for the full product; lists default to the lite projection (`fields: 'full'` or a list of keys changes it).
49
+
50
+ ## Keys
51
+
52
+ Only publishable keys (`pc_pk_`) are accepted. Anything else throws `CatalogError` `INVALID_PUBLISHABLE_KEY` when the client is created, and a secret key (`pc_sk_`) gets its own message: secret keys must never reach a storefront. The key itself is never part of an error message.
53
+
54
+ Publishable keys have a type, chosen when you create them:
55
+
56
+ - **browser**: the request must come from an `https` origin on the key's list. Use it in client-side code.
57
+ - **app**: a request with no `Origin` is fine; one with an `Origin` must be on the list. For native apps and other non-browser clients.
58
+ - **server**: a request with any `Origin` is refused (`403 SERVER_KEY_IN_BROWSER`). Use it for SSR, static builds and scripts; it has higher rate limits and no per-visitor bucket.
59
+
60
+ The allowed origins protect browsers only; a publishable key is public by design. For carts, the same origins must also be on Throttle's allowed origins.
61
+
62
+ ## Customer prices
63
+
64
+ Pass `customerToken`, a function the client calls on every request, to get the signed-in customer's prices. With `@usethrottle/auth` 0.10.1 or later:
65
+
66
+ ```ts
67
+ const catalog = createCatalogClient({
68
+ publishableKey,
69
+ customerToken: () => auth.getAccessToken(),
70
+ });
71
+ ```
72
+
73
+ The token travels only in the `X-Customer-Token` header; it is never stored on the client, put in a URL or logged. A token the catalog refuses is asked for once more; a read then falls back to list prices (the answer has no `priceContext`), while `reviews.eligibility`, `reviews.submit` and a cart create with a token throw `CustomerTokenInvalidError`.
74
+
75
+ ## Errors
76
+
77
+ Every error the SDK throws is a `CatalogError` (which extends `ThrottleError` from `@usethrottle/errors`), with `code`, `statusCode`, `retryAfterSeconds`, `issues`, `retryable` and `source` (`'catalog'`, `'throttle'` for Throttle's cart-session routes, or `'client'` when nothing answered).
78
+
79
+ | Class | When |
80
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
81
+ | `RateLimitedError` | every `429`; `retryAfterSeconds` comes from `Retry-After` (60 when it is missing or unusable). The client never retries a `429` itself. |
82
+ | `OriginNotAllowedError` | `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER`, and Throttle's `origin_not_allowed`. |
83
+ | `CustomerTokenInvalidError` | a refused customer token on a call that cannot fall back to list prices. |
84
+ | `CartLineError` | `CART_ITEMS_INVALID` and Throttle's `quantity_unavailable` and `line_not_purchasable`, with `lines: { sku, code, maxQuantity? }[]`. |
85
+ | `TimeoutError` | `TIMEOUT`: no answer within `timeoutMs`. |
86
+ | `NetworkError` | `NETWORK_ERROR`: the request never got an answer. |
87
+ | `FeatureUnavailableError` | `FEATURE_UNAVAILABLE`: a `404` marked as a route this catalog does not have (an older catalog), or a `404` from a route that never answers a missing resource (`config`, the category tree, `tags`, the collections list, `suggest`, `carts/check`, `carts`). Any other `404` is `CatalogError` `NOT_FOUND`. |
88
+ | `CatalogError` | everything else, including `INVALID_RESPONSE` (an answer that is not the JSON expected) and `CLIENT_ERROR` (a `cartStorage` adapter failed). |
89
+
90
+ A `503 STOREFRONT_INDEXING` (the catalog's first index build) on a read is retried once after `Retry-After` when that is at most 30 seconds (`indexingRetry` changes or turns it off); otherwise it throws with `retryAfterSeconds`. Your own `AbortSignal` rethrows its native abort reason, never wrapped.
91
+
92
+ ```ts
93
+ import { isCatalogError, RateLimitedError } from '@usethrottle/catalog';
94
+
95
+ try {
96
+ await catalog.products.list({ q: 'brake pads' });
97
+ } catch (error) {
98
+ if (error instanceof RateLimitedError) wait(error.retryAfterSeconds);
99
+ else if (isCatalogError(error)) show(error.code, error.issues);
100
+ else throw error;
101
+ }
102
+ ```
103
+
104
+ ## Carts and checkout
105
+
106
+ Carts are resolved by the catalog: the browser sends SKUs and quantities, never prices. `carts.create` answers Throttle's hosted cart URL (`cartUrl`) and a session id. It takes three optional URLs, each an absolute `https` URL on the store's Checkout settings › Allowed return websites (otherwise `422 RETURN_URL_NOT_ALLOWED`, one issue per refused field):
107
+
108
+ ```ts
109
+ const cart = await catalog.carts.create({
110
+ items: [{ sku: 'BRK-PAD-01', quantity: 2 }],
111
+ returnUrl: 'https://shop.example.com/complete', // after payment
112
+ cancelUrl: 'https://shop.example.com/cart', // the hosted cart's cancel link
113
+ storefrontUrl: 'https://shop.example.com/shop', // "Continue shopping"
114
+ });
115
+ // cart.returnUrlApplied is false when Throttle did not store them.
116
+ ```
117
+
118
+ Without them the shopper ends on Throttle's own completion page. To build your own cart page instead, use `catalog.carts.session(cart.sessionId)` (`get`, `add`, `update`, `remove`, `refresh`, `checkout({ returnUrl, cancelUrl })`). `checkout()` answers the checkout URL; it never navigates. Both are checked before any request: `carts.create` refuses a URL that is not absolute `https` (`INVALID_REQUEST`, naming the field), and `checkout()` one that is not absolute `http` or `https`.
119
+
120
+ ## React
121
+
122
+ ```tsx
123
+ 'use client';
124
+ import { useMemo } from 'react';
125
+ import { createCatalogClient } from '@usethrottle/catalog';
126
+ import {
127
+ CatalogProvider,
128
+ useCart,
129
+ useProducts,
130
+ type CartStorage,
131
+ } from '@usethrottle/catalog/react';
132
+
133
+ // One client for the life of the app.
134
+ const client = createCatalogClient({
135
+ publishableKey: process.env.NEXT_PUBLIC_CATALOG_KEY ?? '',
136
+ customerToken: () => auth.getAccessToken(),
137
+ });
138
+
139
+ // Keeps the cart across reloads, one slot per customer.
140
+ function localCartStorage(customerId: string | null): CartStorage {
141
+ const key = `catalog-cart:${customerId ?? 'guest'}`;
142
+ return {
143
+ get: () => localStorage.getItem(key),
144
+ set: (id) => localStorage.setItem(key, id),
145
+ remove: () => localStorage.removeItem(key),
146
+ };
147
+ }
148
+
149
+ export function Shop({ customerId }: { customerId: string | null }) {
150
+ const cartStorage = useMemo(() => localCartStorage(customerId), [customerId]);
151
+ return (
152
+ <CatalogProvider
153
+ client={client}
154
+ customerKey={customerId}
155
+ cartStorage={cartStorage}
156
+ cartUrls={{
157
+ returnUrl: 'https://shop.example.com/complete',
158
+ cancelUrl: 'https://shop.example.com/cart',
159
+ storefrontUrl: 'https://shop.example.com/shop',
160
+ }}
161
+ >
162
+ <ProductGrid />
163
+ </CatalogProvider>
164
+ );
165
+ }
166
+
167
+ function ProductGrid() {
168
+ const { data, error } = useProducts({ limit: 24 });
169
+ const cart = useCart(); // cart.add(sku, quantity) from a product page
170
+ const checkout = async () =>
171
+ location.assign((await cart.checkout()).checkoutUrl);
172
+ if (error) return <p>{error.message}</p>;
173
+ return (
174
+ <>
175
+ <ul>
176
+ {data?.products.map((product) => (
177
+ <li key={product.handle}>{product.title}</li>
178
+ ))}
179
+ </ul>
180
+ <button disabled={!cart.cart || cart.pending} onClick={checkout}>
181
+ Checkout ({cart.cart?.unitCount ?? 0})
182
+ </button>
183
+ </>
184
+ );
185
+ }
186
+ ```
187
+
188
+ - **Hooks:** `useProducts`, `useProduct`, `useRelated`, `useFilterValues`, `useCategories`, `useCollection`, `useSuggest` (debounced 200 ms, 2 characters at least), `useReviews` and `useConfig`, each answering `{ data, error, isLoading, isValidating, refetch }` and taking `enabled`, `initialData` (for an SSR hand-off) and `staleTimeMs`.
189
+ - **The cache** is per provider: answers are shown at once and refetched when older than 60 seconds, and calls for one key share one request. Two providers never share it.
190
+ - **`customerKey`** partitions the cache by customer. Change it in the same update as the token: a change starts an empty cache and every mounted hook refetches, so one customer never sees another's prices. It is required whenever `customerToken` can return a token: `useCart` drops the cart on a sign-out or a customer change only by watching it, so without it one customer's cart stays for the next shopper. Keep one `client` for the app's life and pass `customerToken` as a function.
191
+ - **`useCart`** keeps one cart per provider and runs its changes one at a time. It creates the cart on the first `add`, recreates it once if Throttle ended the session, and makes a new cart on `switchCurrency`. The cart is dropped when the signed-in customer signs out or changes, and kept on sign-in. `checkout()` uses the provider's `cartUrls` unless given URLs, and answers the checkout URL. `cartUrls` go on every cart create, so they must be absolute `https` URLs on the store's Allowed return websites, or every `add` fails; otherwise (on `http://localhost`, say) leave them out and pass the URLs to `checkout(input)`. The default `cartStorage` is memory only; key a persistent one by customer, as above: a new adapter object per customer, never one stable object that reads the current customer when called (a drop removes the outgoing customer's entry through the outgoing object, so such an adapter would delete the incoming customer's cart instead).
192
+ - **Rate limits:** a catalog `429` pauses every read of the provider until its retry time; each mounted key then refetches once.
193
+ - **`useSubmitReview(handle)`** submits a review with one idempotency key per submission.
194
+
195
+ ## Next.js
196
+
197
+ On the server, use a **server** key and `createServerCatalogClient`. Every catalog read's `fetch` carries `next: { revalidate, tags }` (60 seconds by default); carts, Throttle calls and any request with a customer token are `cache: 'no-store'`. Next.js itself is never imported.
198
+
199
+ ```ts
200
+ // lib/catalog.ts
201
+ import { createServerCatalogClient } from '@usethrottle/catalog/server';
202
+
203
+ export const catalog = createServerCatalogClient({
204
+ publishableKey: process.env.CATALOG_SERVER_KEY ?? '',
205
+ next: { revalidate: 300 },
206
+ });
207
+ ```
208
+
209
+ During the catalog's first index build, a read that meets `503 STOREFRONT_INDEXING` waits its `Retry-After` (when that is at most 30 seconds by default; `indexingRetry: { maxWaitSeconds }` changes the limit) and asks once more. Pass `indexingRetry: false` to `createServerCatalogClient` to fail fast inside a page render or route handler instead.
210
+
211
+ Tags are `throttle-catalog`, `throttle-catalog:<route>` and, on a route for one handle (a product, its related products or reviews, a category or a collection), `throttle-catalog:<route>:<handle>`; `catalogTags(route, handle)` builds them. Two catalog installations in one Next.js app share these tags, so revalidating one also refreshes the other. To refresh pages when the catalog changes, poll `syncCatalogGeneration` (from a cron job, every minute for example) and revalidate on a change. Keep the last generation somewhere durable (KV, a database row, an edge config): on a serverless host a call often lands on a fresh instance, where a module variable starts empty, so that call only records the generation and a change made since is never revalidated. The pages' `revalidate` still bounds how stale they get if a change is missed.
212
+
213
+ ```ts
214
+ // app/api/catalog-sync/route.ts
215
+ import { revalidateTag } from 'next/cache';
216
+ import { syncCatalogGeneration } from '@usethrottle/catalog/server';
217
+ import { catalog } from '@/lib/catalog';
218
+ import { kv } from '@/lib/kv'; // any durable store: KV, a database row, an edge config
219
+
220
+ export async function GET() {
221
+ const previous = (await kv.get<number>('catalog-generation')) ?? undefined;
222
+ const { generation, changed } = await syncCatalogGeneration({
223
+ client: catalog,
224
+ previous,
225
+ onChange: () => revalidateTag('throttle-catalog'),
226
+ });
227
+ await kv.set('catalog-generation', generation);
228
+ return Response.json({ generation, changed });
229
+ }
230
+ ```
231
+
232
+ `staticProductParams(catalog)` feeds `generateStaticParams`, and `listAllProductHandles(catalog)` (an async iterable) feeds a sitemap. In the browser, use a browser key and the React provider.
233
+
234
+ ## URL helpers
235
+
236
+ `@usethrottle/catalog/url` keeps a shop page's selection in its URL, with one canonical string per selection and every parameter it does not know kept in order:
237
+
238
+ ```ts
239
+ import {
240
+ parseShopQuery,
241
+ serializeShopQuery,
242
+ toProductsQuery,
243
+ } from '@usethrottle/catalog/url';
244
+
245
+ const shop = parseShopQuery(location.search); // or URLSearchParams, or a record
246
+ const query = toProductsQuery(shop, { limit: 24, facets: true });
247
+ const next = serializeShopQuery({
248
+ ...shop,
249
+ filters: { ...shop.filters, brand: ['Bosch'] },
250
+ page: 1,
251
+ });
252
+ history.pushState(null, '', `?${next}`);
253
+ ```
254
+
255
+ Ranges stay strings (`10..50`); `parseRange` and `formatRange` convert them.
256
+
257
+ ## Module formats
258
+
259
+ The package ships ESM and CommonJS builds. Within one format, every entry shares one copy of the client and the error classes, so `instanceof` works across `/react`, `/server` and the root. An app that loads the package through both `import` and `require` gets two copies: `instanceof RateLimitedError` (or any other class) is then true only for errors from the same format. `isCatalogError` works across both, because it also checks a shared brand; use it, then branch on `error.code`.
260
+
261
+ ## Versioning
262
+
263
+ The package stays at `0.x` until the reference store ships with `1.0.0`. While it is `0.x`, a breaking type change is a minor release, named in the changelog. SDK 0.1.0 targets the current catalog. Against an older one, everything works except the route-miss marker behind `FeatureUnavailableError`, the OpenAPI document and the cart return URLs (an older catalog refuses them with `400 INVALID_REQUEST`).
264
+
265
+ The OpenAPI 3.1 document of the storefront API is at https://catalog.usethrottle.dev/v1/storefront/openapi.json.
266
+
267
+ ## License
268
+
269
+ MIT
@@ -0,0 +1,243 @@
1
+ import { p as CatalogContractErrorCode } from './types-DYfIvIaI.js';
2
+
3
+ /**
4
+ * Shared error type for every Throttle SDK.
5
+ *
6
+ * Historically each SDK shipped its own structurally-identical error class
7
+ * (`ThrottleApiError` in `@usethrottle/cart`, `ThrottleCheckoutError` in
8
+ * `@usethrottle/checkout-sdk`, `ThrottleDiscountsError`,
9
+ * `ThrottleSubscriptionsError`, …). Catching across two packages meant catching
10
+ * two names. Every SDK error class now extends `ThrottleError`, so a single
11
+ * `catch (e) { if (e instanceof ThrottleError) … }` covers all of them while the
12
+ * original per-SDK class names keep working as aliases for backward
13
+ * compatibility.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { ThrottleError } from '@usethrottle/errors';
18
+ * try {
19
+ * await cart.items.add(cartId, item);
20
+ * await checkout.completeSession(sessionId, payment);
21
+ * } catch (e) {
22
+ * if (e instanceof ThrottleError) {
23
+ * console.error(e.code, e.statusCode, e.message);
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+ interface ThrottleErrorArgs {
29
+ /** Stable machine-readable error code from the API envelope (`error.code`). */
30
+ code: string;
31
+ /** Human-readable message (`error.message`). */
32
+ message: string;
33
+ /** HTTP status code of the failed response. */
34
+ statusCode: number;
35
+ /** Structured per-field detail when the API provides it (`error.details`). */
36
+ details?: unknown;
37
+ /** The full parsed response body, when an SDK chooses to attach it. */
38
+ body?: unknown;
39
+ /** The API's request id (x-request-id / meta.requestId), for support + Sentry correlation. */
40
+ requestId?: string;
41
+ /**
42
+ * The failed response's headers, when an SDK constructs the error directly
43
+ * from a `fetch` `Response`. Used to recover `requestId` from the
44
+ * `x-request-id` header for hand-built error bodies (401/403/500) that lack
45
+ * a `meta` envelope — the header is always set, even when `meta` isn't.
46
+ */
47
+ responseHeaders?: {
48
+ get(name: string): string | null | undefined;
49
+ } | Record<string, string | undefined>;
50
+ }
51
+ declare class ThrottleError extends Error {
52
+ readonly code: string;
53
+ readonly statusCode: number;
54
+ readonly details?: unknown;
55
+ readonly body?: unknown;
56
+ readonly requestId?: string;
57
+ constructor(args: ThrottleErrorArgs);
58
+ }
59
+
60
+ declare const THROTTLE_CART_CODES: {
61
+ readonly price_locked: {
62
+ readonly statuses: readonly [400];
63
+ readonly retryable: false;
64
+ };
65
+ readonly quantity_unavailable: {
66
+ readonly statuses: readonly [409];
67
+ readonly retryable: false;
68
+ };
69
+ readonly line_not_purchasable: {
70
+ readonly statuses: readonly [422];
71
+ readonly retryable: false;
72
+ };
73
+ readonly line_resolver_unavailable: {
74
+ readonly statuses: readonly [503];
75
+ readonly retryable: true;
76
+ };
77
+ readonly line_resolver_not_available: {
78
+ readonly statuses: readonly [422];
79
+ readonly retryable: false;
80
+ };
81
+ readonly currency_not_enabled: {
82
+ readonly statuses: readonly [422];
83
+ readonly retryable: false;
84
+ };
85
+ readonly resolved_lines_required: {
86
+ readonly statuses: readonly [422];
87
+ readonly retryable: false;
88
+ };
89
+ readonly line_limit_reached: {
90
+ readonly statuses: readonly [422];
91
+ readonly retryable: false;
92
+ };
93
+ readonly cart_prices_changed: {
94
+ readonly statuses: readonly [409];
95
+ readonly retryable: false;
96
+ };
97
+ readonly shipping_method_unavailable: {
98
+ readonly statuses: readonly [422];
99
+ readonly retryable: false;
100
+ };
101
+ readonly shipping_quote_unavailable: {
102
+ readonly statuses: readonly [503];
103
+ readonly retryable: true;
104
+ };
105
+ readonly amount_too_large: {
106
+ readonly statuses: readonly [422];
107
+ readonly retryable: false;
108
+ };
109
+ readonly cart_changed: {
110
+ readonly statuses: readonly [409];
111
+ readonly retryable: false;
112
+ };
113
+ readonly discount_currency_unsupported: {
114
+ readonly statuses: readonly [422];
115
+ readonly retryable: false;
116
+ };
117
+ readonly shipping_currency_unsupported: {
118
+ readonly statuses: readonly [422];
119
+ readonly retryable: false;
120
+ };
121
+ readonly cart_not_open: {
122
+ readonly statuses: readonly [409];
123
+ readonly retryable: false;
124
+ };
125
+ readonly cart_not_found: {
126
+ readonly statuses: readonly [404];
127
+ readonly retryable: false;
128
+ };
129
+ readonly item_not_found: {
130
+ readonly statuses: readonly [404];
131
+ readonly retryable: false;
132
+ };
133
+ /**
134
+ * `details: { reason, storefrontUrl }`. Throttle sends `storefrontUrl` raw
135
+ * and unvalidated (its hosted cart page validates it), and the SDK passes
136
+ * it through untouched in `details` and `body`. Never render it as a link
137
+ * or navigate to it without your own check (http(s) only, an origin you
138
+ * trust): it may hold a `javascript:` URL.
139
+ */
140
+ readonly cart_session_expired: {
141
+ readonly statuses: readonly [410];
142
+ readonly retryable: false;
143
+ };
144
+ readonly cart_session_not_found: {
145
+ readonly statuses: readonly [404];
146
+ readonly retryable: false;
147
+ };
148
+ readonly origin_not_allowed: {
149
+ readonly statuses: readonly [403];
150
+ readonly retryable: false;
151
+ };
152
+ readonly checkout_unavailable: {
153
+ readonly statuses: readonly [501];
154
+ readonly retryable: false;
155
+ };
156
+ };
157
+ type ThrottleCartCode = keyof typeof THROTTLE_CART_CODES;
158
+
159
+ /** Codes the SDK makes itself, with no server answer behind them (P6-R7). */
160
+ type ClientErrorCode = 'TIMEOUT' | 'NETWORK_ERROR' | 'FEATURE_UNAVAILABLE' | 'INVALID_RESPONSE' | 'INVALID_PUBLISHABLE_KEY' | 'CLIENT_ERROR';
161
+ type CatalogErrorCode = CatalogContractErrorCode | ThrottleCartCode | ClientErrorCode;
162
+ /**
163
+ * Which side answered: the catalog Worker, Throttle's cart-session API, or
164
+ * nobody (`client`: a timeout, a network failure or a bad option).
165
+ */
166
+ type CatalogErrorSource = 'catalog' | 'throttle' | 'client';
167
+ type CatalogErrorIssue = {
168
+ path: string;
169
+ message: string;
170
+ };
171
+ type CartLineProblem = {
172
+ sku: string;
173
+ code: string;
174
+ maxQuantity?: number;
175
+ };
176
+ type CatalogErrorInit = {
177
+ code: CatalogErrorCode | (string & {});
178
+ message: string;
179
+ statusCode: number;
180
+ source: CatalogErrorSource;
181
+ /** Defaults to true exactly for a 429 or 503 status. */
182
+ retryable?: boolean;
183
+ retryAfterSeconds?: number | null;
184
+ issues?: readonly CatalogErrorIssue[];
185
+ details?: unknown;
186
+ body?: unknown;
187
+ requestId?: string | undefined;
188
+ cause?: unknown;
189
+ };
190
+ /** The base of every error the SDK throws (spec §13.2, P6-R7). */
191
+ declare class CatalogError extends ThrottleError {
192
+ readonly code: CatalogErrorCode | (string & {});
193
+ /** Seconds to wait before retrying, when the answer says (or P6-R10/R11 default). */
194
+ readonly retryAfterSeconds: number | null;
195
+ /** Field problems from an `INVALID_REQUEST`-style answer. */
196
+ readonly issues: readonly CatalogErrorIssue[];
197
+ /** Whether the same request may succeed later. */
198
+ readonly retryable: boolean;
199
+ readonly source: CatalogErrorSource;
200
+ constructor(init: CatalogErrorInit);
201
+ }
202
+ /** Every `429`. The core never retries one (P6-R11). */
203
+ declare class RateLimitedError extends CatalogError {
204
+ readonly retryAfterSeconds: number;
205
+ constructor(init: CatalogErrorInit & {
206
+ retryAfterSeconds: number;
207
+ });
208
+ }
209
+ /** `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER` and Throttle `origin_not_allowed`. */
210
+ declare class OriginNotAllowedError extends CatalogError {
211
+ constructor(init: CatalogErrorInit);
212
+ }
213
+ /** `CUSTOMER_TOKEN_INVALID`. */
214
+ declare class CustomerTokenInvalidError extends CatalogError {
215
+ constructor(init: CatalogErrorInit);
216
+ }
217
+ /** `CART_ITEMS_INVALID` and Throttle `quantity_unavailable` / `line_not_purchasable`. */
218
+ declare class CartLineError extends CatalogError {
219
+ /** One entry per refused line; `maxQuantity` may be 0. */
220
+ readonly lines: readonly CartLineProblem[];
221
+ constructor(init: CatalogErrorInit & {
222
+ lines: readonly CartLineProblem[];
223
+ });
224
+ }
225
+ /** The request did not finish within `timeoutMs` (P6-R9). */
226
+ declare class TimeoutError extends CatalogError {
227
+ constructor(init?: Partial<CatalogErrorInit> & {
228
+ timeoutMs?: number;
229
+ });
230
+ }
231
+ /** `fetch` itself failed (DNS, a CORS refusal, a reset connection). */
232
+ declare class NetworkError extends CatalogError {
233
+ constructor(init?: Partial<CatalogErrorInit>);
234
+ }
235
+ /** The catalog Worker does not have this route yet (P6-R8). */
236
+ declare class FeatureUnavailableError extends CatalogError {
237
+ constructor(init?: Partial<CatalogErrorInit>);
238
+ }
239
+ /** True for a `CatalogError` from this or any other copy of the SDK. */
240
+ declare function isCatalogError(value: unknown): value is CatalogError;
241
+
242
+ export { CartLineError as C, FeatureUnavailableError as F, NetworkError as N, OriginNotAllowedError as O, RateLimitedError as R, CatalogError as b, CustomerTokenInvalidError as h, TimeoutError as i, isCatalogError as j };
243
+ export type { ThrottleCartCode as T, CartLineProblem as a, CatalogErrorCode as c, CatalogErrorInit as d, CatalogErrorIssue as e, CatalogErrorSource as f, ClientErrorCode as g };