@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 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.25.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",