@managesales/storefront 1.0.1 → 1.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,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@managesales/storefront`. This project adheres to
4
+ [Semantic Versioning](https://semver.org).
5
+
6
+ ## 1.1.0 — 2026-07-11
7
+
8
+ ### Added
9
+ - **Stateful cart** — `client.cart.create()` / `client.cart.current()` return a `StorefrontCart` with
10
+ `addLineItem` (auto-enriches price/name from `products.get`, merges duplicate product+variant
11
+ lines), `updateLineItem`, `removeLineItem`, `clear`, and `checkout()` to materialize the cart
12
+ straight into a checkout session. Persisted to `localStorage` by default (`persistCart: false` to
13
+ opt out); safe to import under SSR/Node with no DOM. `client.cart.recover(token)` is unchanged.
14
+ - **`orders` and `customers` alias namespaces** — `client.orders.list()` and
15
+ `client.customers.{login,register,loginWithGoogle,logout,isAuthenticated,setTokens,me,orders,addresses}`
16
+ are convenience references onto the existing `auth`/`customer` methods (no duplicated logic, no new
17
+ backend calls).
18
+ - **Configurable request retries** — network errors and 5xx/408/425/429 responses on idempotent
19
+ (`GET`/`HEAD`) requests are now retried automatically with exponential backoff, honoring a
20
+ `Retry-After` response header as a delay floor. Configure via
21
+ `createStorefrontClient({ retry: { retries, baseDelayMs, maxDelayMs, retryOnStatus } })`, or pass
22
+ `retry: false` to disable. Non-idempotent requests (`POST`/`PUT`/`PATCH`/`DELETE`) are never
23
+ retried once a response is received from the server; a network failure that never reached the
24
+ server (e.g. `fetch` throwing) is retried regardless of method, since no side effect could have
25
+ occurred yet.
26
+
27
+ ### Fixed
28
+ - `checkout.create` now sends line items under the `lines` key (matching the `/checkout` endpoint's
29
+ actual request shape) instead of `items`. Note this only corrects the request envelope — direct
30
+ callers must still supply every field the backend requires (see next item).
31
+ - `CheckoutLineItem` now includes the backend-required `productName` (string) and `unitPrice`
32
+ (number) fields, plus optional `imageUrl`. The previous `{ productId, variantId?, quantity }`
33
+ shape type-checked but was always rejected by the real `/checkout` endpoint's validation for
34
+ direct `client.checkout.create()` callers; `cart.checkout()` was unaffected since `CartLine`
35
+ already carried both fields. Callers who don't want to supply `productName`/`unitPrice` by hand
36
+ should use the stateful cart (`cart.addLineItem`), which auto-enriches them from `products.get`.
37
+
38
+ ## 1.0.2 — 2026-07-11
39
+
40
+ ### Docs
41
+ - Rewrote the README as a full developer landing page: human-readable title and tagline, feature
42
+ overview, all-package-manager install, a copy-paste quick start, a complete **API organization**
43
+ reference, framework examples (React / Next.js / Node.js), server-side auth (SSR) guide, error
44
+ handling, TypeScript guide, a **Versioning** policy, and this changelog.
45
+
46
+ ## 1.0.1 — 2026-07-11
47
+
48
+ ### Docs
49
+ - Expanded the README with per-resource examples and browser/Node runtime support notes.
50
+
51
+ ## 1.0.0 — 2026-07-11
52
+
53
+ ### Added
54
+ - Initial public release. Resource-based Storefront SDK with namespaces for products, collections,
55
+ checkout, cart recovery, auth, customer, blog, bookings, and forms.
56
+ - Built-in customer auth with automatic access-token refresh on `401`.
57
+ - `StorefrontError` class exposing `status`, `code`, `message`, and `details`.
58
+ - Dual ESM + CommonJS builds with bundled TypeScript declarations. Zero runtime dependencies.
package/README.md CHANGED
@@ -1,84 +1,116 @@
1
- # @managesales/storefront
1
+ # ManageSales Storefront SDK
2
+
3
+ **Build custom storefronts on top of the ManageSales Commerce Platform.**
2
4
 
3
5
  [![npm version](https://img.shields.io/npm/v/@managesales/storefront.svg)](https://www.npmjs.com/package/@managesales/storefront)
4
6
  [![license](https://img.shields.io/npm/l/@managesales/storefront.svg)](./LICENSE)
5
7
  [![types](https://img.shields.io/npm/types/@managesales/storefront.svg)](#typescript-support)
6
8
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#browser--node-support)
7
9
 
8
- Resource-based TypeScript client for building online storefronts on the **ManageSales Storefront
9
- API** — think Shopify's Storefront API client, for ManageSales. Browse products, run checkout, and
10
- manage customer accounts with a typed, ergonomic client instead of hand-rolled `fetch` calls.
10
+ `@managesales/storefront` is a typed, resource-based client for the ManageSales Storefront API —
11
+ like Shopify's Storefront client, for ManageSales. Everything you need to ship a storefront:
11
12
 
12
- - 🧩 **Resource namespaces** — `client.products`, `client.checkout`, `client.customer`,
13
- - 🔐 **Auth built in** — customer login + automatic access-token refresh
14
- - 📦 **Zero runtime dependencies** — one small file, uses the platform `fetch`
15
- - 🟦 **Fully typed** — ships `.d.ts`; responses and params are typed end to end
16
- - 🌐 **Runs everywhere** — dual ESM + CommonJS, browser / Node 18+ / Deno / Bun / edge
13
+ - 🛍️ **Products** — list, search, and fetch details
14
+ - 🗂️ **Collections** — browse categories and their products
15
+ - 👤 **Customers** — registration, login, profile, order history
16
+ - 🛒 **Checkout** — line items, coupons, complete purchases (+ abandoned-cart recovery)
17
+ - 🔎 **Search** — full-text product search
18
+ - 🟦 **TypeScript** — first-class types, no `@types` needed
19
+ - ⚡ **SSR-ready** — inject a custom `fetch`; server-side auth pattern built in
20
+ - 📦 **Zero dependencies** — one small file, dual ESM + CommonJS
17
21
 
18
22
  ---
19
23
 
20
24
  ## Table of contents
21
25
 
22
- - [What is this?](#what-is-this)
23
26
  - [Installation](#installation)
24
- - [Quick start](#quick-start-30-seconds)
27
+ - [Quick start](#quick-start)
28
+ - [API organization](#api-organization)
25
29
  - [Create a client](#create-a-client)
26
- - [Products](#products)
30
+ - [Products & search](#products--search)
27
31
  - [Collections](#collections)
28
32
  - [Cart & checkout](#cart--checkout)
33
+ - [Stateful cart](#stateful-cart)
29
34
  - [Authentication](#authentication)
30
35
  - [Customer account](#customer-account)
36
+ - [Retries](#retries)
31
37
  - [Error handling](#error-handling)
32
- - [More resources](#more-resources)
33
38
  - [TypeScript support](#typescript-support)
39
+ - [Framework examples](#framework-examples)
34
40
  - [Browser & Node support](#browser--node-support)
41
+ - [Versioning](#versioning)
42
+ - [Changelog](#changelog)
35
43
  - [No-npm alternative](#no-npm-alternative)
36
44
  - [License](#license)
37
45
 
38
- ---
39
-
40
- ## What is this?
41
-
42
- `@managesales/storefront` is the official client SDK for the **ManageSales Storefront API** — the
43
- public, customer-facing API behind a ManageSales store. Use it to build a custom storefront
44
- (product pages, search, cart, checkout, customer login) in any JavaScript/TypeScript framework,
45
- while the SDK handles request headers, workspace scoping, error parsing, pagination, and token
46
- refresh for you.
47
-
48
- It talks to the same API you can also reach directly over HTTP (`/storefront-api/*`) — the SDK is
49
- just the typed, batteries-included way to do it.
50
-
51
46
  ## Installation
52
47
 
53
48
  ```bash
54
49
  npm install @managesales/storefront
55
- # pnpm add @managesales/storefront
56
- # yarn add @managesales/storefront
57
- # bun add @managesales/storefront
50
+ pnpm add @managesales/storefront
51
+ yarn add @managesales/storefront
52
+ bun add @managesales/storefront
58
53
  ```
59
54
 
60
- ## Quick start (30 seconds)
55
+ ## Quick start
56
+
57
+ Copy-paste, add your workspace ID, and you're fetching live catalog data — no auth required:
61
58
 
62
59
  ```ts
63
60
  import { createStorefrontClient } from "@managesales/storefront";
64
61
 
65
62
  const client = createStorefrontClient({
66
- workspaceId: "ws_123", // your store's workspace ID — Settings → API
63
+ workspaceId: "ws_123", // your store — Settings → API
67
64
  });
68
65
 
69
- // Browse products no authentication required
70
- const { data: products, total } = await client.products.list({ limit: 12 });
71
- console.log(`${total} products`, products);
66
+ const { data: products } = await client.products.list({ limit: 12 });
67
+ console.log(products);
72
68
  ```
73
69
 
74
- That's it — public catalog data needs no auth. Add [authentication](#authentication) only when
75
- customers log in.
70
+ ## API organization
76
71
 
77
- ## Create a client
72
+ Every resource is a namespace on the client. Public resources need no auth; customer resources
73
+ require a logged-in session.
78
74
 
79
75
  ```ts
80
- import { createStorefrontClient } from "@managesales/storefront";
76
+ // Catalog (public)
77
+ client.products.list(params?) // paginated list
78
+ client.products.get(idOrSlug) // product details
79
+ client.products.search(query, params?) // full-text search
80
+ client.collections.list(params?)
81
+ client.collections.get(idOrSlug) // collection + its products
82
+
83
+ // Cart & checkout (public)
84
+ client.checkout.create(lineItems) // start a checkout from line items
85
+ client.checkout.get(checkoutId)
86
+ client.checkout.applyCoupon(checkoutId, code)
87
+ client.checkout.complete(checkoutId, payment?)
88
+ client.cart.create() / client.cart.current() // stateful cart — see Stateful cart
89
+ client.cart.recover(token) // rebuild a checkout from a recovery link
90
+ client.orders.list(params?) // alias for client.customer.orders(params?)
91
+
92
+ // Auth & customer (customer session)
93
+ client.auth.register({ email, password, name })
94
+ client.auth.login(email, password)
95
+ client.auth.loginWithGoogle(idToken)
96
+ client.auth.logout()
97
+ client.auth.isAuthenticated()
98
+ client.auth.setTokens(access, refresh) // restore a session (e.g. from cookies)
99
+ client.customer.me()
100
+ client.customer.orders(params?) // order history
101
+ client.customer.addresses()
102
+ // client.customers.* — alias namespace bundling auth + customer under one name
103
+ // (client.customers.login === client.auth.login, client.customers.me === client.customer.me, ...)
104
+
105
+ // Content (public)
106
+ client.blog.list(params?) / client.blog.get(slug)
107
+ client.bookings.services() / client.bookings.availability(params) / client.bookings.book(data)
108
+ client.forms.get(idOrSlug) / client.forms.submit(formId, data) / client.forms.uploadFile(formId, file)
109
+ ```
81
110
 
111
+ ## Create a client
112
+
113
+ ```ts
82
114
  const client = createStorefrontClient({
83
115
  workspaceId: "ws_123",
84
116
  });
@@ -87,12 +119,11 @@ const client = createStorefrontClient({
87
119
  | Option | Type | Required | Description |
88
120
  |--------|------|----------|-------------|
89
121
  | `workspaceId` | `string` | ✅ | Identifies your store. Found in **Settings → API**. |
90
- | `apiUrl` | `string` | — | Backend base URL **including** the `/api/v1` prefix. Defaults to ManageSales cloud; override for self-hosted/staging/local. |
91
- | `customFetch` | `typeof fetch` | — | Inject a `fetch` implementation for SSR/edge runtimes or Node < 18. |
122
+ | `apiUrl` | `string` | — | Backend base URL **including** the `/api/v1` prefix. Defaults to ManageSales cloud; override for self-hosted / staging / local. |
123
+ | `customFetch` | `typeof fetch` | — | Inject a `fetch` for SSR / edge runtimes or Node < 18. |
92
124
  | `locale` | `string` | — | Default locale for localized content (e.g. `"en"`). |
93
125
 
94
126
  ```ts
95
- // Point at your own backend and inject a custom fetch:
96
127
  const client = createStorefrontClient({
97
128
  workspaceId: "ws_123",
98
129
  apiUrl: "https://api.mystore.com/api/v1",
@@ -101,7 +132,7 @@ const client = createStorefrontClient({
101
132
  });
102
133
  ```
103
134
 
104
- ## Products
135
+ ## Products & search
105
136
 
106
137
  ```ts
107
138
  // List with pagination, search, and sorting
@@ -124,32 +155,30 @@ const { data: results } = await client.products.search("organic cotton");
124
155
  ## Collections
125
156
 
126
157
  ```ts
127
- // List collections
128
158
  const { data: collections } = await client.collections.list();
129
159
 
130
- // Get a collection with its products
131
160
  const summer = await client.collections.get("summer-sale");
132
- console.log(summer.name, summer.products.length);
161
+ console.log(summer.name, summer.products.length); // collection + its products
133
162
  ```
134
163
 
135
164
  ## Cart & checkout
136
165
 
137
- The SDK models a cart as a set of checkout line items. Assemble items in your UI, create a checkout
138
- session, optionally apply a coupon, then complete it.
166
+ Assemble line items in your UI, create a checkout session, optionally apply a coupon, then complete
167
+ it:
139
168
 
140
169
  ```ts
141
- // 1. Create a checkout from cart line items
170
+ // 1. Create a checkout from line items
142
171
  const session = await client.checkout.create([
143
- { productId: "prod_123", variantId: "var_456", quantity: 2 },
144
- { productId: "prod_789", quantity: 1 },
172
+ { productId: "prod_123", variantId: "var_456", productName: "Blue T-Shirt", quantity: 2, unitPrice: 24.99 },
173
+ { productId: "prod_789", productName: "Canvas Tote", quantity: 1, unitPrice: 18 },
145
174
  ]);
146
175
 
147
- // 2. Re-fetch a session (e.g. after a page reload)
176
+ // 2. Re-fetch a session (e.g. after a reload)
148
177
  const current = await client.checkout.get(session.id);
149
178
 
150
179
  // 3. Apply a coupon
151
180
  const discounted = await client.checkout.applyCoupon(session.id, "SAVE10");
152
- console.log("Discount:", discounted.discount, "Total:", discounted.total);
181
+ console.log(discounted.discount, discounted.total);
153
182
 
154
183
  // 4. Complete the purchase
155
184
  const order = await client.checkout.complete(session.id, {
@@ -159,47 +188,73 @@ const order = await client.checkout.complete(session.id, {
159
188
  console.log("Order placed:", order.orderNumber);
160
189
  ```
161
190
 
191
+ > `productName` and `unitPrice` are required on every line item passed directly to
192
+ > `client.checkout.create()` — the backend validates them. If you'd rather not look these up
193
+ > yourself, use the [stateful cart](#stateful-cart) instead: `cart.addLineItem` auto-enriches
194
+ > `productName`/`unitPrice` (and `imageUrl`) via `products.get`, so cart callers only ever supply
195
+ > `productId`/`variantId`/`quantity`.
196
+
162
197
  **Abandoned-cart recovery** — turn a recovery link back into a checkout session:
163
198
 
164
199
  ```ts
165
- // URL: https://yourstore.com/recover?token=abc123
166
200
  const token = new URLSearchParams(location.search).get("token")!;
167
201
  const recovered = await client.cart.recover(token);
168
202
  ```
169
203
 
170
- ## Authentication
204
+ ## Stateful cart
171
205
 
172
- Most storefront pages need **no auth**. When a customer logs in, the SDK stores the access + refresh
173
- tokens internally and refreshes them automatically on `401` you never handle tokens by hand.
206
+ For a persistent, imperative cart add/update/remove lines, then check out in one call use
207
+ `client.cart` instead of assembling line items by hand:
174
208
 
175
209
  ```ts
176
- // Register
177
- const session = await client.auth.register({
178
- email: "jane@example.com",
179
- password: "SecurePass123!",
180
- name: "Jane Doe",
181
- });
210
+ // Create a fresh cart, or resume one already persisted for this workspace
211
+ const cart = client.cart.create(); // client.cart.current() resumes a persisted cart instead
182
212
 
183
- // Login
184
- await client.auth.login("jane@example.com", "SecurePass123!");
213
+ // Add items — price, name, and image are looked up automatically via products.get()
214
+ await cart.addLineItem({ productId: "prod_123", variantId: "var_456", quantity: 2 });
215
+ await cart.addLineItem({ productId: "prod_123", variantId: "var_456", quantity: 1 }); // merges into the existing line
185
216
 
186
- // Google OAuth (pass a Google ID token)
187
- await client.auth.loginWithGoogle(googleIdToken);
217
+ console.log(cart.itemCount); // 3
218
+ console.log(cart.subtotal); // sum of unitPrice * quantity across all lines
188
219
 
189
- // Check state / log out
190
- if (client.auth.isAuthenticated()) {
191
- /* show account menu */
192
- }
220
+ cart.updateLineItem(cart.lines[0].id, 5); // set a line's quantity (quantity <= 0 removes it)
221
+ cart.removeLineItem(cart.lines[0].id);
222
+ cart.clear();
223
+
224
+ // Turn the cart directly into a checkout session
225
+ const session = await cart.checkout({ clearOnSuccess: true });
226
+ console.log(session.id, session.total);
227
+ ```
228
+
229
+ By default the cart is persisted to `localStorage` under a per-workspace key, so `client.cart.current()`
230
+ resumes it after a page reload. Pass `persistCart: false` to `createStorefrontClient` to keep it
231
+ in-memory only (tests, SSR, multi-cart UIs). All storage access is guarded, so the SDK still imports
232
+ and runs fine under Node/SSR where `localStorage` doesn't exist — the cart simply won't persist.
233
+
234
+ > `cart.checkout()` forwards the cart's line items to `client.checkout.create()` under the hood, so
235
+ > coupons/payment on the returned session work exactly as in [Cart & checkout](#cart--checkout).
236
+
237
+ ## Authentication
238
+
239
+ Most pages need **no auth**. When a customer logs in, the SDK stores the access + refresh tokens and
240
+ **auto-refreshes on `401`** — you never handle tokens by hand.
241
+
242
+ ```ts
243
+ await client.auth.register({ email: "jane@example.com", password: "SecurePass123!", name: "Jane Doe" });
244
+ await client.auth.login("jane@example.com", "SecurePass123!");
245
+ await client.auth.loginWithGoogle(googleIdToken); // pass a Google ID token
246
+
247
+ if (client.auth.isAuthenticated()) { /* show account menu */ }
193
248
  await client.auth.logout();
194
249
  ```
195
250
 
196
251
  ### Server-side (recommended)
197
252
 
198
- For production, run the SDK on your server (Next.js route handlers, Express, etc.) with httpOnly
199
- cookies so tokens never reach the browser. Restore a session from cookies with `setTokens`:
253
+ For production, run the SDK on your server with httpOnly cookies so tokens never reach the browser.
254
+ Restore a session with `setTokens`:
200
255
 
201
256
  ```ts
202
- // app/api/storefront/[...path]/route.ts
257
+ // app/api/storefront/[...path]/route.ts (Next.js)
203
258
  import { createStorefrontClient } from "@managesales/storefront";
204
259
  import { cookies } from "next/headers";
205
260
 
@@ -217,18 +272,48 @@ export async function GET() {
217
272
 
218
273
  ## Customer account
219
274
 
220
- Requires a logged-in session (see [Authentication](#authentication)).
275
+ Requires a logged-in session.
221
276
 
222
277
  ```ts
223
- const me = await client.customer.me(); // profile
278
+ const me = await client.customer.me(); // profile
224
279
  const { data: orders } = await client.customer.orders({ page: 1 }); // order history
225
- const addresses = await client.customer.addresses(); // saved addresses
280
+ const addresses = await client.customer.addresses(); // saved addresses
281
+ ```
282
+
283
+ `client.customers` mirrors `auth` + `customer` under one namespace, and `client.orders.list()` is a
284
+ shorthand for `client.customer.orders()` — same functions, same requests, just organized differently:
285
+
286
+ ```ts
287
+ await client.customers.login("jane@example.com", "SecurePass123!"); // === client.auth.login
288
+ const { data: orders } = await client.orders.list({ page: 1 }); // === client.customer.orders({ page: 1 })
289
+ ```
290
+
291
+ ## Retries
292
+
293
+ Network failures and `408` / `425` / `429` / `5xx` responses to idempotent (`GET`/`HEAD`) requests are
294
+ retried automatically with exponential backoff, honoring a `Retry-After` response header as a delay
295
+ floor when the server sends one. Non-idempotent requests (`POST`/`PUT`/`PATCH`/`DELETE`) are retried
296
+ **only** when the request never reached the server (e.g. `fetch` itself throws) — never after a server
297
+ response, since the side effect may already have happened.
298
+
299
+ ```ts
300
+ const client = createStorefrontClient({
301
+ workspaceId: "ws_123",
302
+ retry: {
303
+ retries: 2, // default: 2 additional attempts after the first
304
+ baseDelayMs: 300, // default: 300 — base for exponential backoff
305
+ maxDelayMs: 4000, // default: 4000 — cap on any single backoff delay
306
+ retryOnStatus: [408, 425, 429, 500, 502, 503, 504], // default shown
307
+ },
308
+ });
309
+
310
+ // Disable retries entirely
311
+ const noRetryClient = createStorefrontClient({ workspaceId: "ws_123", retry: false });
226
312
  ```
227
313
 
228
314
  ## Error handling
229
315
 
230
- Every non-2xx response throws a `StorefrontError` with structured fields, so you can branch
231
- precisely:
316
+ Every non-2xx response throws a `StorefrontError` with structured fields:
232
317
 
233
318
  ```ts
234
319
  import { StorefrontError } from "@managesales/storefront";
@@ -247,30 +332,13 @@ try {
247
332
  }
248
333
  ```
249
334
 
250
- ## More resources
251
-
252
- The client also exposes `client.blog`, `client.bookings`, and `client.forms`:
253
-
254
- ```ts
255
- const { data: posts } = await client.blog.list({ limit: 10 });
256
- const post = await client.blog.get("how-to-style-leather");
257
-
258
- const services = await client.bookings.services();
259
- const slots = await client.bookings.availability({ serviceId: "svc_1", date: "2026-08-15" });
260
- const booking = await client.bookings.book({
261
- serviceId: "svc_1",
262
- date: "2026-08-15",
263
- startTime: "10:00",
264
- customer: { name: "Jane Doe", email: "jane@example.com" },
265
- });
266
-
267
- const form = await client.forms.get("contact-us");
268
- await client.forms.submit(form.id, { name: "Jane", email: "jane@example.com", message: "Hi!" });
269
- ```
335
+ > **Resilience:** the client automatically retries a request **once** with a refreshed token when a
336
+ > `401` indicates an expired access token, and separately retries transient network/5xx failures per
337
+ > the [Retries](#retries) config.
270
338
 
271
339
  ## TypeScript support
272
340
 
273
- Written in TypeScript and shipped with declaration files — no `@types/*` package needed. Params and
341
+ Written in TypeScript and shipped with declaration files — no `@types/*` package. Params and
274
342
  responses are fully typed, including generics like `PaginatedResponse<Product>`:
275
343
 
276
344
  ```ts
@@ -288,18 +356,77 @@ const client = createStorefrontClient(config);
288
356
  const page: PaginatedResponse<Product> = await client.products.list();
289
357
  ```
290
358
 
359
+ ## Framework examples
360
+
361
+ **React**
362
+
363
+ ```tsx
364
+ import { useEffect, useState } from "react";
365
+ import { createStorefrontClient, type Product } from "@managesales/storefront";
366
+
367
+ const client = createStorefrontClient({ workspaceId: "ws_123" });
368
+
369
+ export function ProductGrid() {
370
+ const [products, setProducts] = useState<Product[]>([]);
371
+ useEffect(() => {
372
+ client.products.list({ limit: 12 }).then((res) => setProducts(res.data));
373
+ }, []);
374
+ return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
375
+ }
376
+ ```
377
+
378
+ **Next.js (Server Component)**
379
+
380
+ ```tsx
381
+ // app/page.tsx
382
+ import { createStorefrontClient } from "@managesales/storefront";
383
+
384
+ const client = createStorefrontClient({ workspaceId: "ws_123" });
385
+
386
+ export default async function Home() {
387
+ const { data: products } = await client.products.list({ limit: 12 });
388
+ return <ProductGrid products={products} />;
389
+ }
390
+ ```
391
+
392
+ **Node.js**
393
+
394
+ ```ts
395
+ import { createStorefrontClient } from "@managesales/storefront";
396
+
397
+ const client = createStorefrontClient({ workspaceId: "ws_123" });
398
+
399
+ const { data } = await client.products.list({ page: 1, limit: 20 });
400
+ console.log(data);
401
+ ```
402
+
291
403
  ## Browser & Node support
292
404
 
293
405
  - **Zero dependencies** — uses the platform's global `fetch`.
294
406
  - **Node.js ≥ 18** (global `fetch`). On older Node, pass `customFetch` (e.g. `undici`).
295
407
  - **Browsers** — all modern evergreen browsers.
296
- - **Deno, Bun, and edge runtimes** (Cloudflare Workers, Vercel Edge) — supported; inject
297
- `customFetch` where a global isn't available.
408
+ - **Deno, Bun, edge runtimes** (Cloudflare Workers, Vercel Edge) — supported; inject `customFetch`
409
+ where a global isn't available.
298
410
  - **Module formats** — ships both **ESM** (`import`) and **CommonJS** (`require`).
299
411
 
412
+ ## Versioning
413
+
414
+ Follows [Semantic Versioning](https://semver.org):
415
+
416
+ - **patch** (`x.y.Z`) — docs and bug fixes, no API change
417
+ - **minor** (`x.Y.z`) — additive, backward-compatible API
418
+ - **major** (`X.y.z`) — breaking changes to the client surface
419
+
420
+ The SDK version tracks the ManageSales Storefront API contract it targets. See the
421
+ [Changelog](#changelog).
422
+
423
+ ## Changelog
424
+
425
+ See [CHANGELOG.md](./CHANGELOG.md) for release notes.
426
+
300
427
  ## No-npm alternative
301
428
 
302
- Prefer not to add a dependency? The exact same client is served as a single file — download it from
429
+ Prefer no dependency? The identical client is served as a single file — download it from
303
430
  **Developers → SDKs** in the app, or curl it:
304
431
 
305
432
  ```bash
@@ -308,7 +435,7 @@ curl -o managesales-storefront.ts \
308
435
  ```
309
436
 
310
437
  Then `import { createStorefrontClient } from "./managesales-storefront"`. The npm package, the curl
311
- endpoint, and the in-app download are all generated from one source, so they're always identical.
438
+ endpoint, and the in-app download are generated from one source, so they're always identical.
312
439
 
313
440
  ## License
314
441