@gamecore-api/sdk 0.25.0 → 0.26.1
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/AGENTS.md +80 -0
- package/README.md +49 -0
- package/examples/01-quickstart.ts +80 -0
- package/examples/02-locale-switching.ts +56 -0
- package/examples/03-error-handling.ts +83 -0
- package/examples/04-webhook-verify.ts +105 -0
- package/package.json +4 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @gamecore-api/sdk — Agent Brief
|
|
2
|
+
|
|
3
|
+
> **For AI agents reading this:** this file is the short-path orientation. If
|
|
4
|
+
> the user is asking you to build with this SDK, read this first, then the
|
|
5
|
+
> [README](./README.md), then look at [`examples/`](./examples/) for working
|
|
6
|
+
> code you can adapt.
|
|
7
|
+
|
|
8
|
+
## What this is
|
|
9
|
+
|
|
10
|
+
Browser- and Node-safe TypeScript client for the GameCore API — a multi-tenant
|
|
11
|
+
BaaS for game-currency / digital-key storefronts. Zero runtime dependencies.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { GameCoreClient } from "@gamecore-api/sdk";
|
|
15
|
+
|
|
16
|
+
const gc = new GameCoreClient({
|
|
17
|
+
apiKey: "gc_live_…",
|
|
18
|
+
baseUrl: "https://api.gamecore-api.tech",
|
|
19
|
+
locale: "en", // ← drives Accept-Language; "ru" or "en"
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Required setup (don't forget any of these)
|
|
24
|
+
|
|
25
|
+
| Option | Where it comes from | What happens if omitted |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `apiKey` | Server env (`GAMECORE_API_KEY`) — **never** ship to browser bundles | All calls 401 |
|
|
28
|
+
| `baseUrl` | `https://api.gamecore-api.tech` (prod) or self-hosted | Constructor throws |
|
|
29
|
+
| `locale` | Optional. `"ru"` default, `"en"` for English storefronts | Storefront stays Russian |
|
|
30
|
+
| `onAuthError` | Optional callback to handle JWT expiry | Caller has to catch 401s manually |
|
|
31
|
+
|
|
32
|
+
`apiKey` is a **site-level** key. Each tenant ("site") has its own key. Don't
|
|
33
|
+
share keys across tenants.
|
|
34
|
+
|
|
35
|
+
## Namespaces (what to reach for)
|
|
36
|
+
|
|
37
|
+
| Namespace | Purpose | Read first |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `gc.catalog` | Games, products, search, categories, recommendations | `examples/01-quickstart.ts` |
|
|
40
|
+
| `gc.cart` | Server-synced cart for logged-in users | `examples/01-quickstart.ts` |
|
|
41
|
+
| `gc.checkout` | Create payment, get gateway redirect URL | `examples/01-quickstart.ts` |
|
|
42
|
+
| `gc.orders` | Order list, detail, status polling | `examples/03-error-handling.ts` |
|
|
43
|
+
| `gc.profile` | User profile, balance, address book | — |
|
|
44
|
+
| `gc.auth` | Telegram login, email/password, JWT refresh | — |
|
|
45
|
+
| `gc.favorites`, `api.coupons`, `api.referrals`, `api.reviews`, `api.topup`, `api.giftCards`, `api.announcements`, `api.analytics`, `api.seo` | Domain-specific endpoints | — |
|
|
46
|
+
| `gc.sse` | Server-Sent Events stream (order updates) | — |
|
|
47
|
+
| `gc.site` | Site config — homepage layout, footer, theme, modules | — |
|
|
48
|
+
|
|
49
|
+
Server-only helpers live under the `/server` entry point:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { verifyWebhookSignature } from "@gamecore-api/sdk/server";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
(See `examples/04-webhook-verify.ts`.)
|
|
56
|
+
|
|
57
|
+
## Locale (since 0.25.0)
|
|
58
|
+
|
|
59
|
+
- Pass `locale: "ru" | "en"` in constructor → SDK sends `Accept-Language` automatically on every request.
|
|
60
|
+
- Override at runtime: `gc.setLocale("en")` / `gc.getLocale()`.
|
|
61
|
+
- The backend overlays **game names**, short descriptions, and descriptions in the requested locale. Slugs / IDs never change.
|
|
62
|
+
- Original-language responses: just don't set `locale`.
|
|
63
|
+
|
|
64
|
+
## Pitfalls
|
|
65
|
+
|
|
66
|
+
- **Don't put `apiKey` in client-side JS bundles** — proxy through your own
|
|
67
|
+
backend (Next.js API routes, ElysiaJS, etc.) so the key stays server-side.
|
|
68
|
+
- **Catch `GameCoreError`** — every namespace throws this class on non-2xx.
|
|
69
|
+
See `examples/03-error-handling.ts` for the shape.
|
|
70
|
+
- **Cart endpoints require an authenticated user**. Call `api.auth.*` first.
|
|
71
|
+
- **`baseUrl` must NOT end in `/`** — the SDK strips trailing slashes but be
|
|
72
|
+
consistent (`https://api.gamecore-api.tech`, not `…/`).
|
|
73
|
+
- **Webhook verification** is a constant-time HMAC compare — never roll your
|
|
74
|
+
own with `==`. Use `verifyWebhookSignature` from `/server`.
|
|
75
|
+
|
|
76
|
+
## Where to look next
|
|
77
|
+
|
|
78
|
+
- [README.md](./README.md) — full API surface, options, browser/Node notes
|
|
79
|
+
- [examples/](./examples/) — runnable .ts files covering quickstart, locale, errors, webhooks
|
|
80
|
+
- [CHANGELOG.md](./CHANGELOG.md) — what's new per version
|
package/README.md
CHANGED
|
@@ -8,6 +8,55 @@ TypeScript SDK for GameCore API — zero external dependencies, browser-safe.
|
|
|
8
8
|
npm install @gamecore-api/sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## For AI agents
|
|
12
|
+
|
|
13
|
+
If an AI coding assistant is reading this, start with [**AGENTS.md**](./AGENTS.md)
|
|
14
|
+
for a short orientation, then look at runnable code in
|
|
15
|
+
[**examples/**](./examples/):
|
|
16
|
+
|
|
17
|
+
| File | Covers |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `examples/01-quickstart.ts` | catalog → checkout → status polling |
|
|
20
|
+
| `examples/02-locale-switching.ts` | RU/EN switching: constructor, runtime, per-call |
|
|
21
|
+
| `examples/03-error-handling.ts` | `GameCoreError`, status/code patterns, retries |
|
|
22
|
+
| `examples/04-webhook-verify.ts` | HMAC verification with `/server` entry point |
|
|
23
|
+
|
|
24
|
+
## Locale switching (RU / EN)
|
|
25
|
+
|
|
26
|
+
The SDK ships with built-in multilingual support. Pass `locale: "ru" | "en"`
|
|
27
|
+
to the constructor and every catalog / CMS response comes back in that
|
|
28
|
+
language. The client sends an `Accept-Language` header on every request;
|
|
29
|
+
the API resolves it against the unified `catalog_translations` store and
|
|
30
|
+
falls back to the base copy when a translation is missing.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { GameCoreClient, type SdkLocale } from "@gamecore-api/sdk";
|
|
34
|
+
|
|
35
|
+
const gc = new GameCoreClient({
|
|
36
|
+
apiKey: "gc_live_...",
|
|
37
|
+
baseUrl: "https://api.gamecore-api.tech",
|
|
38
|
+
locale: "en", // default for this client instance
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Runtime switch — wire this to a storefront language toggle:
|
|
42
|
+
gc.setLocale("ru");
|
|
43
|
+
const game = await gc.catalog.getGame("afk-journey");
|
|
44
|
+
// game.name / game.description / game.shortDescription are now in RU
|
|
45
|
+
|
|
46
|
+
// Per-call override still wins over the client default:
|
|
47
|
+
const enGame = await gc.catalog.getGame("afk-journey", "en");
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Supported locales: `"ru"` (default when nothing is passed) and `"en"`.
|
|
51
|
+
|
|
52
|
+
## What's new in 0.25.0
|
|
53
|
+
|
|
54
|
+
- **Locale switching (RU / EN).** New `locale` option on `GameCoreClient`
|
|
55
|
+
plus `setLocale` / `getLocale` runtime helpers. See section above.
|
|
56
|
+
- New exported type `SdkLocale = "ru" | "en"`.
|
|
57
|
+
- Backwards-compatible: clients that don't pass `locale` keep the previous
|
|
58
|
+
"server falls back to RU" behaviour.
|
|
59
|
+
|
|
11
60
|
## What's new in 0.14.0
|
|
12
61
|
|
|
13
62
|
- **BREAKING** `giftCards.purchase()` signature changed: first arg is now `amountRub` (was `amountUsd`). GiftCard payload fields renamed — `amount_usd` → `amount_rub`, added `currency`, `remainingBalance`, `expiresAt`. `denomination` is now optional (legacy)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 01 — Quickstart: catalog → checkout → status
|
|
3
|
+
*
|
|
4
|
+
* Minimum viable flow for a guest checkout. Run with:
|
|
5
|
+
*
|
|
6
|
+
* GAMECORE_API_KEY=gc_live_... bun run examples/01-quickstart.ts
|
|
7
|
+
*
|
|
8
|
+
* Don't put `apiKey` in client-side bundles — proxy through your own
|
|
9
|
+
* backend (Next.js route, Express, ElysiaJS, etc.).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { GameCoreClient } from "@gamecore-api/sdk";
|
|
13
|
+
|
|
14
|
+
const gc = new GameCoreClient({
|
|
15
|
+
apiKey: process.env.GAMECORE_API_KEY!,
|
|
16
|
+
baseUrl: "https://api.gamecore-api.tech",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 1. Browse the catalog (first 12 in-stock games)
|
|
20
|
+
const { data: games } = await gc.catalog.getGames({
|
|
21
|
+
limit: 12,
|
|
22
|
+
inStockOnly: true,
|
|
23
|
+
});
|
|
24
|
+
console.log(
|
|
25
|
+
"games:",
|
|
26
|
+
games.map((g) => g.slug),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// 2. Open a game detail page — pick the first in-stock game from the list
|
|
30
|
+
const game = games[0];
|
|
31
|
+
if (!game) {
|
|
32
|
+
console.log("No in-stock games for this site. Done.");
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
await gc.catalog.getGame(game.slug); // detail page lookup (returns GameDetail)
|
|
36
|
+
|
|
37
|
+
// 3. Pull products for that game — getGame returns metadata, not products.
|
|
38
|
+
const products = await gc.catalog.getProducts(game.slug);
|
|
39
|
+
const product = products[0];
|
|
40
|
+
if (!product) {
|
|
41
|
+
console.log(`Game ${game.slug} has no products. Done.`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
console.log("picked product:", product.id, product.name, "price:", product.price);
|
|
45
|
+
|
|
46
|
+
// 4. Create checkout (guest — pass email; no Telegram/VK login required).
|
|
47
|
+
// For balance-based payment, the user must be authenticated first.
|
|
48
|
+
// `amount` in items is the qty for products that have a fixed price
|
|
49
|
+
// OR the topup amount for variable-amount products; pass 1 for
|
|
50
|
+
// single-unit purchases.
|
|
51
|
+
const checkout = await gc.checkout.create({
|
|
52
|
+
email: "buyer@example.com",
|
|
53
|
+
items: [{ productId: product.id, amount: 1, deliveryData: {} }],
|
|
54
|
+
paymentMethod: "antilopay", // gateway slug from checkout.getPaymentMethods()
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!checkout.payment) {
|
|
58
|
+
console.error("Checkout did not return a payment record:", checkout);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
console.log("payment code:", checkout.payment.code);
|
|
62
|
+
console.log("payment URL:", checkout.payment.paymentUrl);
|
|
63
|
+
|
|
64
|
+
// 5. Poll status until the user pays (or 5 minutes pass)
|
|
65
|
+
for (let i = 0; i < 30; i++) {
|
|
66
|
+
const status = await gc.checkout.getStatus(checkout.payment.code);
|
|
67
|
+
console.log(`[${i}] status=${status.payment.status}`);
|
|
68
|
+
if (
|
|
69
|
+
status.payment.status === "completed" ||
|
|
70
|
+
status.payment.status === "failed" ||
|
|
71
|
+
status.payment.status === "cancelled"
|
|
72
|
+
) {
|
|
73
|
+
console.log(
|
|
74
|
+
"orders:",
|
|
75
|
+
status.orders.map((o) => ({ code: o.code, status: o.status })),
|
|
76
|
+
);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
await new Promise((r) => setTimeout(r, 10_000));
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 02 — Locale switching (RU / EN)
|
|
3
|
+
*
|
|
4
|
+
* Same site, different language. Backend overlays game names, short
|
|
5
|
+
* descriptions, and descriptions for the requested locale. Slugs and
|
|
6
|
+
* IDs never change — safe to share across languages.
|
|
7
|
+
*
|
|
8
|
+
* Three ways to pick a locale (highest priority first):
|
|
9
|
+
* 1. ?locale=en on a per-request basis
|
|
10
|
+
* 2. constructor option `locale: "en"` → adds Accept-Language header
|
|
11
|
+
* 3. nothing → API falls back to RU
|
|
12
|
+
*
|
|
13
|
+
* Run with:
|
|
14
|
+
* GAMECORE_API_KEY=gc_live_... bun run examples/02-locale-switching.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { GameCoreClient } from "@gamecore-api/sdk";
|
|
18
|
+
|
|
19
|
+
const apiKey = process.env.GAMECORE_API_KEY!;
|
|
20
|
+
const baseUrl = "https://api.gamecore-api.tech";
|
|
21
|
+
|
|
22
|
+
// Approach A — one client per locale (simplest)
|
|
23
|
+
const ru = new GameCoreClient({ apiKey, baseUrl, locale: "ru" });
|
|
24
|
+
const en = new GameCoreClient({ apiKey, baseUrl, locale: "en" });
|
|
25
|
+
|
|
26
|
+
const slug = "shp"; // a game we know has both RU and EN
|
|
27
|
+
const ruDetail = await ru.catalog.getGame(slug);
|
|
28
|
+
const enDetail = await en.catalog.getGame(slug);
|
|
29
|
+
console.log("RU name:", ruDetail.name);
|
|
30
|
+
console.log("EN name:", enDetail.name);
|
|
31
|
+
console.log("EN short:", enDetail.shortDescription);
|
|
32
|
+
|
|
33
|
+
// Approach B — single client, switch at runtime (useful for SSR
|
|
34
|
+
// where the locale comes from the request headers).
|
|
35
|
+
const gc = new GameCoreClient({ apiKey, baseUrl });
|
|
36
|
+
gc.setLocale("en");
|
|
37
|
+
console.log("locale now:", gc.getLocale());
|
|
38
|
+
const homepage = await gc.catalog.getHomepageGames();
|
|
39
|
+
console.log("homepage first:", homepage[0]?.name);
|
|
40
|
+
|
|
41
|
+
// Approach C — per-call override. Useful when most traffic is one
|
|
42
|
+
// locale but a specific page needs the other (e.g. an admin tool
|
|
43
|
+
// rendering an EN preview from an otherwise-RU client).
|
|
44
|
+
const enGame = await gc.catalog.getGame(slug, "en");
|
|
45
|
+
console.log("per-call EN:", enGame.name);
|
|
46
|
+
|
|
47
|
+
// Approach D — locale on the paginated list endpoint. Same per-call
|
|
48
|
+
// override pattern but via a typed option.
|
|
49
|
+
const { data: enGames } = await gc.catalog.getGames({
|
|
50
|
+
limit: 5,
|
|
51
|
+
locale: "en",
|
|
52
|
+
});
|
|
53
|
+
console.log(
|
|
54
|
+
"per-call EN list:",
|
|
55
|
+
enGames.map((g) => g.name),
|
|
56
|
+
);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 03 — Error handling
|
|
3
|
+
*
|
|
4
|
+
* Every namespace throws `GameCoreError` on non-2xx responses. Always
|
|
5
|
+
* branch on `.status` (numeric HTTP code) and optionally `.code`
|
|
6
|
+
* (machine-readable string set by the API for known cases).
|
|
7
|
+
*
|
|
8
|
+
* try {
|
|
9
|
+
* await gc.checkout.create(...);
|
|
10
|
+
* } catch (e) {
|
|
11
|
+
* if (e instanceof GameCoreError) {
|
|
12
|
+
* if (e.status === 401) { /* re-auth *\/ }
|
|
13
|
+
* if (e.status === 429) { /* backoff + retry *\/ }
|
|
14
|
+
* if (e.code === "INSUFFICIENT_FUNDS") { /* show top-up CTA *\/ }
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Run with:
|
|
19
|
+
* GAMECORE_API_KEY=gc_live_... bun run examples/03-error-handling.ts
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { GameCoreClient, GameCoreError } from "@gamecore-api/sdk";
|
|
23
|
+
|
|
24
|
+
const gc = new GameCoreClient({
|
|
25
|
+
apiKey: process.env.GAMECORE_API_KEY!,
|
|
26
|
+
baseUrl: "https://api.gamecore-api.tech",
|
|
27
|
+
|
|
28
|
+
// Optional: catch silent JWT expiry across all endpoints. Fires
|
|
29
|
+
// when a request returned 401 with code "TOKEN_EXPIRED" — useful
|
|
30
|
+
// for showing a "please log in again" toast without wrapping
|
|
31
|
+
// every call.
|
|
32
|
+
onAuthError: () => {
|
|
33
|
+
console.warn("auth expired — redirect to login");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Force a 404 to demonstrate the error shape
|
|
38
|
+
try {
|
|
39
|
+
await gc.catalog.getGame("definitely-not-a-real-game-slug-9999");
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (e instanceof GameCoreError) {
|
|
42
|
+
console.log("caught:", {
|
|
43
|
+
name: e.name,
|
|
44
|
+
status: e.status,
|
|
45
|
+
code: e.code,
|
|
46
|
+
message: e.message,
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Patterns worth knowing:
|
|
54
|
+
//
|
|
55
|
+
// • 401 + code "UNAUTHORIZED" → API key invalid or revoked. Stop
|
|
56
|
+
// the request, re-fetch a fresh key from your secret store.
|
|
57
|
+
// • 410 + code "TOKEN_EXPIRED" → user JWT expired. Triggers
|
|
58
|
+
// `onAuthError` if you provided one.
|
|
59
|
+
// • 429 + code "RATE_LIMITED" → token bucket. Read the `Retry-After`
|
|
60
|
+
// header if present, otherwise back off ~1s and retry.
|
|
61
|
+
// • 400 with field-level details → check `message` for the offending
|
|
62
|
+
// field (server returns plain-English reason for predictable cases
|
|
63
|
+
// like "amount must be ≥ 1" or "delivery data missing for product X").
|
|
64
|
+
|
|
65
|
+
// Retry helper with exponential backoff for transient errors
|
|
66
|
+
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
|
|
67
|
+
let lastError: unknown;
|
|
68
|
+
for (let i = 0; i < attempts; i++) {
|
|
69
|
+
try {
|
|
70
|
+
return await fn();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
lastError = e;
|
|
73
|
+
const transient =
|
|
74
|
+
e instanceof GameCoreError && (e.status === 429 || e.status >= 500);
|
|
75
|
+
if (!transient) throw e;
|
|
76
|
+
await new Promise((r) => setTimeout(r, 2 ** i * 500));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const games = await withRetry(() => gc.catalog.getHomepageGames());
|
|
83
|
+
console.log("got", games.length, "games");
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 04 — Webhook verification (server-side only)
|
|
3
|
+
*
|
|
4
|
+
* GameCore sends webhooks (order.completed, payment.received, etc.) to
|
|
5
|
+
* a URL you register in the site-admin. Each request is signed with
|
|
6
|
+
* HMAC-SHA256 using your webhook secret. ALWAYS verify before
|
|
7
|
+
* trusting the body — without verification, an attacker who guesses
|
|
8
|
+
* your webhook URL can fake order events.
|
|
9
|
+
*
|
|
10
|
+
* Import from "@gamecore-api/sdk/server", NOT the root entry, since
|
|
11
|
+
* webhook verification uses `node:crypto` and won't bundle for
|
|
12
|
+
* browsers.
|
|
13
|
+
*
|
|
14
|
+
* Below: a minimal Express handler. Same shape works for Bun, Next.js
|
|
15
|
+
* API routes, Hono, ElysiaJS, etc. — what matters is reading the
|
|
16
|
+
* RAW body (don't let your framework parse it before you verify).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
parseWebhookPayload,
|
|
21
|
+
verifyWebhookSignature,
|
|
22
|
+
} from "@gamecore-api/sdk/server";
|
|
23
|
+
|
|
24
|
+
// — Express example —
|
|
25
|
+
//
|
|
26
|
+
// Type imports are inlined as `unknown` here so the example type-checks
|
|
27
|
+
// without `@types/express` in this package. In a real project, replace
|
|
28
|
+
// with `import type { Request, Response } from "express"`.
|
|
29
|
+
type Request = {
|
|
30
|
+
body: string | Buffer;
|
|
31
|
+
header(name: string): string | undefined;
|
|
32
|
+
};
|
|
33
|
+
type Response = {
|
|
34
|
+
status(code: number): Response;
|
|
35
|
+
send(body: string): Response;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const WEBHOOK_SECRET = process.env.GAMECORE_WEBHOOK_SECRET!; // from site-admin
|
|
39
|
+
|
|
40
|
+
export async function handleWebhook(req: Request, res: Response) {
|
|
41
|
+
// Critical: raw body. In Express, add this middleware specifically
|
|
42
|
+
// for the webhook route:
|
|
43
|
+
// app.post("/gamecore-webhook",
|
|
44
|
+
// express.raw({ type: "application/json" }),
|
|
45
|
+
// handleWebhook);
|
|
46
|
+
const rawBody =
|
|
47
|
+
typeof req.body === "string" ? req.body : (req.body as Buffer).toString("utf8");
|
|
48
|
+
|
|
49
|
+
const signature = req.header("X-Webhook-Signature") ?? "";
|
|
50
|
+
|
|
51
|
+
const ok = verifyWebhookSignature(
|
|
52
|
+
rawBody,
|
|
53
|
+
signature,
|
|
54
|
+
WEBHOOK_SECRET,
|
|
55
|
+
300, // freshness window in seconds (default). 0 to disable.
|
|
56
|
+
);
|
|
57
|
+
if (!ok) {
|
|
58
|
+
res.status(401).send("invalid signature");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const payload = parseWebhookPayload(rawBody);
|
|
63
|
+
// payload.event is the WebhookEvent union; payload.data is
|
|
64
|
+
// `Record<string, unknown>` because the shape varies per event.
|
|
65
|
+
// Cast inside each branch when you've checked the discriminant.
|
|
66
|
+
switch (payload.event) {
|
|
67
|
+
case "order.completed": {
|
|
68
|
+
const data = payload.data as { orderCode?: string };
|
|
69
|
+
console.log("order delivered:", data.orderCode);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "payment.received": {
|
|
73
|
+
const data = payload.data as { paymentCode?: string; totalAmount?: number };
|
|
74
|
+
console.log("payment received:", data.paymentCode, data.totalAmount);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "order.failed":
|
|
78
|
+
case "order.cancelled":
|
|
79
|
+
console.log("order ended:", payload.event, payload.data);
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
console.log("unhandled event:", payload.event);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
res.status(200).send("ok");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// — Bun.serve example (no Express) —
|
|
89
|
+
//
|
|
90
|
+
// Bun.serve({
|
|
91
|
+
// port: 3000,
|
|
92
|
+
// async fetch(req) {
|
|
93
|
+
// if (new URL(req.url).pathname === "/gamecore-webhook") {
|
|
94
|
+
// const raw = await req.text();
|
|
95
|
+
// const sig = req.headers.get("X-Webhook-Signature") ?? "";
|
|
96
|
+
// if (!verifyWebhookSignature(raw, sig, WEBHOOK_SECRET)) {
|
|
97
|
+
// return new Response("invalid", { status: 401 });
|
|
98
|
+
// }
|
|
99
|
+
// const payload = parseWebhookPayload(raw);
|
|
100
|
+
// // ... switch on payload.event
|
|
101
|
+
// return new Response("ok");
|
|
102
|
+
// }
|
|
103
|
+
// return new Response("not found", { status: 404 });
|
|
104
|
+
// },
|
|
105
|
+
// });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamecore-api/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "TypeScript SDK for GameCore API — browser-safe, zero dependencies",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
"types": "./dist/server.d.ts"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"files": ["dist"],
|
|
19
|
+
"files": ["dist", "examples", "AGENTS.md"],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "bun build src/index.ts --outdir dist --target browser && bun build src/server.ts --outdir dist --target node && bun x tsc --emitDeclarationOnly",
|
|
22
|
-
"typecheck": "bun x tsc --noEmit"
|
|
22
|
+
"typecheck": "bun x tsc --noEmit",
|
|
23
|
+
"typecheck:examples": "bun x tsc --project tsconfig.examples.json"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^25.5.0",
|