@gamecore-api/sdk 0.25.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 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)
@@ -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.25.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"