create-cartbase 0.1.20 → 0.1.21
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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +24 -24
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +23 -4
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +270 -24
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +258 -238
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +23 -1
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +331 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
- package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
- package/template/app/src/app/checkout/error.tsx +23 -0
- package/template/app/src/app/checkout/page.tsx +16 -7
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/layout.tsx +126 -126
- package/template/app/src/app/page.tsx +37 -37
- package/template/app/src/app/products/[handle]/page.tsx +89 -89
- package/template/app/src/app/search/page.tsx +33 -33
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/catalog.ts +120 -120
- package/template/app/src/lib/server-client.ts +25 -25
- 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.
|
|
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.21",
|
|
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}`) | `
|
|
234
|
-
`invalid_selling_plan` (unknown, disabled, or not
|
|
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.
|