@gamecore-api/sdk 0.24.0 → 0.26.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/AGENTS.md +80 -0
- package/README.md +49 -0
- package/dist/client.d.ts +19 -0
- package/dist/index.js +11 -0
- package/examples/01-quickstart.ts +62 -0
- package/examples/02-locale-switching.ts +51 -0
- package/examples/03-error-handling.ts +83 -0
- package/examples/04-webhook-verify.ts +81 -0
- package/package.json +2 -2
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 { GameCore } from "@gamecore-api/sdk";
|
|
15
|
+
|
|
16
|
+
const api = new GameCore({
|
|
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
|
+
| `api.catalog` | Games, products, search, categories, recommendations | `examples/01-quickstart.ts` |
|
|
40
|
+
| `api.cart` | Server-synced cart for logged-in users | `examples/01-quickstart.ts` |
|
|
41
|
+
| `api.checkout` | Create payment, get gateway redirect URL | `examples/01-quickstart.ts` |
|
|
42
|
+
| `api.orders` | Order list, detail, status polling | `examples/03-error-handling.ts` |
|
|
43
|
+
| `api.profile` | User profile, balance, address book | — |
|
|
44
|
+
| `api.auth` | Telegram login, email/password, JWT refresh | — |
|
|
45
|
+
| `api.favorites`, `api.coupons`, `api.referrals`, `api.reviews`, `api.topup`, `api.giftCards`, `api.announcements`, `api.analytics`, `api.seo` | Domain-specific endpoints | — |
|
|
46
|
+
| `api.sse` | Server-Sent Events stream (order updates) | — |
|
|
47
|
+
| `api.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: `api.setLocale("en")` / `api.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)
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Announcement, AnnouncementBar, CartItem, CashbackPreview, CatalogSection, Category, CategoryInfo, CheckoutRequest, CheckoutResponse, CheckoutStatus, CmsArticleListResponse, CmsArticleLocale, CmsArticleResponse, CmsArticleType, CompleteWithBalanceResult, Conversation, ConversationDetail, CouponResult, DailyBonusClaimResult, DailyBonusStatus, DeliveryHelpResponse, ExchangeRates, FaqListResponse, Favorite, Game, GameDetail, GameRequestList, GiftCard, LegalDocument, LevelStatus, Notification, Order, PagedGamesResponse, PlatformInfo, PaginatedResponse, PaymentInfo, PaymentMethod, Product, ProductFilters, ProfileSummary, PublicCoupon, Quest, QuestCompleteResult, ReferralCommission, ReferralLink, ReferralPerformance, ReferralStats, ReferralTransferResult, Review, ReviewCreateResult, ReviewPolicy, ReviewProof, ReviewStats, ScreenshotsResponse, SearchResult, SiteConfig, SiteStats, SiteUIConfig, SystemRequirementsResponse, TelegramAuthResponse, TelegramBotLoginOptions, TelegramInitResponse, TelegramWidgetRenderOptions, TelegramWidgetUser, TopupMethod, TopupResponse, TopupStatus, Transaction, User, UserBalance, WebPushSubscriptionInput } from "./types";
|
|
2
|
+
/** Supported storefront locales — extend as new languages land. */
|
|
3
|
+
export type SdkLocale = "ru" | "en";
|
|
2
4
|
export interface GameCoreOptions {
|
|
3
5
|
/** Site API key (gc_live_xxx or gc_test_xxx) */
|
|
4
6
|
apiKey: string;
|
|
@@ -6,12 +8,29 @@ export interface GameCoreOptions {
|
|
|
6
8
|
baseUrl: string;
|
|
7
9
|
/** Called on 401 — use to redirect to login */
|
|
8
10
|
onAuthError?: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Default locale for catalog/CMS responses. When set, every request
|
|
13
|
+
* sends an `Accept-Language` header so the API returns localized
|
|
14
|
+
* name/description fields without each call passing `locale=` itself.
|
|
15
|
+
* Per-call `locale` arguments still take precedence over the default.
|
|
16
|
+
* Omit to keep the legacy behaviour (server falls back to "ru").
|
|
17
|
+
*/
|
|
18
|
+
locale?: SdkLocale;
|
|
9
19
|
}
|
|
10
20
|
export declare class GameCoreClient {
|
|
11
21
|
private apiKey;
|
|
12
22
|
private baseUrl;
|
|
13
23
|
private onAuthError?;
|
|
24
|
+
private defaultLocale?;
|
|
14
25
|
constructor(options: GameCoreOptions);
|
|
26
|
+
/**
|
|
27
|
+
* Switch the client's default locale at runtime — useful for a
|
|
28
|
+
* storefront language switcher that should not have to re-instantiate
|
|
29
|
+
* the client.
|
|
30
|
+
*/
|
|
31
|
+
setLocale(locale: SdkLocale | undefined): void;
|
|
32
|
+
/** Current default locale (undefined when none set). */
|
|
33
|
+
getLocale(): SdkLocale | undefined;
|
|
15
34
|
private request;
|
|
16
35
|
site: {
|
|
17
36
|
/** Get site configuration (modules, auth methods, payments, currency) */
|
package/dist/index.js
CHANGED
|
@@ -36,16 +36,27 @@ class GameCoreClient {
|
|
|
36
36
|
apiKey;
|
|
37
37
|
baseUrl;
|
|
38
38
|
onAuthError;
|
|
39
|
+
defaultLocale;
|
|
39
40
|
constructor(options) {
|
|
40
41
|
this.apiKey = options.apiKey;
|
|
41
42
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
42
43
|
this.onAuthError = options.onAuthError;
|
|
44
|
+
this.defaultLocale = options.locale;
|
|
45
|
+
}
|
|
46
|
+
setLocale(locale) {
|
|
47
|
+
this.defaultLocale = locale;
|
|
48
|
+
}
|
|
49
|
+
getLocale() {
|
|
50
|
+
return this.defaultLocale;
|
|
43
51
|
}
|
|
44
52
|
async request(method, path, body, options) {
|
|
45
53
|
const headers = {
|
|
46
54
|
"X-Api-Key": this.apiKey,
|
|
47
55
|
"Content-Type": "application/json"
|
|
48
56
|
};
|
|
57
|
+
if (this.defaultLocale) {
|
|
58
|
+
headers["Accept-Language"] = this.defaultLocale;
|
|
59
|
+
}
|
|
49
60
|
if (options?.idempotencyKey) {
|
|
50
61
|
headers["X-Idempotency-Key"] = options.idempotencyKey;
|
|
51
62
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { GameCore } from "@gamecore-api/sdk";
|
|
13
|
+
|
|
14
|
+
const api = new GameCore({
|
|
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 api.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 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
|
+
const detail = await api.catalog.getGame(game.slug);
|
|
36
|
+
const product = detail.products?.[0];
|
|
37
|
+
if (!product) {
|
|
38
|
+
console.log(`Game ${game.slug} has no products. Done.`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
console.log("picked product:", product.id, product.name);
|
|
42
|
+
|
|
43
|
+
// 3. Create checkout (guest — pass email; no Telegram/VK login required).
|
|
44
|
+
// For balance-based payment, the user must be authenticated first.
|
|
45
|
+
const checkout = await api.checkout.create({
|
|
46
|
+
items: [{ productId: product.id, qty: 1, deliveryData: {} }],
|
|
47
|
+
paymentMethod: "antilopay", // any gateway slug from getPaymentMethods()
|
|
48
|
+
email: "buyer@example.com",
|
|
49
|
+
});
|
|
50
|
+
console.log("payment URL:", checkout.gatewayPaymentUrl);
|
|
51
|
+
console.log("payment code:", checkout.paymentCode);
|
|
52
|
+
|
|
53
|
+
// 4. Poll status until the user pays (or 5 minutes pass)
|
|
54
|
+
for (let i = 0; i < 30; i++) {
|
|
55
|
+
const status = await api.checkout.getStatus(checkout.paymentCode);
|
|
56
|
+
console.log(`[${i}] status=${status.payment.status}`);
|
|
57
|
+
if (status.payment.status === "completed" || status.payment.status === "failed") {
|
|
58
|
+
console.log("orders:", status.orders);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
await new Promise((r) => setTimeout(r, 10_000));
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 → defaults to "ru"
|
|
12
|
+
*
|
|
13
|
+
* Run with:
|
|
14
|
+
* GAMECORE_API_KEY=gc_live_... bun run examples/02-locale-switching.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { GameCore } 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 GameCore({ apiKey, baseUrl, locale: "ru" });
|
|
24
|
+
const en = new GameCore({ 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 api = new GameCore({ apiKey, baseUrl });
|
|
36
|
+
api.setLocale("en");
|
|
37
|
+
console.log("locale now:", api.getLocale());
|
|
38
|
+
const homepage = await api.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 { data: enGames } = await api.catalog.getGames({
|
|
45
|
+
limit: 5,
|
|
46
|
+
locale: "en",
|
|
47
|
+
});
|
|
48
|
+
console.log(
|
|
49
|
+
"per-call EN:",
|
|
50
|
+
enGames.map((g) => g.name),
|
|
51
|
+
);
|
|
@@ -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 api.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 { GameCore, GameCoreError } from "@gamecore-api/sdk";
|
|
23
|
+
|
|
24
|
+
const api = new GameCore({
|
|
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 api.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
|
+
// • 401 + code "TOKEN_EXPIRED" → user session 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 "qty 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(() => api.catalog.getHomepageGames());
|
|
83
|
+
console.log("got", games.length, "games");
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 04 — Webhook verification (server-side only)
|
|
3
|
+
*
|
|
4
|
+
* GameCore sends webhooks (order.completed, payment.failed, 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 { verifyWebhookSignature, parseWebhookPayload } from "@gamecore-api/sdk/server";
|
|
20
|
+
|
|
21
|
+
// — Express example —
|
|
22
|
+
import type { Request, Response } from "express";
|
|
23
|
+
|
|
24
|
+
const WEBHOOK_SECRET = process.env.GAMECORE_WEBHOOK_SECRET!; // from site-admin
|
|
25
|
+
|
|
26
|
+
export async function handleWebhook(req: Request, res: Response) {
|
|
27
|
+
// Critical: raw body. In Express, add this middleware specifically
|
|
28
|
+
// for the webhook route:
|
|
29
|
+
// app.post("/gamecore-webhook",
|
|
30
|
+
// express.raw({ type: "application/json" }),
|
|
31
|
+
// handleWebhook);
|
|
32
|
+
const rawBody =
|
|
33
|
+
typeof req.body === "string" ? req.body : (req.body as Buffer).toString("utf8");
|
|
34
|
+
|
|
35
|
+
const signature = req.header("X-Webhook-Signature") ?? "";
|
|
36
|
+
|
|
37
|
+
const ok = verifyWebhookSignature(
|
|
38
|
+
rawBody,
|
|
39
|
+
signature,
|
|
40
|
+
WEBHOOK_SECRET,
|
|
41
|
+
300, // freshness window in seconds (default). 0 to disable.
|
|
42
|
+
);
|
|
43
|
+
if (!ok) {
|
|
44
|
+
res.status(401).send("invalid signature");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const event = parseWebhookPayload(rawBody);
|
|
49
|
+
switch (event.type) {
|
|
50
|
+
case "order.completed":
|
|
51
|
+
console.log("order delivered:", event.data.orderCode);
|
|
52
|
+
// e.g. flip your internal order state, send a fulfillment email, etc.
|
|
53
|
+
break;
|
|
54
|
+
case "payment.completed":
|
|
55
|
+
console.log("payment received:", event.data.paymentCode);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
console.log("unhandled event:", event.type);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
res.status(200).send("ok");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// — Bun.serve example (no Express) —
|
|
65
|
+
//
|
|
66
|
+
// Bun.serve({
|
|
67
|
+
// port: 3000,
|
|
68
|
+
// async fetch(req) {
|
|
69
|
+
// if (new URL(req.url).pathname === "/gamecore-webhook") {
|
|
70
|
+
// const raw = await req.text();
|
|
71
|
+
// const sig = req.headers.get("X-Webhook-Signature") ?? "";
|
|
72
|
+
// if (!verifyWebhookSignature(raw, sig, WEBHOOK_SECRET)) {
|
|
73
|
+
// return new Response("invalid", { status: 401 });
|
|
74
|
+
// }
|
|
75
|
+
// const event = parseWebhookPayload(raw);
|
|
76
|
+
// // ... handle event
|
|
77
|
+
// return new Response("ok");
|
|
78
|
+
// }
|
|
79
|
+
// return new Response("not found", { status: 404 });
|
|
80
|
+
// },
|
|
81
|
+
// });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamecore-api/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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,7 +16,7 @@
|
|
|
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
22
|
"typecheck": "bun x tsc --noEmit"
|