@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 +21 -0
- package/README.md +242 -0
- package/dist/index.d.mts +2421 -0
- package/dist/index.d.ts +2421 -0
- package/dist/index.js +177 -0
- package/dist/index.mjs +142 -0
- package/package.json +50 -0
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.
|