@frontdesk-africa/store-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Live Capital, Inc.
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 ADDED
@@ -0,0 +1,242 @@
1
+ # @frontdesk-africa/store-js
2
+
3
+ Typed client for the FrontDesk Storefront API. Build your own website or app on a merchant's
4
+ catalogue, events, forms and checkout.
5
+
6
+ It is deliberately thin: a fetch wrapper with the response types attached and the error envelope
7
+ turned into a real `Error`. It does not cache, retry or reshape anything, because a client that does
8
+ becomes a second implementation of the API's semantics.
9
+
10
+ Full reference for agents: `GET /v1/store/llms.txt`. OpenAPI: `GET /v1/store/openapi.json`.
11
+ MCP: `POST /v1/store/mcp`.
12
+
13
+ ---
14
+
15
+ ## The two keys
16
+
17
+ | Key | Where it goes | What it does |
18
+ |---|---|---|
19
+ | `fd_pk_…` | your **page**, safe to ship | read-only, and only from origins the merchant registered |
20
+ | `fd_sk_…` | your **server**, never a page | opens checkouts |
21
+
22
+ A publishable key is designed to be public. It is locked to exact origins, and an empty origin list
23
+ refuses everything — that is deliberate, not a misconfiguration. A secret key in browser code lets
24
+ anyone viewing source take payments as that merchant.
25
+
26
+ `fd_pk_test_…` / `fd_sk_test_…` work against staging; `live` against production. A key from the
27
+ wrong environment is refused.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pnpm add @frontdesk-africa/store-js
33
+ ```
34
+
35
+ ## Read (browser)
36
+
37
+ ```ts
38
+ import { createStoreClient } from '@frontdesk-africa/store-js'
39
+
40
+ const store = createStoreClient({
41
+ baseUrl: 'https://api.frontdesk.africa/v1',
42
+ key: process.env.NEXT_PUBLIC_FRONTDESK_PK!, // fd_pk_…
43
+ })
44
+
45
+ const site = await store.storefront() // brand, theme, sections, pages, embedded lists
46
+ const products = await store.products()
47
+ const item = await store.product('blue-mug')
48
+ ```
49
+
50
+ You never choose the workspace: the key does. Any workspace ref you send is ignored.
51
+
52
+ ## Checkout (server)
53
+
54
+ You never handle card details and you never price a cart. The buyer pays on a FrontDesk-hosted page
55
+ on the merchant's own domain, so their branding does not change mid-payment.
56
+
57
+ ```ts
58
+ // server only — fd_sk_…
59
+ const checkout = await store.createCheckout(
60
+ {
61
+ items: [{ variantRef: variant.ref, quantity: 2 }],
62
+ returnUrl: 'https://yourbrand.com/thanks',
63
+ },
64
+ `cart_${cart.id}`, // idempotency key: stable per cart, NOT random per attempt
65
+ )
66
+
67
+ redirect(checkout.hostedUrl)
68
+ ```
69
+
70
+ Then, when the buyer comes back:
71
+
72
+ ```ts
73
+ const done = await store.getCheckout(ref)
74
+ if (done.status === 'completed') fulfil(done.orderRef)
75
+ ```
76
+
77
+ **Do not fulfil on the redirect itself.** It is a browser navigation: it can be lost, replayed or
78
+ forged. `getCheckout` or the `checkout.completed` webhook are the only proof.
79
+
80
+ Event tickets work the same way, with the event telling you what to collect first:
81
+
82
+ ```ts
83
+ const event = await store.event('gala-night') // tiers carry formFields, requiresAttendeeDetails, min/max
84
+ const checkout = await store.createEventCheckout(
85
+ 'gala-night',
86
+ {
87
+ tickets: [{ ticketTypeRef: tier.ref, quantity: 2 }],
88
+ contact: { name, email },
89
+ returnUrl: 'https://yourbrand.com/thanks',
90
+ },
91
+ `evtcart_${cart.id}`,
92
+ )
93
+ redirect(checkout.hostedUrl)
94
+ ```
95
+
96
+ If the buyer walks away, `cancelCheckout(ref)` releases the seats straight away instead of holding
97
+ them until the session lapses. For carts that live in the buyer's storage, `availability(variantRefs)`
98
+ tells you which lines are still purchasable before you reopen a days-old cart.
99
+
100
+ `createHeadlessCheckout` (you render the payment step yourself) needs headless switched on for the
101
+ workspace, and one `provider` from `paymentMethods()` — see the API docs for the per-rail flow.
102
+
103
+ Prices from the read endpoints are for **display**. The order is priced when the buyer pays, so a
104
+ price that moved in between is resolved there, not by you.
105
+
106
+ ## Webhooks
107
+
108
+ ```ts
109
+ import { verifyWebhook } from '@frontdesk-africa/store-js'
110
+
111
+ export async function POST(req: Request) {
112
+ const raw = await req.text() // RAW body — verify before parsing
113
+ const ok = await verifyWebhook({
114
+ rawBody: raw,
115
+ signatureHeader: req.headers.get('x-fd-signature'),
116
+ timestampHeader: req.headers.get('x-fd-timestamp'),
117
+ secret: process.env.FRONTDESK_WEBHOOK_SECRET!,
118
+ })
119
+ if (!ok) return new Response('bad signature', { status: 400 })
120
+
121
+ const event = JSON.parse(raw)
122
+ // Return 2xx fast and do the work asynchronously; we retry with backoff for 24h.
123
+ queue(event)
124
+ return new Response('ok')
125
+ }
126
+ ```
127
+
128
+ Pass the **raw** body. `JSON.parse` then `JSON.stringify` will not reproduce the bytes we signed, and
129
+ the check will fail in a way that looks like a key mismatch.
130
+
131
+ Payloads are thin — refs only. Re-fetch through the API for detail, so a replayed delivery can never
132
+ present stale figures as current.
133
+
134
+ ## Test mode
135
+
136
+ A `fd_sk_test_…` key opens a test checkout. The hosted page shows a test banner and a
137
+ **Simulate a successful payment** button, so you can build the whole loop with no bank account and no
138
+ spend.
139
+
140
+ A simulated payment completes the session, sets a synthetic `test_ord_…` ref, and fires
141
+ `checkout.completed` with `"test": true`. It creates **no order**, credits nobody and posts no ledger
142
+ entry — so do not try to fetch that order ref. Test mode proves your integration, not fulfilment.
143
+
144
+ ---
145
+
146
+ ## Starters
147
+
148
+ ### Lovable / Replit prompt
149
+
150
+ Paste this, with your key:
151
+
152
+ > Build a storefront on the FrontDesk Storefront API.
153
+ >
154
+ > Read the full API reference first: https://api.frontdesk.africa/v1/store/llms.txt
155
+ >
156
+ > Use publishable key `fd_pk_live_…` for all reads, in the browser. Base URL
157
+ > `https://api.frontdesk.africa/v1`, `Authorization: Bearer <key>`.
158
+ >
159
+ > Pages: home rendering `GET /v1/store/storefront`, a product list from `GET /v1/store/products`, and
160
+ > a product page from `GET /v1/store/products/{slug}`.
161
+ >
162
+ > For checkout, call `POST /v1/store/checkouts` **from a server route, never the browser**, using the
163
+ > secret key, with an `Idempotency-Key` header derived from the cart id. Redirect the buyer to the
164
+ > `hostedUrl` you get back. Do not build a payment form and do not ask for card details.
165
+ >
166
+ > After the buyer returns, confirm with `GET /v1/store/checkouts/{ref}` server-side before showing a
167
+ > success page. Never trust the redirect alone.
168
+
169
+ The origin your app is served from must be registered on the publishable key, in the merchant's
170
+ FrontDesk portal under Settings → Website → Developers. Until it is, every call is refused.
171
+
172
+ ### Next.js (App Router)
173
+
174
+ ```ts
175
+ // lib/store.ts — browser-safe reads
176
+ import { createStoreClient } from '@frontdesk-africa/store-js'
177
+ export const store = createStoreClient({
178
+ baseUrl: process.env.NEXT_PUBLIC_FRONTDESK_API!,
179
+ key: process.env.NEXT_PUBLIC_FRONTDESK_PK!,
180
+ })
181
+
182
+ // lib/store.server.ts — server only, never imported by a client component
183
+ import 'server-only'
184
+ import { createStoreClient } from '@frontdesk-africa/store-js'
185
+ export const storeServer = createStoreClient({
186
+ baseUrl: process.env.FRONTDESK_API!,
187
+ key: process.env.FRONTDESK_SK!,
188
+ })
189
+ ```
190
+
191
+ ```ts
192
+ // app/api/checkout/route.ts
193
+ import { storeServer } from '@/lib/store.server'
194
+
195
+ export async function POST(req: Request) {
196
+ const { items, cartId } = await req.json()
197
+ const checkout = await storeServer.createCheckout(
198
+ { items, returnUrl: `${process.env.SITE_URL}/thanks` },
199
+ `cart_${cartId}`,
200
+ )
201
+ return Response.json({ hostedUrl: checkout.hostedUrl })
202
+ }
203
+ ```
204
+
205
+ The `server-only` import is the guard that matters: it turns "I accidentally imported the secret key
206
+ into a client component" from a silent production leak into a build error.
207
+
208
+ ### Expo / React Native
209
+
210
+ ```ts
211
+ const store = createStoreClient({
212
+ baseUrl: process.env.EXPO_PUBLIC_FRONTDESK_API!,
213
+ key: process.env.EXPO_PUBLIC_FRONTDESK_PK!,
214
+ })
215
+ ```
216
+
217
+ A native app has no browser `Origin`, so the origin lock does not apply the same way — treat the
218
+ publishable key as public (it is), keep the secret key on your own server, and open checkouts there.
219
+ Send the buyer to `hostedUrl` in a system browser or web view, then confirm server-side on return.
220
+
221
+ ## Errors
222
+
223
+ ```ts
224
+ import { StoreApiError } from '@frontdesk-africa/store-js'
225
+
226
+ try {
227
+ await store.product('nope')
228
+ } catch (e) {
229
+ if (e instanceof StoreApiError) {
230
+ e.code // 'NOT_FOUND' | 'ORIGIN_NOT_ALLOWED' | 'INSUFFICIENT_SCOPE' | 'RATE_LIMITED' | …
231
+ e.retryable // honour Retry-After on RATE_LIMITED
232
+ e.requestId // quote this when asking us for help
233
+ }
234
+ }
235
+ ```
236
+
237
+ `ORIGIN_NOT_ALLOWED` almost always means the origin is not on the key, or the key has no origins at
238
+ all. `INSUFFICIENT_SCOPE` means a publishable key tried something only a secret key can do.
239
+
240
+ `STORE_API_SECRET_DISABLED` means your workspace is not cleared for server-side (`fd_sk_`) calls yet —
241
+ ask us to switch it on. Publishable reads are not affected and keep working. `STORE_API_DISABLED` is
242
+ different: it means the whole workspace is switched off, and both kinds of key stop.