create-cartbase 0.1.20 → 0.1.22

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.
Files changed (35) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +24 -24
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +23 -4
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +270 -24
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/regions.md +269 -269
  16. package/template/app/docs/reviews.md +258 -238
  17. package/template/app/docs/search.md +227 -227
  18. package/template/app/docs/store.md +23 -1
  19. package/template/app/docs/subscriptions.md +148 -148
  20. package/template/app/docs/variables.md +331 -315
  21. package/template/app/package.json +1 -1
  22. package/template/app/postcss.config.cjs +11 -11
  23. package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
  25. package/template/app/src/app/checkout/error.tsx +23 -0
  26. package/template/app/src/app/checkout/page.tsx +16 -7
  27. package/template/app/src/app/globals.css +26 -26
  28. package/template/app/src/app/layout.tsx +126 -126
  29. package/template/app/src/app/page.tsx +37 -37
  30. package/template/app/src/app/products/[handle]/page.tsx +89 -89
  31. package/template/app/src/app/search/page.tsx +33 -33
  32. package/template/app/src/lib/browser-client.ts +35 -35
  33. package/template/app/src/lib/catalog.ts +120 -120
  34. package/template/app/src/lib/server-client.ts +25 -25
  35. package/template/app/src/lib/store-client.ts +16 -16
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Cartbase
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cartbase
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,25 +1,25 @@
1
- # create-cartbase
2
-
3
- Scaffold a storefront on the [Cartbase](https://cartbase.net) commerce
4
- platform. One input: your store's publishable key, handed out on the
5
- **Storefront** page of your Cartbase admin.
6
-
7
- ```bash
8
- npx create-cartbase@latest my-store --key pk_…
9
- # or
10
- npm create cartbase my-store -- --key pk_…
11
- bun create cartbase my-store --key pk_…
12
- ```
13
-
14
- You get a working Next.js storefront wired to your store through
15
- `@cartbase/storefront`, styled, with the **complete platform docs
16
- included in `docs/`** and a `CLAUDE.md` brief. Hand the folder to a
17
- coding agent and it has everything it needs offline. `--url` points the
18
- app at a local or staging platform; without it the app talks to the
19
- platform. Deploy with the
20
- [`cartbase`](https://www.npmjs.com/package/cartbase) CLI: `cartbase login`,
21
- then `cartbase deploy`.
22
-
23
- Run it in a folder of its own. Inside an existing project, Next.js takes
24
- that project's folder as the workspace root; the scaffold notices and
25
- prints the one line to add to `next.config.ts` if that is what you meant.
1
+ # create-cartbase
2
+
3
+ Scaffold a storefront on the [Cartbase](https://cartbase.net) commerce
4
+ platform. One input: your store's publishable key, handed out on the
5
+ **Storefront** page of your Cartbase admin.
6
+
7
+ ```bash
8
+ npx create-cartbase@latest my-store --key pk_…
9
+ # or
10
+ npm create cartbase my-store -- --key pk_…
11
+ bun create cartbase my-store --key pk_…
12
+ ```
13
+
14
+ You get a working Next.js storefront wired to your store through
15
+ `@cartbase/storefront`, styled, with the **complete platform docs
16
+ included in `docs/`** and a `CLAUDE.md` brief. Hand the folder to a
17
+ coding agent and it has everything it needs offline. `--url` points the
18
+ app at a local or staging platform; without it the app talks to the
19
+ platform. Deploy with the
20
+ [`cartbase`](https://www.npmjs.com/package/cartbase) CLI: `cartbase login`,
21
+ then `cartbase deploy`.
22
+
23
+ Run it in a folder of its own. Inside an existing project, Next.js takes
24
+ that project's folder as the workspace root; the scaffold notices and
25
+ prints the one line to add to `next.config.ts` if that is what you meant.
package/dist/index.js CHANGED
@@ -53,13 +53,13 @@ function envFile(input) {
53
53
  function main() {
54
54
  const { dir, flags } = parseArgs(process.argv.slice(2));
55
55
  if (!dir || flags.get("help") === true) {
56
- console.log(`create-cartbase — scaffold a Cartbase storefront
57
-
58
- Usage:
59
- npx create-cartbase@latest <directory> --key <pk_…>
60
-
61
- The key is your store's publishable key (Cartbase admin, Storefront page).
62
- Given as a flag it is written into .env.local; otherwise a placeholder is.
56
+ console.log(`create-cartbase — scaffold a Cartbase storefront
57
+
58
+ Usage:
59
+ npx create-cartbase@latest <directory> --key <pk_…>
60
+
61
+ The key is your store's publishable key (Cartbase admin, Storefront page).
62
+ Given as a flag it is written into .env.local; otherwise a placeholder is.
63
63
  --url points at a local or staging platform; the default is the platform.`);
64
64
  process.exitCode = dir ? 0 : 1;
65
65
  return;
@@ -86,21 +86,21 @@ Given as a flag it is written into .env.local; otherwise a placeholder is.
86
86
  // A store scaffolded inside another project inherits that project's
87
87
  // workspace root in Next's eyes; said once, here, while it is cheap to move.
88
88
  const enclosing = findEnclosingProject(target);
89
- console.log(`
90
- Created ${path.basename(target)}/
91
-
92
- Next steps:
93
- cd ${dir}
89
+ console.log(`
90
+ Created ${path.basename(target)}/
91
+
92
+ Next steps:
93
+ cd ${dir}
94
94
  bun install (or npm install)${key
95
95
  ? ""
96
- : `
97
- open .env.local and paste your store's publishable key
98
- (Cartbase admin, Storefront page)`}
99
- bun run dev
100
-
101
- The complete storefront reference is in docs/ — hand CLAUDE.md (or
102
- AGENTS.md) to your coding agent and it has everything. Deploy with the
103
- Cartbase CLI: cartbase login, then cartbase deploy.
96
+ : `
97
+ open .env.local and paste your store's publishable key
98
+ (Cartbase admin, Storefront page)`}
99
+ bun run dev
100
+
101
+ The complete storefront reference is in docs/ — hand CLAUDE.md (or
102
+ AGENTS.md) to your coding agent and it has everything. Deploy with the
103
+ Cartbase CLI: cartbase login, then cartbase deploy.
104
104
  ${enclosing ? `\n${enclosingProjectNote(target, enclosing)}\n` : ""}`);
105
105
  }
106
106
  main();
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
- {
2
- "name": "create-cartbase",
3
- "version": "0.1.20",
4
- "description": "Scaffold a Cartbase storefront: npm create cartbase my-store",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "create-cartbase": "./dist/index.js"
9
- },
10
- "files": [
11
- "dist",
12
- "template"
13
- ],
14
- "scripts": {
15
- "build": "bun scripts/build-template.mjs && tsc -p tsconfig.json",
16
- "prepack": "bun scripts/build-template.mjs && tsc -p tsconfig.json"
17
- },
18
- "engines": {
19
- "node": ">=20"
20
- },
21
- "devDependencies": {
22
- "typescript": "^5"
23
- }
24
- }
1
+ {
2
+ "name": "create-cartbase",
3
+ "version": "0.1.22",
4
+ "description": "Scaffold a Cartbase storefront: npm create cartbase my-store",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-cartbase": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "template"
13
+ ],
14
+ "scripts": {
15
+ "build": "bun scripts/build-template.mjs && tsc -p tsconfig.json",
16
+ "prepack": "bun scripts/build-template.mjs && tsc -p tsconfig.json"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5"
23
+ }
24
+ }
@@ -1,105 +1,105 @@
1
- # Auth
2
-
3
- Passwordless email-code login (customer-accounts card, Shopify Customer
4
- Account API direction). Two calls: **request** emails a 6-digit code,
5
- **verify** exchanges it for a session. Password login
6
- (`supabase.auth.signInWithPassword` client-side) stays available and yields
7
- an equivalent JWT — nothing here is needed for it.
8
-
9
- ## The session discipline (one rule)
10
-
11
- `verify` mints a **real Supabase session**. Its `access_token` is the SAME
12
- `authorization: Bearer <jwt>` every `/api/store/*` route accepts — wire it
13
- into `StorefrontClient` once and every customer surface (customers/me,
14
- orders, documents, cart attach) is authenticated:
15
-
16
- ```jsonc
17
- // new StorefrontClient({ baseUrl, clientId, getAuthToken: () => storedAccessToken })
18
- ```
19
-
20
- Persist `access_token` + `refresh_token`; refresh client-side with the
21
- Supabase SDK (`setSession` → `refreshSession`) — the Cartbase API does not
22
- proxy token refresh. `expires_in` is seconds; `expires_at` epoch-seconds.
23
-
24
- ## Server-enforced security posture (the SDK adds nothing)
25
-
26
- - Request NEVER reveals whether the email is registered — `{ok: true}`
27
- always (no enumeration oracle). The code exists ONLY in the sent email.
28
- - Codes: hashed at rest, TTL **10 min**, single-use, one live code per
29
- email (a new request supersedes prior codes), **5-wrong-attempt lockout**.
30
- - Rate limits per 15-min window: **5 requests per email**, **20 per IP** →
31
- `429 rate_limited` — the only distinguishable request failure.
32
- - Every verify failure — wrong code, expired, consumed, locked out, unknown
33
- email — is the SAME `401 invalid_code`.
34
-
35
- ## POST /api/store/auth/code/request — step 1
36
-
37
- - **Purpose**: email a 6-digit one-time login code. First-time emails
38
- register lazily at VERIFY, not here.
39
- - **Auth**: anon (`x-client-id` only).
40
- - **Request**: `{email: string}`.
41
- - **Response**: `{ok: true}` — always, by design.
42
- - **Errors**: `429 rate_limited` · `400 validation_failed` (malformed
43
- email) · `400 missing_client_id`.
44
- - **SDK**: `auth.requestLoginCode(client, {email})`
45
- - **Components**: login form (account pages family).
46
- - **Settings**: the `auth-code` notification template carries the code.
47
-
48
- ```bash
49
- # The request contract: {ok:true}, nothing else — no code echo, no
50
- # registered-or-not oracle. RUN-stamped email → no rate-limit collisions.
51
- curl -sf -X POST "$BASE/api/store/auth/code/request" \
52
- -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
53
- -d '{"email": "doc-auth-'$RUN'@doc.test"}' \
54
- | grep -q '^{"ok":true}$'
55
- ```
56
-
57
- ## POST /api/store/auth/code/verify — step 2
58
-
59
- - **Purpose**: exchange (email, code) for a session. This is ALSO
60
- passwordless **registration** — a first-time email lazy-creates the
61
- customer (`account_status` per store policy) and triggers the welcome
62
- email.
63
- - **Auth**: anon (`x-client-id` only).
64
- - **Request**: `{email: string, code: string}` — code is exactly 6 digits
65
- (leading zeros count; any other shape → `400 validation_failed`).
66
- - **Response**:
67
-
68
- ```jsonc
69
- {
70
- "access_token": "eyJ…", // the Bearer JWT for every store route
71
- "refresh_token": "…",
72
- "expires_in": 3600, // seconds
73
- "expires_at": 1789300000, // epoch seconds (may be absent)
74
- "token_type": "bearer",
75
- "customer": { /* full customer + addresses — see customers.md */ }
76
- }
77
- ```
78
-
79
- - **Errors**: `401 invalid_code` (every auth failure, generic) ·
80
- `400 validation_failed` · `400 missing_client_id`.
81
- - **SDK**: `auth.verifyLoginCode(client, {email, code})`
82
- - **Components**: code-entry form (account pages family).
83
- - **Settings**: store approval policy (new-customer `account_status`);
84
- `account-welcome` template.
85
-
86
- ```bash
87
- # The 401 invalid_code contract — a well-formed but wrong code against the
88
- # RUN-stamped email (its real code lives only in the email we never read).
89
- STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
90
- -X POST "$BASE/api/store/auth/code/verify" \
91
- -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
92
- -d '{"email": "doc-auth-'$RUN'@doc.test", "code": "000000"}')
93
- test "$STATUS" = 401
94
- curl -s -X POST "$BASE/api/store/auth/code/verify" \
95
- -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
96
- -d '{"email": "doc-auth-'$RUN'@doc.test", "code": "000000"}' \
97
- | grep -q '"code":"invalid_code"'
98
- ```
99
-
100
- > Cleanup note: the code rows this doc creates are single-per-run
101
- > (RUN-stamped email), superseded on any later request and dead after the
102
- > 10-minute TTL — there is no store-surface delete for them by design (they
103
- > are the auth trail). The happy-path session mint is exercised with a real
104
- > inbox-free code by `tests/store/customer-accounts-auth.test.ts` and the
105
- > SDK contract test (server-side code issue via the lib).
1
+ # Auth
2
+
3
+ Passwordless email-code login (customer-accounts card, Shopify Customer
4
+ Account API direction). Two calls: **request** emails a 6-digit code,
5
+ **verify** exchanges it for a session. Password login
6
+ (`supabase.auth.signInWithPassword` client-side) stays available and yields
7
+ an equivalent JWT — nothing here is needed for it.
8
+
9
+ ## The session discipline (one rule)
10
+
11
+ `verify` mints a **real Supabase session**. Its `access_token` is the SAME
12
+ `authorization: Bearer <jwt>` every `/api/store/*` route accepts — wire it
13
+ into `StorefrontClient` once and every customer surface (customers/me,
14
+ orders, documents, cart attach) is authenticated:
15
+
16
+ ```jsonc
17
+ // new StorefrontClient({ baseUrl, clientId, getAuthToken: () => storedAccessToken })
18
+ ```
19
+
20
+ Persist `access_token` + `refresh_token`; refresh client-side with the
21
+ Supabase SDK (`setSession` → `refreshSession`) — the Cartbase API does not
22
+ proxy token refresh. `expires_in` is seconds; `expires_at` epoch-seconds.
23
+
24
+ ## Server-enforced security posture (the SDK adds nothing)
25
+
26
+ - Request NEVER reveals whether the email is registered — `{ok: true}`
27
+ always (no enumeration oracle). The code exists ONLY in the sent email.
28
+ - Codes: hashed at rest, TTL **10 min**, single-use, one live code per
29
+ email (a new request supersedes prior codes), **5-wrong-attempt lockout**.
30
+ - Rate limits per 15-min window: **5 requests per email**, **20 per IP** →
31
+ `429 rate_limited` — the only distinguishable request failure.
32
+ - Every verify failure — wrong code, expired, consumed, locked out, unknown
33
+ email — is the SAME `401 invalid_code`.
34
+
35
+ ## POST /api/store/auth/code/request — step 1
36
+
37
+ - **Purpose**: email a 6-digit one-time login code. First-time emails
38
+ register lazily at VERIFY, not here.
39
+ - **Auth**: anon (`x-client-id` only).
40
+ - **Request**: `{email: string}`.
41
+ - **Response**: `{ok: true}` — always, by design.
42
+ - **Errors**: `429 rate_limited` · `400 validation_failed` (malformed
43
+ email) · `400 missing_client_id`.
44
+ - **SDK**: `auth.requestLoginCode(client, {email})`
45
+ - **Components**: login form (account pages family).
46
+ - **Settings**: the `auth-code` notification template carries the code.
47
+
48
+ ```bash
49
+ # The request contract: {ok:true}, nothing else — no code echo, no
50
+ # registered-or-not oracle. RUN-stamped email → no rate-limit collisions.
51
+ curl -sf -X POST "$BASE/api/store/auth/code/request" \
52
+ -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
53
+ -d '{"email": "doc-auth-'$RUN'@doc.test"}' \
54
+ | grep -q '^{"ok":true}$'
55
+ ```
56
+
57
+ ## POST /api/store/auth/code/verify — step 2
58
+
59
+ - **Purpose**: exchange (email, code) for a session. This is ALSO
60
+ passwordless **registration** — a first-time email lazy-creates the
61
+ customer (`account_status` per store policy) and triggers the welcome
62
+ email.
63
+ - **Auth**: anon (`x-client-id` only).
64
+ - **Request**: `{email: string, code: string}` — code is exactly 6 digits
65
+ (leading zeros count; any other shape → `400 validation_failed`).
66
+ - **Response**:
67
+
68
+ ```jsonc
69
+ {
70
+ "access_token": "eyJ…", // the Bearer JWT for every store route
71
+ "refresh_token": "…",
72
+ "expires_in": 3600, // seconds
73
+ "expires_at": 1789300000, // epoch seconds (may be absent)
74
+ "token_type": "bearer",
75
+ "customer": { /* full customer + addresses — see customers.md */ }
76
+ }
77
+ ```
78
+
79
+ - **Errors**: `401 invalid_code` (every auth failure, generic) ·
80
+ `400 validation_failed` · `400 missing_client_id`.
81
+ - **SDK**: `auth.verifyLoginCode(client, {email, code})`
82
+ - **Components**: code-entry form (account pages family).
83
+ - **Settings**: store approval policy (new-customer `account_status`);
84
+ `account-welcome` template.
85
+
86
+ ```bash
87
+ # The 401 invalid_code contract — a well-formed but wrong code against the
88
+ # RUN-stamped email (its real code lives only in the email we never read).
89
+ STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
90
+ -X POST "$BASE/api/store/auth/code/verify" \
91
+ -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
92
+ -d '{"email": "doc-auth-'$RUN'@doc.test", "code": "000000"}')
93
+ test "$STATUS" = 401
94
+ curl -s -X POST "$BASE/api/store/auth/code/verify" \
95
+ -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
96
+ -d '{"email": "doc-auth-'$RUN'@doc.test", "code": "000000"}' \
97
+ | grep -q '"code":"invalid_code"'
98
+ ```
99
+
100
+ > Cleanup note: the code rows this doc creates are single-per-run
101
+ > (RUN-stamped email), superseded on any later request and dead after the
102
+ > 10-minute TTL — there is no store-surface delete for them by design (they
103
+ > are the auth trail). The happy-path session mint is exercised with a real
104
+ > inbox-free code by `tests/store/customer-accounts-auth.test.ts` and the
105
+ > SDK contract test (server-side code issue via the lib).
@@ -47,10 +47,20 @@ SDK module: `@cartbase/storefront/api/carts`.
47
47
  ```
48
48
 
49
49
  - **Response** — `201 {cart}` (decorated shape below).
50
+ - **Limits**: `items` carries at most **100** entries and `promo_codes` at
51
+ most **10**. A longer array is `400 validation_failed`, never a silent
52
+ trim.
50
53
  - **Errors** — 400 `validation_failed` | `invalid_region` | `region_required`
51
54
  (no `region_id` and no store default) | `invalid_sales_channel` (channel
52
55
  outside the publishable key's scope) | `price_not_found` (an initial item
53
56
  has no price in the cart currency); 404 `variant_not_found`.
57
+
58
+ > **A saved basket longer than 100 lines.** The browser's basket is the
59
+ > shopper's, and it can outlive any cart, so restore it in two moves: send
60
+ > the first 100 entries in `items`, then add the remainder one at a time
61
+ > through `POST /carts/:id/line-items`. A cart itself holds 100 lines, so an
62
+ > add past that answers `400 cart_item_limit` and the storefront tells the
63
+ > shopper which lines did not fit rather than dropping them without a word.
54
64
  - **SDK** — `carts.createCart(client, input)`.
55
65
  - **Components** — cart drawer / add-to-cart buttons.
56
66
  - **Settings** — store default region (`stores.default_region_id`); enabled
@@ -227,12 +237,18 @@ test "$STATUS" = 401
227
237
  the storefront never computes it. The line comes back with
228
238
  `selling_plan_id` set.
229
239
  - **Response** — `200 {cart}`.
240
+ - **Limits**: a cart holds at most **100** lines. An add that would create
241
+ the 101st is `400 cart_item_limit` with `details {limit, lines}`; bumping
242
+ the quantity of a line the cart already holds is not a new line and is
243
+ never refused. The cap holds under concurrent adds too: when several adds
244
+ race for the last free line, one lands and the others answer
245
+ `400 cart_item_limit`, so show that message rather than retrying.
230
246
  - **Errors** — 404 `cart_not_found` | `variant_not_found`; 409
231
247
  `cart_completed`; 400 `insufficient_inventory` (kit-aware — every linked
232
248
  inventory component is checked; `details` carries `{variant_id,
233
- inventory_item_id, available, requested}`) | `price_not_found` |
234
- `invalid_selling_plan` (unknown, disabled, or not attached to this
235
- product) | `validation_failed`.
249
+ inventory_item_id, available, requested}`) | `cart_item_limit` |
250
+ `price_not_found` | `invalid_selling_plan` (unknown, disabled, or not
251
+ attached to this product) | `validation_failed`.
236
252
  - **SDK** — `carts.addLineItem(client, cartId, input)`.
237
253
  - **Components** — PDP add-to-cart, cart drawer upsell.
238
254
  - **Settings** — B2B price lists (cart customer's groups); gift-card
@@ -306,7 +322,10 @@ curl -sf -X DELETE "$BASE/api/store/carts/$CART_ID/line-items/$LINE_ID" \
306
322
  their own — only coded ones travel through here).
307
323
  - **Auth** — anon `x-client-id`.
308
324
  - **Request** — `{promo_codes: string[]}` on BOTH verbs (the DELETE reads
309
- its body, not query params).
325
+ its body, not query params). At most **10** codes per call
326
+ (`MAX_PROMO_CODES`); a longer array is `400 validation_failed` on both
327
+ verbs. More than ten codes on one cart is more than any campaign stacks,
328
+ so send them in calls of ten if a saved cart ever carries more.
310
329
  - **Response** — `200 {cart}` (decorated; `cart.promotions` is the raw
311
330
  pivot embed `[{promotion: {...}}]`). Unknown codes on REMOVE silently
312
331
  no-op by design; on ADD they error.