@malawistore/store-sdk 0.0.0-stage → 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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +382 -2
- package/dist/index.cjs +1117 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +809 -0
- package/dist/index.d.ts +809 -0
- package/dist/index.js +1109 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-10-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `MalawiStore` client with resources for auth, users, categories, products,
|
|
14
|
+
variants, cart, inventory, orders, payments, wishlist, and reviews.
|
|
15
|
+
- Full coverage of the backend `/api/v1` surface, including admin writes,
|
|
16
|
+
inventory operations, and multipart image uploads.
|
|
17
|
+
- Automatic unwrapping of the `{ data }` success envelope.
|
|
18
|
+
- `MalawiStoreError` with stable API codes, HTTP status, `X-Request-ID`, and
|
|
19
|
+
`Retry-After` information.
|
|
20
|
+
- Automatic access-token rotation on `401` with single-flight refresh, token
|
|
21
|
+
change notifications, and an `onUnauthorized` hook.
|
|
22
|
+
- Timeouts, `AbortSignal` support, and exponential backoff retries for
|
|
23
|
+
idempotent `GET`/`HEAD` requests.
|
|
24
|
+
- `verifyStripeSignature` and `constructStripeEvent` helpers built on Web
|
|
25
|
+
Crypto.
|
|
26
|
+
- Dual ESM/CommonJS output with type declarations, source maps, and zero
|
|
27
|
+
runtime dependencies.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Malawi Store
|
|
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,3 +1,383 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @malawistore/store-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed TypeScript client for the **Malawi Store** e-commerce API — catalog,
|
|
4
|
+
cart, checkout, Stripe payments, wishlist, reviews, auth, and the full admin
|
|
5
|
+
surface.
|
|
6
|
+
|
|
7
|
+
- Zero runtime dependencies (uses the built-in `fetch`, `FormData`, and Web Crypto)
|
|
8
|
+
- Works in browsers, Node.js 20+, Bun, Deno, and edge runtimes
|
|
9
|
+
- ESM + CommonJS builds with complete type declarations
|
|
10
|
+
- Automatic envelope unwrapping and typed API errors
|
|
11
|
+
- Automatic access-token refresh on `401`
|
|
12
|
+
- Timeouts, backoff retries for safe requests, and request cancellation
|
|
13
|
+
- Stripe webhook signature verification helper
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @malawistore/store-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { MalawiStore } from "@malawistore/store-sdk";
|
|
25
|
+
|
|
26
|
+
const store = new MalawiStore({
|
|
27
|
+
baseUrl: "https://shop.malawi.mw/api/v1",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Browse the catalog without logging in
|
|
31
|
+
const categories = await store.categories.list();
|
|
32
|
+
const products = await store.products.list({ category_id: categories[0].id });
|
|
33
|
+
|
|
34
|
+
// Log in — the client stores the returned tokens for you
|
|
35
|
+
const session = await store.auth.login({
|
|
36
|
+
email: "aisha@example.mw",
|
|
37
|
+
password: "correct horse battery staple",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Authenticated calls now carry the access token
|
|
41
|
+
const me = await store.users.me();
|
|
42
|
+
const cart = await store.cart.get();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The default `baseUrl` is `http://localhost:8080/api/v1`, which matches the Go
|
|
46
|
+
backend's local development server.
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const store = new MalawiStore({
|
|
52
|
+
baseUrl: "https://shop.malawi.mw/api/v1", // full API base, including /api/v1
|
|
53
|
+
token: savedAccessToken, // optional starting access token
|
|
54
|
+
refreshToken: savedRefreshToken, // enables automatic refresh
|
|
55
|
+
timeoutMs: 30_000, // 0 disables the timeout
|
|
56
|
+
autoRefresh: true, // rotate once on 401, then retry
|
|
57
|
+
retry: { retries: 2, baseDelayMs: 250, maxDelayMs: 2_000 },
|
|
58
|
+
headers: { "X-Client": "storefront-web" },
|
|
59
|
+
onTokenChange: ({ token, refreshToken }) => {
|
|
60
|
+
// Persist however your app does it: localStorage, cookies, secure storage…
|
|
61
|
+
},
|
|
62
|
+
onUnauthorized: () => {
|
|
63
|
+
// Refresh failed or no refresh token: send the user to the login screen.
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Purpose |
|
|
69
|
+
| ----------------- | ----------------------------- | --------------------------- | -------------------------------------------------------------- |
|
|
70
|
+
| `baseUrl` | `string` | `http://localhost:8080/api/v1` | API base URL, including the version prefix |
|
|
71
|
+
| `token` | `string` | — | Access token to start with |
|
|
72
|
+
| `refreshToken` | `string` | — | Enables automatic session renewal |
|
|
73
|
+
| `fetch` | `FetchLike` | global `fetch` | Custom transport (tests, proxies, tracing) |
|
|
74
|
+
| `headers` | `Record<string, string>` | `{}` | Headers added to every request |
|
|
75
|
+
| `timeoutMs` | `number` | `30000` | Per-request timeout; `0` disables it |
|
|
76
|
+
| `autoRefresh` | `boolean` | `true` | Refresh once on `401` and replay the request |
|
|
77
|
+
| `retry` | `RetryOptions \| false` | `{ retries: 2, … }` | Retries `GET`/`HEAD` on `429`, `5xx`, and network errors |
|
|
78
|
+
| `healthUrl` | `string` | derived `<origin>/health` | Override the raw health-check URL |
|
|
79
|
+
| `onTokenChange` | `(tokens) => void` | — | Persist rotated tokens |
|
|
80
|
+
| `onUnauthorized` | `() => void` | — | Called when a request stays unauthorized |
|
|
81
|
+
|
|
82
|
+
### Session persistence
|
|
83
|
+
|
|
84
|
+
The SDK never touches `localStorage` itself. Persist the pair wherever your app
|
|
85
|
+
keeps secrets, then pass it back on the next boot:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const store = new MalawiStore({
|
|
89
|
+
baseUrl: "https://shop.malawi.mw/api/v1",
|
|
90
|
+
token: localStorage.getItem("access_token") ?? undefined,
|
|
91
|
+
refreshToken: localStorage.getItem("refresh_token") ?? undefined,
|
|
92
|
+
onTokenChange: ({ token, refreshToken }) => {
|
|
93
|
+
if (token) localStorage.setItem("access_token", token);
|
|
94
|
+
if (refreshToken) localStorage.setItem("refresh_token", refreshToken);
|
|
95
|
+
},
|
|
96
|
+
onUnauthorized: () => {
|
|
97
|
+
localStorage.removeItem("access_token");
|
|
98
|
+
localStorage.removeItem("refresh_token");
|
|
99
|
+
redirectToLogin();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Concurrent requests that hit `401` share a single refresh call, so a page with
|
|
105
|
+
several parallel queries does not rotate the token multiple times.
|
|
106
|
+
|
|
107
|
+
> ⚠️ Browser storage is readable by any script on the page. For high-value
|
|
108
|
+
> accounts, keep refresh tokens in an `HttpOnly` cookie on your own backend and
|
|
109
|
+
> proxy session handling. The API itself uses Authorization headers, not cookies.
|
|
110
|
+
|
|
111
|
+
## Resources
|
|
112
|
+
|
|
113
|
+
| Property | Covers |
|
|
114
|
+
| --------------------- | ------------------------------------------------------------------- |
|
|
115
|
+
| `store.auth` | Register, login, refresh, logout, email verification, password reset |
|
|
116
|
+
| `store.users` | Current account, user lookup, role management (admin) |
|
|
117
|
+
| `store.categories` | Taxonomy reads, admin CRUD, image upload |
|
|
118
|
+
| `store.products` | Catalog reads, admin CRUD, image upload |
|
|
119
|
+
| `store.variants` | SKU reads and admin CRUD |
|
|
120
|
+
| `store.cart` | Get, add, update quantity, remove, clear |
|
|
121
|
+
| `store.inventory` | Stock lookup and admin set/reserve/release/confirm |
|
|
122
|
+
| `store.orders` | Checkout, list, get, cancel |
|
|
123
|
+
| `store.payments` | Stripe payment intent creation |
|
|
124
|
+
| `store.wishlist` | List, add, remove |
|
|
125
|
+
| `store.reviews` | Product summary, create, edit, delete, admin moderation |
|
|
126
|
+
|
|
127
|
+
### Auth
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
await store.auth.register({ email, name, password });
|
|
131
|
+
await store.auth.login({ email, password });
|
|
132
|
+
await store.auth.refresh(); // rotate the stored refresh token
|
|
133
|
+
await store.auth.logout(); // revoke + clear local tokens
|
|
134
|
+
|
|
135
|
+
await store.auth.requestEmailVerification(email);
|
|
136
|
+
await store.auth.verifyEmail({ email, code: "482914" });
|
|
137
|
+
|
|
138
|
+
await store.auth.requestPasswordReset(email);
|
|
139
|
+
await store.auth.resetPassword({ token, new_password });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Register, login, and refresh store the returned `TokenPair` automatically.
|
|
143
|
+
Password reset clears local tokens because the API revokes sessions.
|
|
144
|
+
|
|
145
|
+
### Catalog
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const categories = await store.categories.list();
|
|
149
|
+
const category = await store.categories.getBySlug("cooking-oil");
|
|
150
|
+
|
|
151
|
+
const products = await store.products.list({
|
|
152
|
+
category_id: category.id,
|
|
153
|
+
active_only: true,
|
|
154
|
+
limit: 20,
|
|
155
|
+
offset: 0,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const detail = await store.products.getBySlug("sunseed-cooking-oil-2l");
|
|
159
|
+
console.log(detail.product.price_cents); // 12500 → K125.00
|
|
160
|
+
console.log(detail.variants); // sellable SKUs
|
|
161
|
+
|
|
162
|
+
const skus = await store.variants.list(detail.product.id);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For paginated grids use `listPage()`, which returns `{ data, meta }`:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const page = await store.products.listPage({
|
|
169
|
+
q: "dress",
|
|
170
|
+
category_slug: "dresses",
|
|
171
|
+
min_price_cents: 1_000_00,
|
|
172
|
+
max_price_cents: 9_000_00,
|
|
173
|
+
in_stock: true,
|
|
174
|
+
size: "XL",
|
|
175
|
+
min_rating: 4,
|
|
176
|
+
sort: "price_desc",
|
|
177
|
+
page: 2,
|
|
178
|
+
limit: 24,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
page.meta; // page, limit, offset, total, total_pages, has_next, has_prev
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Categories support `q`, `parent_id`, `root_only`, `has_products`, `sort`,
|
|
185
|
+
`page`, `limit` and `offset` through `categories.list()` or
|
|
186
|
+
`categories.listPage()`. `page` wins over `offset`; the API defaults to 20
|
|
187
|
+
rows and caps a page at 100.
|
|
188
|
+
|
|
189
|
+
Variant rows include `available` (on-hand minus reservations) and `in_stock`,
|
|
190
|
+
both in the product detail and `/products/:id/variants` responses.
|
|
191
|
+
|
|
192
|
+
Prices are integer **USD cents**: `12500` means `$125.00`. Never convert to
|
|
193
|
+
floating point for arithmetic. Historical orders may still carry MWK
|
|
194
|
+
tambala; the API converts those at charge time.
|
|
195
|
+
|
|
196
|
+
### Cart and checkout
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await store.cart.addItem({ product_id: "…", qty: 2 });
|
|
200
|
+
await store.cart.addItem({ product_id: "…", variant_id: "…", qty: 1 });
|
|
201
|
+
await store.cart.setQty("cart-line-id", 3);
|
|
202
|
+
await store.cart.removeItem("cart-line-id");
|
|
203
|
+
|
|
204
|
+
const cart = await store.cart.get();
|
|
205
|
+
console.log(cart.subtotal_cents);
|
|
206
|
+
|
|
207
|
+
// Freezes the cart, reserves stock, and clears the cart
|
|
208
|
+
const order = await store.orders.checkout();
|
|
209
|
+
|
|
210
|
+
// Create a Stripe intent and confirm it with Stripe.js
|
|
211
|
+
const payment = await store.payments.createIntent(order.id);
|
|
212
|
+
// stripe.confirmCardPayment(payment.client_secret, { … })
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Orders
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const orders = await store.orders.list();
|
|
219
|
+
const order = await store.orders.get(orderId);
|
|
220
|
+
const cancelled = await store.orders.cancel(orderId); // pending orders only
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Wishlist and reviews
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
await store.wishlist.add(productId);
|
|
227
|
+
const wishlist = await store.wishlist.list();
|
|
228
|
+
await store.wishlist.remove(productId); // product ID, not row ID
|
|
229
|
+
|
|
230
|
+
const summary = await store.reviews.summary(productId);
|
|
231
|
+
await store.reviews.create(productId, { rating: 5, title: "Great", body: "…" });
|
|
232
|
+
await store.reviews.update(reviewId, { rating: 4 });
|
|
233
|
+
await store.reviews.delete(reviewId);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Admin
|
|
237
|
+
|
|
238
|
+
All write methods require an account with the `admin` role.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const product = await store.products.create({
|
|
242
|
+
name: "Sunseed Cooking Oil 2L",
|
|
243
|
+
category_id: categoryId,
|
|
244
|
+
price_cents: 12500,
|
|
245
|
+
currency: "USD",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await store.products.update(product.id, { is_active: false });
|
|
249
|
+
await store.variants.create(product.id, { name: "500ml", sku: "OIL-500" });
|
|
250
|
+
await store.inventory.setStock({ product_id: product.id, qty: 50 });
|
|
251
|
+
|
|
252
|
+
await store.users.setRole(userId, { role: "admin" });
|
|
253
|
+
await store.reviews.deleteAny(reviewId); // moderation
|
|
254
|
+
|
|
255
|
+
// Admin user directory: filters + pagination
|
|
256
|
+
const users = await store.users.list({ role: "customer", q: "aisha", sort: "newest" });
|
|
257
|
+
const page = await store.users.listPage({ email_verified: false, page: 1, limit: 50 });
|
|
258
|
+
page.meta.total; // matching accounts
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Image uploads
|
|
262
|
+
|
|
263
|
+
Uploads are queued server-side (HTTP `202`) and applied by the worker once
|
|
264
|
+
Cloudinary finishes. The method resolves as soon as the job is accepted.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// Browser
|
|
268
|
+
const file = fileInput.files![0];
|
|
269
|
+
await store.products.uploadImage(productId, file);
|
|
270
|
+
|
|
271
|
+
// Node: wrap a Buffer
|
|
272
|
+
import { readFile } from "node:fs/promises";
|
|
273
|
+
const data = await readFile("./photo.jpg");
|
|
274
|
+
await store.categories.uploadImage(categoryId, {
|
|
275
|
+
data,
|
|
276
|
+
name: "photo.jpg",
|
|
277
|
+
type: "image/jpeg",
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Maximum size is 10 MiB per image.
|
|
282
|
+
|
|
283
|
+
## Error handling
|
|
284
|
+
|
|
285
|
+
Every failure is a `MalawiStoreError` with a stable `code`, the HTTP `status`,
|
|
286
|
+
and the API's `X-Request-ID` when available.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { isMalawiStoreError } from "@malawistore/store-sdk";
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await store.products.get("missing");
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (isMalawiStoreError(error)) {
|
|
295
|
+
switch (error.code) {
|
|
296
|
+
case "NOT_FOUND": /* show an empty state */ break;
|
|
297
|
+
case "UNAUTHORIZED": /* log in again */ break;
|
|
298
|
+
case "FORBIDDEN": /* not an admin */ break;
|
|
299
|
+
case "CONFLICT": /* duplicate / state conflict */ break;
|
|
300
|
+
case "RATE_LIMITED": /* back off for error.retryAfterMs */ break;
|
|
301
|
+
case "TIMEOUT": /* network too slow */ break;
|
|
302
|
+
default: /* INTERNAL, NETWORK_ERROR, PARSE_ERROR, ABORTED */
|
|
303
|
+
}
|
|
304
|
+
console.error(error.code, error.message, error.requestId);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
API codes: `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`,
|
|
310
|
+
`CONFLICT`, `RATE_LIMITED`, `INTERNAL`.
|
|
311
|
+
Client-side codes: `NETWORK_ERROR`, `TIMEOUT`, `ABORTED`, `PARSE_ERROR`.
|
|
312
|
+
|
|
313
|
+
Cancellation and per-request overrides are supported on every method via the
|
|
314
|
+
escape hatch:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const controller = new AbortController();
|
|
318
|
+
await store.request("GET", "/products", {
|
|
319
|
+
signal: controller.signal,
|
|
320
|
+
timeoutMs: 5_000,
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Stripe webhook verification
|
|
325
|
+
|
|
326
|
+
The Go API verifies its own Stripe webhooks. If you also receive events in an
|
|
327
|
+
edge function or a separate service, the SDK ships a dependency-free verifier
|
|
328
|
+
built on Web Crypto:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { constructStripeEvent, verifyStripeSignature } from "@malawistore/store-sdk";
|
|
332
|
+
|
|
333
|
+
// Edge function / Node handler
|
|
334
|
+
const payload = await request.text(); // raw body, never re-stringified
|
|
335
|
+
const signature = request.headers.get("stripe-signature")!;
|
|
336
|
+
|
|
337
|
+
if (!(await verifyStripeSignature({ payload, signature, secret }))) {
|
|
338
|
+
return new Response("invalid signature", { status: 400 });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const event = await constructStripeEvent({ payload, signature, secret });
|
|
342
|
+
if (event.type === "checkout.session.completed") {
|
|
343
|
+
// fulfil the order
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The verifier enforces a 5-minute timestamp tolerance by default
|
|
348
|
+
(`toleranceSeconds: 300`; set `0` to disable). It also accepts `Uint8Array`
|
|
349
|
+
payloads.
|
|
350
|
+
|
|
351
|
+
## Health check
|
|
352
|
+
|
|
353
|
+
`health()` calls the raw `/health` endpoint outside `/api/v1` and does not
|
|
354
|
+
unwrap an envelope:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
await store.health(); // { status: "ok" }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Runtime notes
|
|
361
|
+
|
|
362
|
+
- **Node.js 20+** is required (global `fetch`, `FormData`, and Web Crypto).
|
|
363
|
+
- In browsers, route requests through the same origin or enable CORS on the API
|
|
364
|
+
(`FRONTEND_URL` on the backend).
|
|
365
|
+
- The SDK sets `Content-Type: application/json` only when sending a JSON body;
|
|
366
|
+
multipart uploads let the runtime generate the boundary.
|
|
367
|
+
- All IDs in paths are URL-encoded.
|
|
368
|
+
- `request()` is available as an escape hatch for endpoints added after an SDK
|
|
369
|
+
release.
|
|
370
|
+
|
|
371
|
+
## Development
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
npm install
|
|
375
|
+
npm run typecheck # strict TypeScript
|
|
376
|
+
npm test # Vitest, 81 tests
|
|
377
|
+
npm run build # ESM + CJS + declarations
|
|
378
|
+
npm run pack:check # inspect the published tarball
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT
|