@gamecore-api/sdk 0.26.0 → 0.26.2
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 +12 -12
- package/examples/01-quickstart.ts +53 -17
- package/examples/02-locale-switching.ts +15 -10
- package/examples/03-error-handling.ts +7 -7
- package/examples/04-webhook-verify.ts +37 -13
- package/package.json +3 -2
package/AGENTS.md
CHANGED
|
@@ -11,9 +11,9 @@ Browser- and Node-safe TypeScript client for the GameCore API — a multi-tenant
|
|
|
11
11
|
BaaS for game-currency / digital-key storefronts. Zero runtime dependencies.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import {
|
|
14
|
+
import { GameCoreClient } from "@gamecore-api/sdk";
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const gc = new GameCoreClient({
|
|
17
17
|
apiKey: "gc_live_…",
|
|
18
18
|
baseUrl: "https://api.gamecore-api.tech",
|
|
19
19
|
locale: "en", // ← drives Accept-Language; "ru" or "en"
|
|
@@ -36,15 +36,15 @@ share keys across tenants.
|
|
|
36
36
|
|
|
37
37
|
| Namespace | Purpose | Read first |
|
|
38
38
|
|---|---|---|
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
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
48
|
|
|
49
49
|
Server-only helpers live under the `/server` entry point:
|
|
50
50
|
|
|
@@ -57,7 +57,7 @@ import { verifyWebhookSignature } from "@gamecore-api/sdk/server";
|
|
|
57
57
|
## Locale (since 0.25.0)
|
|
58
58
|
|
|
59
59
|
- Pass `locale: "ru" | "en"` in constructor → SDK sends `Accept-Language` automatically on every request.
|
|
60
|
-
- Override at runtime: `
|
|
60
|
+
- Override at runtime: `gc.setLocale("en")` / `gc.getLocale()`.
|
|
61
61
|
- The backend overlays **game names**, short descriptions, and descriptions in the requested locale. Slugs / IDs never change.
|
|
62
62
|
- Original-language responses: just don't set `locale`.
|
|
63
63
|
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
* backend (Next.js route, Express, ElysiaJS, etc.).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { GameCoreClient } from "@gamecore-api/sdk";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const gc = new GameCoreClient({
|
|
15
15
|
apiKey: process.env.GAMECORE_API_KEY!,
|
|
16
16
|
baseUrl: "https://api.gamecore-api.tech",
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
// 1. Browse the catalog (first 12 in-stock games)
|
|
20
|
-
const { data: games } = await
|
|
20
|
+
const { data: games } = await gc.catalog.getGames({
|
|
21
21
|
limit: 12,
|
|
22
22
|
inStockOnly: true,
|
|
23
23
|
});
|
|
@@ -26,36 +26,72 @@ console.log(
|
|
|
26
26
|
games.map((g) => g.slug),
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
-
// 2. Open a game detail page — pick the first in-stock game from list
|
|
29
|
+
// 2. Open a game detail page — pick the first in-stock game from the list
|
|
30
30
|
const game = games[0];
|
|
31
31
|
if (!game) {
|
|
32
32
|
console.log("No in-stock games for this site. Done.");
|
|
33
33
|
process.exit(0);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
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];
|
|
37
40
|
if (!product) {
|
|
38
41
|
console.log(`Game ${game.slug} has no products. Done.`);
|
|
39
42
|
process.exit(0);
|
|
40
43
|
}
|
|
41
|
-
console.log("picked product:", product.id, product.name);
|
|
44
|
+
console.log("picked product:", product.id, product.name, "price:", product.price);
|
|
42
45
|
|
|
43
|
-
//
|
|
46
|
+
// 4. Create checkout (guest — pass email; no Telegram/VK login required).
|
|
44
47
|
// For balance-based payment, the user must be authenticated first.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
//
|
|
49
|
+
// Item fields:
|
|
50
|
+
// • productId — REQUIRED, the supplier product
|
|
51
|
+
// • gameId — REQUIRED (despite being typed optional). It's
|
|
52
|
+
// the game slug; the backend reads it directly to
|
|
53
|
+
// route Steam top-ups, SuperPass bundles, and
|
|
54
|
+
// Robux-via-pass through their special validation
|
|
55
|
+
// paths. A missing `gameId` crashes the request.
|
|
56
|
+
// • amount — quantity for fixed-price items (default 1) OR
|
|
57
|
+
// the top-up amount in local currency for Steam /
|
|
58
|
+
// variable-amount products.
|
|
59
|
+
// • deliveryData — product-specific fields (account name, server
|
|
60
|
+
// id, etc.). For simple gift cards this can be
|
|
61
|
+
// an empty object.
|
|
62
|
+
const checkout = await gc.checkout.create({
|
|
48
63
|
email: "buyer@example.com",
|
|
64
|
+
items: [
|
|
65
|
+
{
|
|
66
|
+
productId: product.id,
|
|
67
|
+
gameId: game.slug,
|
|
68
|
+
amount: 1,
|
|
69
|
+
deliveryData: {},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
paymentMethod: "antilopay", // gateway slug from checkout.getPaymentMethods()
|
|
49
73
|
});
|
|
50
|
-
console.log("payment URL:", checkout.gatewayPaymentUrl);
|
|
51
|
-
console.log("payment code:", checkout.paymentCode);
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
if (!checkout.payment) {
|
|
76
|
+
console.error("Checkout did not return a payment record:", checkout);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.log("payment code:", checkout.payment.code);
|
|
80
|
+
console.log("payment URL:", checkout.payment.paymentUrl);
|
|
81
|
+
|
|
82
|
+
// 5. Poll status until the user pays (or 5 minutes pass)
|
|
54
83
|
for (let i = 0; i < 30; i++) {
|
|
55
|
-
const status = await
|
|
84
|
+
const status = await gc.checkout.getStatus(checkout.payment.code);
|
|
56
85
|
console.log(`[${i}] status=${status.payment.status}`);
|
|
57
|
-
if (
|
|
58
|
-
|
|
86
|
+
if (
|
|
87
|
+
status.payment.status === "completed" ||
|
|
88
|
+
status.payment.status === "failed" ||
|
|
89
|
+
status.payment.status === "cancelled"
|
|
90
|
+
) {
|
|
91
|
+
console.log(
|
|
92
|
+
"orders:",
|
|
93
|
+
status.orders.map((o) => ({ code: o.code, status: o.status })),
|
|
94
|
+
);
|
|
59
95
|
break;
|
|
60
96
|
}
|
|
61
97
|
await new Promise((r) => setTimeout(r, 10_000));
|
|
@@ -8,20 +8,20 @@
|
|
|
8
8
|
* Three ways to pick a locale (highest priority first):
|
|
9
9
|
* 1. ?locale=en on a per-request basis
|
|
10
10
|
* 2. constructor option `locale: "en"` → adds Accept-Language header
|
|
11
|
-
* 3. nothing →
|
|
11
|
+
* 3. nothing → API falls back to RU
|
|
12
12
|
*
|
|
13
13
|
* Run with:
|
|
14
14
|
* GAMECORE_API_KEY=gc_live_... bun run examples/02-locale-switching.ts
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { GameCoreClient } from "@gamecore-api/sdk";
|
|
18
18
|
|
|
19
19
|
const apiKey = process.env.GAMECORE_API_KEY!;
|
|
20
20
|
const baseUrl = "https://api.gamecore-api.tech";
|
|
21
21
|
|
|
22
22
|
// Approach A — one client per locale (simplest)
|
|
23
|
-
const ru = new
|
|
24
|
-
const en = new
|
|
23
|
+
const ru = new GameCoreClient({ apiKey, baseUrl, locale: "ru" });
|
|
24
|
+
const en = new GameCoreClient({ apiKey, baseUrl, locale: "en" });
|
|
25
25
|
|
|
26
26
|
const slug = "shp"; // a game we know has both RU and EN
|
|
27
27
|
const ruDetail = await ru.catalog.getGame(slug);
|
|
@@ -32,20 +32,25 @@ console.log("EN short:", enDetail.shortDescription);
|
|
|
32
32
|
|
|
33
33
|
// Approach B — single client, switch at runtime (useful for SSR
|
|
34
34
|
// where the locale comes from the request headers).
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
console.log("locale now:",
|
|
38
|
-
const homepage = await
|
|
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
39
|
console.log("homepage first:", homepage[0]?.name);
|
|
40
40
|
|
|
41
41
|
// Approach C — per-call override. Useful when most traffic is one
|
|
42
42
|
// locale but a specific page needs the other (e.g. an admin tool
|
|
43
43
|
// rendering an EN preview from an otherwise-RU client).
|
|
44
|
-
const
|
|
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({
|
|
45
50
|
limit: 5,
|
|
46
51
|
locale: "en",
|
|
47
52
|
});
|
|
48
53
|
console.log(
|
|
49
|
-
"per-call EN:",
|
|
54
|
+
"per-call EN list:",
|
|
50
55
|
enGames.map((g) => g.name),
|
|
51
56
|
);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (machine-readable string set by the API for known cases).
|
|
7
7
|
*
|
|
8
8
|
* try {
|
|
9
|
-
* await
|
|
9
|
+
* await gc.checkout.create(...);
|
|
10
10
|
* } catch (e) {
|
|
11
11
|
* if (e instanceof GameCoreError) {
|
|
12
12
|
* if (e.status === 401) { /* re-auth *\/ }
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* GAMECORE_API_KEY=gc_live_... bun run examples/03-error-handling.ts
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { GameCoreClient, GameCoreError } from "@gamecore-api/sdk";
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const gc = new GameCoreClient({
|
|
25
25
|
apiKey: process.env.GAMECORE_API_KEY!,
|
|
26
26
|
baseUrl: "https://api.gamecore-api.tech",
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@ const api = new GameCore({
|
|
|
36
36
|
|
|
37
37
|
// Force a 404 to demonstrate the error shape
|
|
38
38
|
try {
|
|
39
|
-
await
|
|
39
|
+
await gc.catalog.getGame("definitely-not-a-real-game-slug-9999");
|
|
40
40
|
} catch (e) {
|
|
41
41
|
if (e instanceof GameCoreError) {
|
|
42
42
|
console.log("caught:", {
|
|
@@ -54,13 +54,13 @@ try {
|
|
|
54
54
|
//
|
|
55
55
|
// • 401 + code "UNAUTHORIZED" → API key invalid or revoked. Stop
|
|
56
56
|
// the request, re-fetch a fresh key from your secret store.
|
|
57
|
-
// •
|
|
57
|
+
// • 410 + code "TOKEN_EXPIRED" → user JWT expired. Triggers
|
|
58
58
|
// `onAuthError` if you provided one.
|
|
59
59
|
// • 429 + code "RATE_LIMITED" → token bucket. Read the `Retry-After`
|
|
60
60
|
// header if present, otherwise back off ~1s and retry.
|
|
61
61
|
// • 400 with field-level details → check `message` for the offending
|
|
62
62
|
// field (server returns plain-English reason for predictable cases
|
|
63
|
-
// like "
|
|
63
|
+
// like "amount must be ≥ 1" or "delivery data missing for product X").
|
|
64
64
|
|
|
65
65
|
// Retry helper with exponential backoff for transient errors
|
|
66
66
|
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
|
|
@@ -79,5 +79,5 @@ async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
|
|
|
79
79
|
throw lastError;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const games = await withRetry(() =>
|
|
82
|
+
const games = await withRetry(() => gc.catalog.getHomepageGames());
|
|
83
83
|
console.log("got", games.length, "games");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 04 — Webhook verification (server-side only)
|
|
3
3
|
*
|
|
4
|
-
* GameCore sends webhooks (order.completed, payment.
|
|
4
|
+
* GameCore sends webhooks (order.completed, payment.received, etc.) to
|
|
5
5
|
* a URL you register in the site-admin. Each request is signed with
|
|
6
6
|
* HMAC-SHA256 using your webhook secret. ALWAYS verify before
|
|
7
7
|
* trusting the body — without verification, an attacker who guesses
|
|
@@ -16,10 +16,24 @@
|
|
|
16
16
|
* RAW body (don't let your framework parse it before you verify).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
parseWebhookPayload,
|
|
21
|
+
verifyWebhookSignature,
|
|
22
|
+
} from "@gamecore-api/sdk/server";
|
|
20
23
|
|
|
21
24
|
// — Express example —
|
|
22
|
-
|
|
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
|
+
};
|
|
23
37
|
|
|
24
38
|
const WEBHOOK_SECRET = process.env.GAMECORE_WEBHOOK_SECRET!; // from site-admin
|
|
25
39
|
|
|
@@ -45,17 +59,27 @@ export async function handleWebhook(req: Request, res: Response) {
|
|
|
45
59
|
return;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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);
|
|
53
75
|
break;
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
}
|
|
77
|
+
case "order.failed":
|
|
78
|
+
case "order.cancelled":
|
|
79
|
+
console.log("order ended:", payload.event, payload.data);
|
|
56
80
|
break;
|
|
57
81
|
default:
|
|
58
|
-
console.log("unhandled event:", event
|
|
82
|
+
console.log("unhandled event:", payload.event);
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
res.status(200).send("ok");
|
|
@@ -72,8 +96,8 @@ export async function handleWebhook(req: Request, res: Response) {
|
|
|
72
96
|
// if (!verifyWebhookSignature(raw, sig, WEBHOOK_SECRET)) {
|
|
73
97
|
// return new Response("invalid", { status: 401 });
|
|
74
98
|
// }
|
|
75
|
-
// const
|
|
76
|
-
// // ...
|
|
99
|
+
// const payload = parseWebhookPayload(raw);
|
|
100
|
+
// // ... switch on payload.event
|
|
77
101
|
// return new Response("ok");
|
|
78
102
|
// }
|
|
79
103
|
// return new Response("not found", { status: 404 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamecore-api/sdk",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
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",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
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",
|