@wocart/storefront 1.0.0 → 1.2.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 CHANGED
@@ -5,6 +5,86 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
5
5
  in a new major version, and an export is marked `@deprecated` for at least one
6
6
  minor release before a major removes it.
7
7
 
8
+ ## 1.2.0 — 2026-10-15
9
+
10
+ ### Added
11
+
12
+ - **`createStorefrontRoutes`** (`@wocart/storefront/astro`): the eight
13
+ same-origin API routes every storefront needs — OTP send and verify, cart
14
+ validation, abandoned-checkout tracking and conversion, communes, and
15
+ reviews (list and submit) — ready to export, so a theme's route file is one
16
+ line. Every POST refuses cross-origin requests, the OTP and abandoned
17
+ routes forward the shopper's address, and input is bounded before it is
18
+ sent. The theme passes only the request's own client.
19
+ - **`runFormAction`** (`@wocart/storefront/astro`) runs an action posted from
20
+ a form and stores its result for the page that POST renders.
21
+ - `orders.otp.send` and `orders.otp.verify` take the shopper's forwarded
22
+ headers as a last argument. The server limits sends and attempts per
23
+ shopper; without them every shopper of a store shared the theme worker's
24
+ one limit.
25
+
26
+ ### Changed
27
+
28
+ - **A proxy route's failure status follows the server's category** when
29
+ `errorStatus` is unset: 400 for validation, 422 for a business refusal, 429
30
+ when the server named a wait, 504 for a timeout, and 502 for anything the
31
+ shopper cannot fix (and for a failure that carries no `domainError`, as
32
+ before). An explicit `errorStatus` still wins.
33
+ - A client function that throws answers 502 with
34
+ `code: "PROXY_UPSTREAM_FAILED"` and a generic message; the exception's own
35
+ text no longer reaches the browser.
36
+ - `placeOrderInput` refusals name their field without a sentence. The Arabic
37
+ messages it carried reached French and English stores as-is; the theme
38
+ answers each field with its own copy.
39
+ - `placeOrderInput` and `executePlaceOrder` accept a `pricePerUnit` of 0. The
40
+ price is display-only, and a product the merchant prices at 0 was not
41
+ orderable from its product page.
42
+
43
+ ### Deprecated
44
+
45
+ - `resolveActionRedirectDecision`: use `runFormAction` and call `next()`. It
46
+ now always answers `{ kind: "next" }`. Its 303 back to the form on an
47
+ error lost the result in Astro 7 — the shopper returned to the form with no
48
+ message — so an errored post now renders in place. Its `referer` and
49
+ `pathname` inputs are unused.
50
+
51
+ ### Fixed
52
+
53
+ - A server error's `context` can no longer overwrite the result's own
54
+ `success`, `error`, `code` or `domainError` fields.
55
+ - A `*_NOT_FOUND` answer from `products.get`, `pages.get`, `pages.landing`,
56
+ `pageData.get` or `orders.tracking` is absence, not a failure: it no longer
57
+ reaches `onError` as a degraded read.
58
+
59
+ ## 1.1.0 — 2026-10-14
60
+
61
+ ### Fixed
62
+
63
+ - **A read never waits on another read's pending request.** The client used to
64
+ merge an identical read with one already in flight, and its opt-in caches
65
+ held the pending request itself. A client kept across requests (one per
66
+ isolate) could therefore make a request wait on a read another request had
67
+ started; on Cloudflare Workers that read never settles once the request
68
+ that started it is cancelled (a shopper leaving mid-load, a navigation
69
+ interrupted by a click), and every later request for the same URL hung.
70
+ Each call now makes its own request, and the caches keep finished values
71
+ only.
72
+
73
+ ### Added
74
+
75
+ - **`readTimeoutMs`** on `createStoreClient` bounds every read, retries
76
+ included: 10 000 ms by default, `0` for no limit. A read that runs out of
77
+ time degrades like any failed read and reports `REQUEST_TIMEOUT` to
78
+ `onError`, so a server that stops answering costs a page seconds instead of
79
+ holding it open. Writes keep no default limit: a write cut off by a timer
80
+ may already have happened.
81
+
82
+ ### Changed
83
+
84
+ - Parallel identical reads are no longer coalesced into one request. A page
85
+ that asks for the same data from several places should ask once and share
86
+ the result within its render.
87
+
8
88
  ## 1.0.0 — 2026-10-13
9
89
 
10
90
  The first public release.
package/README.md CHANGED
@@ -69,7 +69,7 @@ if (result.success) {
69
69
  | --- | --- | --- |
70
70
  | `@wocart/storefront` | browser and server | read-model types and schemas, reference data (58 wilayas, 1,551 communes), money and image helpers, the basket and OTP state machines, language routing, Meta pixel payloads |
71
71
  | `@wocart/storefront/server` | server only | `createStoreClient`, `StoreClient`, `ClientResult`, `StorefrontError` — the API key lives here |
72
- | `@wocart/storefront/astro` | server only | `createProxyEndpoint`, `placeOrderInput`, `executePlaceOrder`, `collectForwardedHeaders`, `resolveActionRedirectDecision` |
72
+ | `@wocart/storefront/astro` | server only | `createStorefrontRoutes`, `createProxyEndpoint`, `placeOrderInput`, `executePlaceOrder`, `collectForwardedHeaders`, `runFormAction` |
73
73
 
74
74
  Browser code never receives the key. It calls your own same-origin routes
75
75
  (for example `src/pages/api/cart.ts`), which call the SDK on the server:
@@ -115,6 +115,7 @@ import { createBasketStore, formatMoney } from "@wocart/storefront";
115
115
  | `locale` | no | read merchant text in a published store language (`"fr"`); one client per language |
116
116
  | `theme` | no | your theme's key (`defineTheme({ key })`), to receive the merchant's edits to your theme's text |
117
117
  | `onError` | no | observability hook for degraded reads |
118
+ | `readTimeoutMs` | no | how long one read may take, retries included, before it degrades (default 10 000; `0` for no limit) |
118
119
 
119
120
  A storefront deployed by Wocart receives its origin and key as the
120
121
  `COD_SERVER_URL` and `STORE_API_KEY` environment variables. The SDK reads no
package/USAGE.md CHANGED
@@ -162,9 +162,11 @@ const [config, featuredProducts] = await Promise.all([
162
162
  ]);
163
163
  ~~~
164
164
 
165
- Parallel identical GET requests made through the same client are coalesced.
166
- That makes it safe for independent page loaders to ask for the same
167
- configuration without manually coordinating them.
165
+ Every call makes its own request; a call never waits on another call's
166
+ pending request, even an identical one. Within one render, ask once and share
167
+ the result (`Promise.all`, or a per-request memo) when several parts of a page
168
+ need the same data. A client never holds pending work across calls, so one
169
+ kept for several requests cannot leave a request waiting on another's read.
168
170
 
169
171
  ### Available imports
170
172
 
@@ -438,10 +440,13 @@ conversion event.
438
440
 
439
441
  ### Caching and retries
440
442
 
441
- - GET requests are fresh by default.
442
- - Parallel identical GET requests are deduplicated per client instance.
443
- - config.get(), categories.list(), and geo.communes() use the client
444
- lifetime cache after a successful response.
443
+ - GET requests are fresh by default, and each call makes its own request.
444
+ - config.get(), categories.list(), and geo.communes() keep their finished
445
+ result for the client's lifetime; a call made while the first is still
446
+ pending makes its own request rather than waiting on it.
447
+ - Every read is bounded by `readTimeoutMs` (10 seconds by default), retries
448
+ included; a read that runs out of time degrades and reports
449
+ `REQUEST_TIMEOUT` to `onError`. Writes have no default limit.
445
450
  - null results and failures are not cached, so a temporary failure can recover
446
451
  on the next call.
447
452
  - GET network failures use two short exponential backoff retries.
@@ -910,6 +915,12 @@ verify returns a verified status with otpToken on success. Failure context can
910
915
  include attemptsRemaining and a terminal state. Use it for the countdown,
911
916
  retry, and lockout UI.
912
917
 
918
+ The server limits sends and attempts per shopper. Both calls take the
919
+ shopper's forwarded headers as their last argument
920
+ (`send(phone, collectForwardedHeaders(request))`); without them every shopper
921
+ of the store shares the one limit of the theme's worker. The `otpSend` and
922
+ `otpVerify` routes of `createStorefrontRoutes` pass them for you.
923
+
913
924
  ### Using the SDK's OtpMachine
914
925
 
915
926
  The SDK exports `createOtpMachine` to manage the complete interactive WhatsApp
@@ -1012,6 +1023,8 @@ wrapper while the SDK owns reusable validation and execution logic.
1012
1023
  placeOrderInput is the shared form schema. It:
1013
1024
 
1014
1025
  - whitelists supported fields and strips unknown fields;
1026
+ - names the failing field on a refusal, with no shopper-facing sentence — the
1027
+ theme answers each field with its own copy in the store's language;
1015
1028
  - coerces form number strings;
1016
1029
  - turns empty optional values into undefined;
1017
1030
  - canonicalizes phone input;
@@ -1041,7 +1054,7 @@ export const server = {
1041
1054
 
1042
1055
  if (!result.success) {
1043
1056
  throw new ActionError({
1044
- code: "BAD_REQUEST",
1057
+ code: result.errorKind === "shape" ? "BAD_REQUEST" : "UNPROCESSABLE_CONTENT",
1045
1058
  message: result.error,
1046
1059
  });
1047
1060
  }
@@ -1071,71 +1084,113 @@ order request:
1071
1084
  Use it when the server route is the trusted boundary that forwards the order.
1072
1085
  Do not let arbitrary browser code choose identity headers.
1073
1086
 
1074
- ### Redirect behavior
1087
+ ### Running the action from middleware
1075
1088
 
1076
- The middleware helper resolveActionRedirectDecision handles the form POST
1077
- flow:
1089
+ runFormAction runs an action posted from an HTML form and stores its result
1090
+ for the page that POST renders:
1078
1091
 
1079
- - form actions execute and serialize their result;
1080
- - form errors redirect to the referer, or the current pathname, with status
1081
- 303;
1082
- - successful form actions return next so the result cookie survives;
1083
- - non-form and JSON requests continue without a redirect.
1092
+ ~~~ts
1093
+ // src/middleware.ts
1094
+ import { defineMiddleware } from "astro:middleware";
1095
+ import { getActionContext } from "astro:actions";
1096
+ import { runFormAction } from "@wocart/storefront/astro";
1097
+
1098
+ export const onRequest = defineMiddleware(async (context, next) => {
1099
+ const { action, setActionResult, serializeActionResult } = getActionContext(context);
1100
+ await runFormAction({
1101
+ action: action && {
1102
+ calledFrom: action.calledFrom,
1103
+ name: action.name,
1104
+ handler: action.handler,
1105
+ },
1106
+ setActionResult,
1107
+ serializeActionResult,
1108
+ });
1109
+ return next();
1110
+ });
1111
+ ~~~
1084
1112
 
1085
- The common mistake is redirecting successful actions too early. Let success
1086
- continue through next, then redirect from the successful page/action flow to
1087
- the thank-you experience.
1113
+ The page reads the result with `Astro.getActionResult()`: on success it
1114
+ redirects to the thank-you page, and on a refusal it renders the form again
1115
+ with the error in place.
1116
+
1117
+ Never redirect from the middleware, on success or on error. Astro 7 keeps an
1118
+ action result for the response being rendered only, so a redirect arrives at
1119
+ the next page with no result: the thank-you redirect never fires, and a
1120
+ refused order shows no message. resolveActionRedirectDecision, the earlier
1121
+ name, is deprecated and now always answers next.
1088
1122
 
1089
1123
  ## Browser-facing proxy routes
1090
1124
 
1091
- Browser code should call a same-origin theme route. That route uses
1092
- createProxyEndpoint to call the SDK with the server-only API key.
1125
+ Browser code calls same-origin theme routes, which call the platform with the
1126
+ server-only API key. The key never reaches the browser.
1093
1127
 
1094
- This keeps the key out of the browser and centralizes CSRF checks, input
1095
- validation, parameter parsing, attribution forwarding, and error shape.
1128
+ ### The storefront routes, ready to export
1096
1129
 
1097
- ### Cart validation route
1130
+ createStorefrontRoutes returns the eight routes every storefront needs. Bind
1131
+ them once to the request's own client:
1098
1132
 
1099
1133
  ~~~ts
1100
- import { z } from "zod";
1101
- import { createProxyEndpoint } from "@wocart/storefront/astro";
1102
-
1103
- import { BasketLineInputSchema, MAX_BASKET_LINES } from "@wocart/storefront";
1134
+ // src/theme/api-routes.ts
1135
+ import type { APIContext } from "astro";
1136
+ import { createStorefrontRoutes } from "@wocart/storefront/astro";
1104
1137
 
1105
- const cartInputSchema = z.object({
1106
- items: z.array(BasketLineInputSchema).min(1).max(MAX_BASKET_LINES),
1107
- });
1108
-
1109
- export const POST = createProxyEndpoint({
1110
- method: "POST",
1111
- client: (input) => store.cart.validate(input.items),
1112
- inputSchema: cartInputSchema,
1113
- successBody: (data) => ({ success: true, data }),
1138
+ export const routes = createStorefrontRoutes<APIContext>({
1139
+ client: (context) => context.locals.store.client,
1114
1140
  });
1115
1141
  ~~~
1116
1142
 
1117
- The browser can now call the theme's own route. It never calls the server
1118
- directly and never needs the API key.
1119
-
1120
- ### Order route with attribution
1143
+ Each route file then exports one of them:
1121
1144
 
1122
1145
  ~~~ts
1123
- export const POST = createProxyEndpoint({
1124
- method: "POST",
1125
- client: (input, headers) => store.orders.create(input, headers),
1126
- forwardAttribution: true,
1127
- successStatus: 201,
1128
- errorStatus: 422,
1129
- });
1146
+ // src/pages/api/otp/send.ts
1147
+ import { routes } from "@/theme/api-routes";
1148
+ export const POST = routes.otpSend;
1130
1149
  ~~~
1131
1150
 
1132
- ### GET route with a numeric path parameter
1151
+ | Route | Method | Browser sends | Answers |
1152
+ | --- | --- | --- | --- |
1153
+ | `otpSend` | POST | `{ phone }` | `{ data: OtpSendResult }` |
1154
+ | `otpVerify` | POST | `{ phone, requestId, code }` | `{ data: OtpVerifyResult }` |
1155
+ | `cartValidate` | POST | `{ items }` | `{ success: true, data: CartValidation }` |
1156
+ | `abandonedUpsert` | POST | `AbandonedCheckoutInput` | 204 |
1157
+ | `abandonedConvert` | POST | `{ sessionId, orderId, orderNumber }` | 204 |
1158
+ | `communes` | GET | path param `wilayaId` | `{ data: Commune[] }` |
1159
+ | `reviewsList` | GET | path param `productId`, query `limit` (1–50, default 20) and `offset` | `{ data: Review[], total }` |
1160
+ | `reviewSubmit` | POST | `ReviewInput` | 201 `{ success: true, data: { id } }` |
1161
+
1162
+ What every route gets right:
1163
+
1164
+ - every POST refuses a cross-origin request with 403;
1165
+ - the OTP routes and abandoned tracking forward the shopper's address and
1166
+ user agent, so the server limits and attributes per shopper;
1167
+ - input is bounded before it is sent, and a malformed body is a 400 that never
1168
+ reaches the server;
1169
+ - a refusal answers with the status the server's category implies — 400 for
1170
+ validation, 422 for a business refusal, 429 when the server named a wait —
1171
+ and carries the server's `code` and context (`windowSeconds`,
1172
+ `attemptsRemaining`, `terminal`), which is what the browser branches on;
1173
+ - a failure the shopper cannot fix (a wrong store key, a server fault, a
1174
+ dropped connection) is a 502, and a timeout a 504.
1175
+
1176
+ The client resolver runs once per request. Return the request's own client,
1177
+ never one kept across requests.
1178
+
1179
+ ### A custom route
1180
+
1181
+ createProxyEndpoint is the factory behind those routes, for a route of the
1182
+ theme's own. Its client function returns a result (`{ success, data }` or a
1183
+ failure), so wrap a read that returns a plain value:
1133
1184
 
1134
1185
  ~~~ts
1186
+ import { z } from "zod";
1187
+ import { createProxyEndpoint } from "@wocart/storefront/astro";
1188
+
1135
1189
  export const GET = createProxyEndpoint({
1136
1190
  method: "GET",
1137
- client: (input) => store.geo.communes(input.wilayaId),
1138
- pathParams: { wilayaId: "number" },
1191
+ pathParams: { slug: "string" },
1192
+ inputSchema: z.object({ slug: z.string().min(1).max(200) }),
1193
+ client: async ({ slug }) => ({ success: true, data: await store.products.get(slug) }),
1139
1194
  });
1140
1195
  ~~~
1141
1196
 
@@ -1148,12 +1203,18 @@ Proxy configuration supports:
1148
1203
  - numeric or string path parameter parsing;
1149
1204
  - CSRF protection, enabled by default for POST and disabled by default for GET;
1150
1205
  - attribution forwarding;
1151
- - custom success and error status codes;
1152
- - custom successBody;
1153
- - noContent responses.
1206
+ - a custom success status, successBody and noContent;
1207
+ - errorStatus, which overrides the status derived from the failure. Leave it
1208
+ unset unless the browser contract needs one fixed status.
1209
+
1210
+ A thrown client function answers 502 with `code: "PROXY_UPSTREAM_FAILED"`
1211
+ and a generic message: an exception's own text never reaches the browser.
1212
+
1213
+ Orders do not go through a proxy route: they go through the placeOrder action
1214
+ (see above), which validates the form and forwards attribution itself.
1154
1215
 
1155
- Use same-origin proxy routes for browser operations. Do not solve a proxy
1156
- problem by moving the API key into public environment variables.
1216
+ Do not solve a proxy problem by moving the API key into public environment
1217
+ variables.
1157
1218
 
1158
1219
  ## Money, images, and presentation utilities
1159
1220
 
package/dist/actions.d.ts CHANGED
@@ -17,10 +17,11 @@
17
17
  * handler: async (input, context) => {
18
18
  * const result = await executePlaceOrder(input, context.request, client);
19
19
  * if (!result.success) {
20
- * if (result.errorKind === "shape") {
21
- * throw new ActionError({ code: "BAD_REQUEST", message: result.error });
22
- * }
23
- * throw new Error(result.error);
20
+ * // A refusal renders on the form, in place (see middleware.ts).
21
+ * throw new ActionError({
22
+ * code: result.errorKind === "shape" ? "BAD_REQUEST" : "UNPROCESSABLE_CONTENT",
23
+ * message: result.error,
24
+ * });
24
25
  * }
25
26
  * return result.data;
26
27
  * },
package/dist/actions.js CHANGED
@@ -17,10 +17,11 @@
17
17
  * handler: async (input, context) => {
18
18
  * const result = await executePlaceOrder(input, context.request, client);
19
19
  * if (!result.success) {
20
- * if (result.errorKind === "shape") {
21
- * throw new ActionError({ code: "BAD_REQUEST", message: result.error });
22
- * }
23
- * throw new Error(result.error);
20
+ * // A refusal renders on the form, in place (see middleware.ts).
21
+ * throw new ActionError({
22
+ * code: result.errorKind === "shape" ? "BAD_REQUEST" : "UNPROCESSABLE_CONTENT",
23
+ * message: result.error,
24
+ * });
24
25
  * }
25
26
  * return result.data;
26
27
  * },
@@ -92,7 +93,9 @@ export const placeOrderInput = z.object({
92
93
  // Forms always submit all hidden inputs — empty string must become undefined
93
94
  variantId: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().min(1).optional()),
94
95
  variantLabel: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().max(100).optional()),
95
- pricePerUnit: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.coerce.number().positive().optional()),
96
+ // Display-only — the server prices every order from the catalog — so a
97
+ // product the merchant prices at 0 is as orderable as any other.
98
+ pricePerUnit: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.coerce.number().nonnegative().optional()),
96
99
  quantity: z.coerce.number().int().min(1).max(100).default(1),
97
100
  // Explicit offer tier selected by the user
98
101
  offerId: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().optional()),
@@ -101,10 +104,12 @@ export const placeOrderInput = z.object({
101
104
  variantId: z.string().min(1),
102
105
  variantLabel: z.string().optional(),
103
106
  })).optional()),
104
- customerName: z.string().min(2, "الاسم مطلوب").max(100),
105
- phone: z.preprocess((v) => (typeof v === "string" ? toLocalAlgerianMobile(v) ?? v : v), z.string().regex(/^0[567]\d{8}$/, "رقم الهاتف غير صحيح")),
107
+ // No custom messages: a failure names its field, and the theme answers it
108
+ // with its own copy in the store's language.
109
+ customerName: z.string().min(2).max(100),
110
+ phone: z.preprocess((v) => (typeof v === "string" ? toLocalAlgerianMobile(v) ?? v : v), z.string().regex(/^0[567]\d{8}$/)),
106
111
  wilayaId: z.coerce.number().int().min(1).max(58),
107
- communeId: z.string().min(1, "يرجى اختيار البلدية"),
112
+ communeId: z.string().min(1),
108
113
  address: z.string().max(300).optional(),
109
114
  deliveryType: z.enum(["home", "stop_desk"]).default("home"),
110
115
  notes: z.string().max(500).optional(),
@@ -176,7 +181,7 @@ export function collectForwardedHeaders(request) {
176
181
  export async function executePlaceOrder(input, request, client) {
177
182
  // An order is either a basket or a single product, never neither.
178
183
  if (!input.items?.length &&
179
- (!input.productId || !input.productName || !input.pricePerUnit)) {
184
+ (!input.productId || !input.productName || input.pricePerUnit === undefined)) {
180
185
  return {
181
186
  success: false,
182
187
  error: "An order needs either a basket or a product.",
package/dist/astro.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { createProxyEndpoint } from "./endpoints/proxy.js";
2
2
  export type { ProxyClientFn, ProxyEndpointConfig, AstroAPIContext, AstroAPIRoute, } from "./endpoints/proxy.js";
3
+ export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
4
+ export type { StorefrontApiRoute, StorefrontRoutes, StorefrontRoutesOptions, } from "./endpoints/storefront-routes.js";
3
5
  export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
4
6
  export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
5
- export { resolveActionRedirectDecision } from "./middleware.js";
7
+ export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
6
8
  export type { PrgDecision, ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
package/dist/astro.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { createProxyEndpoint } from "./endpoints/proxy.js";
2
+ export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
2
3
  export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
3
- export { resolveActionRedirectDecision } from "./middleware.js";
4
+ export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
@@ -17,8 +17,8 @@ export type ClientConfig = StoreScope;
17
17
  /**
18
18
  * Options every request accepts.
19
19
  *
20
- * `signal` cancels the request — an aborted call never retries, never coalesces
21
- * with another caller's request, and returns a REQUEST_ABORTED DomainError
20
+ * `signal` cancels the request — an aborted call never retries, is never
21
+ * cached, and returns a REQUEST_ABORTED DomainError
22
22
  * (a read throws instead). `timeoutMs` bounds the whole operation, retries
23
23
  * included, and reads as REQUEST_TIMEOUT. `operation` names the call in the
24
24
  * error it produces; namespaces pass a stable label (`orders.otp.send`) so logs
@@ -61,6 +61,8 @@ export interface ClientCore {
61
61
  post: <TInput, TOutput>(path: string, body: TInput, opts?: PostOptions<TOutput>) => Promise<ClientResult<TOutput>>;
62
62
  patch: <TInput, TOutput>(path: string, body: TInput, optsOrSchema?: z.Schema<TOutput> | PatchOptions<TOutput>) => Promise<ClientResult<TOutput>>;
63
63
  }
64
+ /** How long a read may take, retries included, unless the client sets another limit. */
65
+ export declare const DEFAULT_READ_TIMEOUT_MS = 10000;
64
66
  /**
65
67
  * Create client core infrastructure
66
68
  *
@@ -68,6 +70,14 @@ export interface ClientCore {
68
70
  * @returns Core infrastructure for namespace implementations
69
71
  */
70
72
  export declare function createClientCore(config: ClientConfig): ClientCore;
73
+ /**
74
+ * The server's own answer that a looked-up record does not exist
75
+ * (`PRODUCT_NOT_FOUND`, `STORE_PAGE_NOT_FOUND`, …). A lookup returns `null` for
76
+ * it without reporting a failure: a stale link is not an outage. A 404 that
77
+ * does not carry the server's code — a wrong `serverUrl`, a route the platform
78
+ * does not have — is not absence, and is still reported.
79
+ */
80
+ export declare function isNotFoundAnswer(error: unknown): boolean;
71
81
  /**
72
82
  * Report a read that a namespace swallowed.
73
83
  *