@gamecore-api/sdk 0.15.0 → 0.17.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/README.md CHANGED
@@ -42,20 +42,62 @@ const results = await gc.catalog.search("roblox");
42
42
 
43
43
  ## Authentication
44
44
 
45
- ### Telegram Auth Flow
45
+ ### Telegram Auth — two paths, pick both
46
+
47
+ Two flavours, designed to coexist as two buttons in the UI:
48
+
49
+ **1. Official Login Widget** (fastest, needs official Telegram Web session)
46
50
 
47
51
  ```typescript
48
- // 1. Start auth get bot link
49
- const { token, botLink } = await gc.auth.initTelegram();
52
+ // Mount the blue "Log in with Telegram" button into your own <div>.
53
+ // Bot username is pulled from /site/config; no hardcoding.
54
+ // BotFather /setdomain must point at your storefront's origin, or
55
+ // telegram.org refuses to render the widget.
56
+ const cleanup = await gc.auth.renderTelegramWidget({
57
+ container: document.querySelector("#tg-login")!,
58
+ size: "large",
59
+ onAuth: (user) => {
60
+ console.log("Logged in:", user.firstName);
61
+ window.location.href = "/profile";
62
+ },
63
+ onError: (err) => console.error(err),
64
+ });
50
65
 
51
- // 2. Open Telegram bot (user clicks "Start")
52
- window.open(botLink, "_blank");
66
+ // Later (React unmount, SPA route change):
67
+ cleanup();
68
+ ```
53
69
 
54
- // 3. Poll until user authenticates
55
- const user = await gc.auth.pollTelegramStatus(token);
70
+ **2. Bot-link flow** (works in every Telegram client including 3rd-party)
71
+
72
+ ```typescript
73
+ const user = await gc.auth.loginViaTelegramBot({
74
+ onBotLinkReady: (botLink) => {
75
+ // Open in new tab — every TG client handles tg:// deep links.
76
+ // For desktop-only users you could also render botLink as a QR.
77
+ window.open(botLink, "_blank");
78
+ },
79
+ pollMs: 2000,
80
+ timeoutMs: 120_000,
81
+ });
56
82
  console.log("Logged in:", user.firstName);
57
83
  ```
58
84
 
85
+ **Low-level pieces (for custom flows)**
86
+
87
+ ```typescript
88
+ // Manual init+poll — equivalent to loginViaTelegramBot above
89
+ const { token, botLink } = await gc.auth.initTelegram();
90
+ window.open(botLink, "_blank");
91
+ const user = await gc.auth.pollTelegramStatus(token);
92
+
93
+ // Manual widget verification — when you render Telegram's <script> yourself
94
+ // and wire data-onauth to your own JS callback
95
+ const auth = await gc.auth.verifyTelegramWidget(telegramWidgetUser);
96
+
97
+ // Mini App (inside the Telegram bot's built-in WebApp)
98
+ const auth = await gc.auth.verifyMiniApp(window.Telegram.WebApp.initData);
99
+ ```
100
+
59
101
  ### VK Auth
60
102
 
61
103
  ```typescript
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TelegramInitResponse, TelegramWidgetUser, TelegramAuthResponse, User, SiteConfig, ExchangeRates, LegalDocument, Game, GameDetail, Product, SearchResult, Category, CartItem, CheckoutRequest, CheckoutResponse, Conversation, ConversationDetail, Order, PaymentInfo, CompleteWithBalanceResult, UserBalance, WebPushSubscriptionInput, LevelStatus, Transaction, Notification, Favorite, Review, ReviewCreateResult, ReviewStats, CouponResult, ReferralStats, ReferralLink, ReferralCommission, ReferralPerformance, TopupMethod, TopupResponse, TopupStatus, GiftCard, Announcement, AnnouncementBar, SiteUIConfig, PaymentMethod, CatalogSection, SiteStats, ProductFilters, PaginatedResponse, PagedGamesResponse } from "./types";
1
+ import type { Announcement, AnnouncementBar, CartItem, CatalogSection, Category, CategoryInfo, CheckoutRequest, CheckoutResponse, CompleteWithBalanceResult, Conversation, ConversationDetail, CouponResult, ExchangeRates, Favorite, Game, GameDetail, GiftCard, LegalDocument, LevelStatus, Notification, Order, PagedGamesResponse, PlatformInfo, PaginatedResponse, PaymentInfo, PaymentMethod, Product, ProductFilters, ReferralCommission, ReferralLink, ReferralPerformance, ReferralStats, Review, ReviewCreateResult, ReviewStats, SearchResult, SiteConfig, SiteStats, SiteUIConfig, TelegramAuthResponse, TelegramBotLoginOptions, TelegramInitResponse, TelegramWidgetRenderOptions, TelegramWidgetUser, TopupMethod, TopupResponse, TopupStatus, Transaction, User, UserBalance, WebPushSubscriptionInput } from "./types";
2
2
  export interface GameCoreOptions {
3
3
  /** Site API key (gc_live_xxx or gc_test_xxx) */
4
4
  apiKey: string;
@@ -124,6 +124,43 @@ export declare class GameCoreClient {
124
124
  * directly to this method.
125
125
  */
126
126
  verifyTelegramWidget: (data: TelegramWidgetUser, ref?: string) => Promise<TelegramAuthResponse>;
127
+ /**
128
+ * High-level helper: mount the official Telegram Login Widget into
129
+ * your own DOM element and hand the resulting User to `onAuth`
130
+ * after server-side HMAC verification. Browser-only.
131
+ *
132
+ * Compared to dropping the raw `<script>` on the page:
133
+ * - Bot username is pulled from `gc.site.getConfig().authConfig.
134
+ * telegram.botUsername` (no hardcoding).
135
+ * - The widget's `data-onauth` bridge is plumbed to
136
+ * {@link verifyTelegramWidget} so the caller only sees a
137
+ * verified User.
138
+ * - Errors (hash mismatch, missing bot, expired payload) surface
139
+ * through `onError` instead of becoming silent no-ops.
140
+ *
141
+ * BotFather prerequisite: `/setdomain` must be set for the bot to
142
+ * the storefront's origin; the widget refuses to render otherwise.
143
+ *
144
+ * Returns a cleanup function that removes the widget from the
145
+ * container.
146
+ */
147
+ renderTelegramWidget: (opts: TelegramWidgetRenderOptions) => Promise<() => void>;
148
+ /**
149
+ * High-level helper: run the bot-link auth flow (initTelegram →
150
+ * poll) and resolve with the authenticated User. Works in every
151
+ * Telegram client (including third-party like Nicegram, Plus
152
+ * Messenger) because it doesn't depend on the Login Widget — the
153
+ * user just opens the deep link and presses Start in the bot.
154
+ *
155
+ * The caller's `onBotLinkReady` callback receives the generated
156
+ * `https://t.me/<bot>?start=<token>` link exactly once; use it to
157
+ * `window.open(url, "_blank")`, render a QR, or redirect the
158
+ * user. Polling for the auth handshake starts immediately after.
159
+ *
160
+ * Rejects with `GameCoreError("Timeout")` if the user doesn't
161
+ * press Start within `timeoutMs`.
162
+ */
163
+ loginViaTelegramBot: (opts: TelegramBotLoginOptions) => Promise<User>;
127
164
  /**
128
165
  * Get VK OAuth authorize URL (authorization code flow).
129
166
  *
@@ -248,7 +285,31 @@ export declare class GameCoreClient {
248
285
  inStockOnly?: boolean;
249
286
  sort?: "popular" | "name" | "new" | "soldCount";
250
287
  q?: string;
288
+ /**
289
+ * Filter by canonical platform slug — "steam", "xbox", "psn",
290
+ * "mobile_game", etc. Use slugs from `getPlatforms()`.
291
+ */
292
+ platform?: string;
293
+ /**
294
+ * Filter by canonical category slug — "currency",
295
+ * "battle_pass", "bundle", etc. Use slugs from `getCategories()`.
296
+ */
297
+ category?: string;
251
298
  }) => Promise<PagedGamesResponse>;
299
+ /**
300
+ * List distribution platforms present in the site catalog with
301
+ * per-platform game counts (post site-overrides). Drives
302
+ * storefront filter chips. Introduced in task 118 Phase 3.
303
+ */
304
+ getPlatforms: () => Promise<PlatformInfo[]>;
305
+ /**
306
+ * List canonical category slugs present in the site catalog
307
+ * with per-slug game counts. Use the returned `slug` as the
308
+ * `category` argument to `getGames()`. Not to be confused with
309
+ * `getCategories(gameSlug)` which returns categories of a
310
+ * single game. Introduced in task 119 Phase 3.
311
+ */
312
+ getCatalogCategories: () => Promise<CategoryInfo[]>;
252
313
  /** Get homepage ranked games */
253
314
  getHomepageGames: () => Promise<Game[]>;
254
315
  /** Get single game with categories */
package/dist/index.js CHANGED
@@ -109,6 +109,91 @@ class GameCoreClient {
109
109
  }),
110
110
  verifyMiniApp: (initData, ref) => this.request("POST", "/auth/telegram/miniapp", { initData, ref }, { rawResponse: true }),
111
111
  verifyTelegramWidget: (data, ref) => this.request("POST", "/auth/telegram/widget", { ...data, ref }, { rawResponse: true }),
112
+ renderTelegramWidget: async (opts) => {
113
+ if (typeof document === "undefined") {
114
+ throw new GameCoreError("renderTelegramWidget requires a browser environment", 0, "NO_DOM");
115
+ }
116
+ let botUsername = opts.botUsername;
117
+ if (!botUsername) {
118
+ const config = await this.request("GET", "/site/config");
119
+ botUsername = config.authConfig?.telegram?.botUsername ?? undefined;
120
+ }
121
+ if (!botUsername) {
122
+ throw new GameCoreError("Telegram bot is not configured for this site — contact the operator to set a bot in master-admin", 0, "TG_NOT_CONFIGURED");
123
+ }
124
+ const callbackName = `__gcTgWidget_${Math.random().toString(36).slice(2)}`;
125
+ const onAuth = opts.onAuth;
126
+ const onError = opts.onError;
127
+ const ref = opts.ref;
128
+ const verify = this.auth.verifyTelegramWidget.bind(this.auth);
129
+ window[callbackName] = async (user) => {
130
+ try {
131
+ const res = await verify(user, ref);
132
+ if (!res.user) {
133
+ throw new GameCoreError(res.error || "Telegram verification returned no user", 401, "VERIFY_FAILED");
134
+ }
135
+ await onAuth(res.user);
136
+ } catch (err) {
137
+ if (onError)
138
+ onError(err);
139
+ else
140
+ throw err;
141
+ }
142
+ };
143
+ const script = document.createElement("script");
144
+ script.async = true;
145
+ script.src = "https://telegram.org/js/telegram-widget.js?22";
146
+ script.setAttribute("data-telegram-login", botUsername);
147
+ script.setAttribute("data-size", opts.size ?? "large");
148
+ if (typeof opts.cornerRadius === "number") {
149
+ script.setAttribute("data-radius", String(opts.cornerRadius));
150
+ }
151
+ if (opts.showUserPic === false) {
152
+ script.setAttribute("data-userpic", "false");
153
+ }
154
+ if (opts.requestWriteAccess !== false) {
155
+ script.setAttribute("data-request-access", "write");
156
+ }
157
+ script.setAttribute("data-onauth", `${callbackName}(user)`);
158
+ opts.container.replaceChildren();
159
+ opts.container.appendChild(script);
160
+ return () => {
161
+ opts.container.replaceChildren();
162
+ delete window[callbackName];
163
+ };
164
+ },
165
+ loginViaTelegramBot: async (opts) => {
166
+ const init = await this.auth.initTelegram();
167
+ opts.onBotLinkReady(init.botLink, init.token);
168
+ const pollMs = opts.pollMs ?? 2000;
169
+ const timeoutMs = opts.timeoutMs ?? 120000;
170
+ const deadline = Date.now() + timeoutMs;
171
+ const refParam = opts.ref ? `?ref=${encodeURIComponent(opts.ref)}` : "";
172
+ return new Promise((resolve, reject) => {
173
+ const tick = async () => {
174
+ if (Date.now() > deadline) {
175
+ reject(new GameCoreError("Telegram login timed out", 408, "TIMEOUT"));
176
+ return;
177
+ }
178
+ try {
179
+ const res = await this.request("GET", `/auth/telegram/status/${init.token}${refParam}`, undefined, { rawResponse: true });
180
+ if (res.status === "authenticated" && res.user) {
181
+ resolve(res.user);
182
+ return;
183
+ }
184
+ if (res.status === "expired" || res.status === "used") {
185
+ reject(new GameCoreError("Telegram auth token expired", 410, "TOKEN_EXPIRED"));
186
+ return;
187
+ }
188
+ } catch (err) {
189
+ reject(err);
190
+ return;
191
+ }
192
+ setTimeout(tick, pollMs);
193
+ };
194
+ setTimeout(tick, pollMs);
195
+ });
196
+ },
112
197
  getVkAuthUrl: (redirectUri, ref) => {
113
198
  const params = new URLSearchParams;
114
199
  params.set("redirect", redirectUri);
@@ -120,7 +205,10 @@ class GameCoreClient {
120
205
  verifyVk: (accessToken, ref) => this.request("POST", "/auth/vk/verify", { accessToken, ref }, { rawResponse: true }),
121
206
  register: (email, password, firstName, ref) => this.request("POST", "/auth/register", { email, password, firstName, ref }, { rawResponse: true }),
122
207
  login: (email, password) => this.request("POST", "/auth/login", { email, password }, { rawResponse: true }),
123
- changePassword: (currentPassword, newPassword) => this.request("POST", "/auth/change-password", { currentPassword, newPassword }),
208
+ changePassword: (currentPassword, newPassword) => this.request("POST", "/auth/change-password", {
209
+ currentPassword,
210
+ newPassword
211
+ }),
124
212
  getMe: () => this.request("GET", "/auth/me", undefined, { rawResponse: true }),
125
213
  logout: () => this.request("POST", "/auth/logout"),
126
214
  getIdentities: () => this.request("GET", "/auth/identities", undefined, { rawResponse: true }),
@@ -151,9 +239,15 @@ class GameCoreClient {
151
239
  qs.set("sort", params.sort);
152
240
  if (params?.q)
153
241
  qs.set("q", params.q);
242
+ if (params?.platform)
243
+ qs.set("platform", params.platform);
244
+ if (params?.category)
245
+ qs.set("category", params.category);
154
246
  const q = qs.toString();
155
247
  return this.request("GET", `/catalog/games${q ? `?${q}` : ""}`);
156
248
  },
249
+ getPlatforms: () => this.request("GET", "/catalog/platforms"),
250
+ getCatalogCategories: () => this.request("GET", "/catalog/categories"),
157
251
  getHomepageGames: () => this.request("GET", "/catalog/homepage-games"),
158
252
  getGame: (slug, locale) => {
159
253
  const qs = locale ? `?locale=${locale}` : "";
@@ -261,7 +355,9 @@ class GameCoreClient {
261
355
  },
262
356
  getPushPublicKey: () => this.request("GET", "/profile/push/public-key"),
263
357
  subscribePush: (subscription) => this.request("POST", "/profile/push/subscribe", subscription),
264
- unsubscribePush: (endpoint) => this.request("POST", "/profile/push/unsubscribe", { endpoint })
358
+ unsubscribePush: (endpoint) => this.request("POST", "/profile/push/unsubscribe", {
359
+ endpoint
360
+ })
265
361
  };
266
362
  favorites = {
267
363
  list: () => this.request("GET", "/favorites"),
package/dist/types.d.ts CHANGED
@@ -69,6 +69,17 @@ export interface SiteConfig {
69
69
  };
70
70
  modules: Record<string, boolean>;
71
71
  auth: string[];
72
+ /**
73
+ * Structured per-method auth config. Companion to the flat `auth` array
74
+ * — `auth` tells you WHICH methods are available, `authConfig` gives
75
+ * the details you need to render each method on the client (bot
76
+ * username for the Telegram Login Widget, etc).
77
+ */
78
+ authConfig?: {
79
+ telegram: {
80
+ botUsername: string;
81
+ } | null;
82
+ };
72
83
  payments: string[];
73
84
  displayCurrency: string;
74
85
  rateMode: "auto" | "manual";
@@ -87,6 +98,76 @@ export interface SiteConfig {
87
98
  } | null;
88
99
  };
89
100
  }
101
+ /**
102
+ * Options for the high-level {@link GameCoreClient.auth.renderTelegramWidget}
103
+ * helper. Values map 1:1 to Telegram Login Widget `data-*` attributes
104
+ * (see https://core.telegram.org/widgets/login).
105
+ */
106
+ export interface TelegramWidgetRenderOptions {
107
+ /** DOM element the widget renders into. Previous children are cleared. */
108
+ container: HTMLElement;
109
+ /** Button size. Default: "large". */
110
+ size?: "small" | "medium" | "large";
111
+ /** Corner radius in pixels. Default: Telegram's own (20). */
112
+ cornerRadius?: number;
113
+ /**
114
+ * Show user photo next to the button. Default: true (matches Telegram's
115
+ * own default of `data-userpic="true"`).
116
+ */
117
+ showUserPic?: boolean;
118
+ /**
119
+ * Request permission to send messages to the user from the bot. Sets
120
+ * `data-request-access="write"`. Default: true.
121
+ */
122
+ requestWriteAccess?: boolean;
123
+ /**
124
+ * Called after the widget reports an authenticated user AND the
125
+ * payload has been verified by GameCore server-side (HMAC-SHA256).
126
+ * The `user` object is GameCore's {@link TelegramAuthUser} —
127
+ * the same shape the underlying `POST /auth/telegram/widget`
128
+ * endpoint returns.
129
+ */
130
+ onAuth: (user: TelegramAuthUser) => void | Promise<void>;
131
+ /**
132
+ * Called if verification fails (expired hash, wrong signature, or
133
+ * network error). When omitted the error is thrown to the browser's
134
+ * unhandled-promise handler.
135
+ */
136
+ onError?: (err: Error) => void;
137
+ /** Optional referral code to thread through to the signup flow. */
138
+ ref?: string;
139
+ /**
140
+ * Bot username override. Defaults to the value from `gc.site.getConfig()`
141
+ * — only pass this when you have not called getConfig() yet and want
142
+ * to skip that round trip.
143
+ */
144
+ botUsername?: string;
145
+ }
146
+ /**
147
+ * Options for the high-level {@link GameCoreClient.auth.loginViaTelegramBot}
148
+ * helper. Orchestrates the init → poll flow for users whose Telegram
149
+ * client can't render the Login Widget (third-party mobile clients with
150
+ * no active Telegram Web session, desktop browsers without TG Web
151
+ * logged in, etc).
152
+ */
153
+ export interface TelegramBotLoginOptions {
154
+ /**
155
+ * Callback that receives the generated bot deep-link. Use it to open
156
+ * a popup, render a QR code, or redirect the user. This is called
157
+ * exactly once per login attempt, as soon as the server returns the
158
+ * link.
159
+ */
160
+ onBotLinkReady: (botLink: string, token: string) => void;
161
+ /** Poll interval in milliseconds. Default: 2000. */
162
+ pollMs?: number;
163
+ /**
164
+ * Give up after this many milliseconds. Default: 120000 (2 minutes,
165
+ * matches the server-side token TTL).
166
+ */
167
+ timeoutMs?: number;
168
+ /** Optional referral code to thread through to the signup flow. */
169
+ ref?: string;
170
+ }
90
171
  export interface ExchangeRates {
91
172
  usdToRub: number;
92
173
  base: string;
@@ -216,6 +297,40 @@ export interface Category {
216
297
  productCount: number;
217
298
  deliveryTypes?: string[];
218
299
  platforms?: string[];
300
+ /**
301
+ * Canonical category slug (e.g. "currency", "battle_pass"). Present
302
+ * on responses from servers running task 119 Phase 2+. Stable across
303
+ * suppliers — Nexus "Алмазы" and Vendoria "Diamonds" both carry
304
+ * canonicalSlug="currency". Use this (not `name`) for storefront
305
+ * dedup and routing.
306
+ */
307
+ canonicalSlug?: string | null;
308
+ }
309
+ /**
310
+ * Distribution platform descriptor from GET /catalog/platforms.
311
+ * Use `slug` as the key for ?platform= filter and as a stable identifier
312
+ * across locales. `label` is storefront-ready display text.
313
+ * Introduced by task 118 Phase 3.
314
+ */
315
+ export interface PlatformInfo {
316
+ slug: string;
317
+ label: string;
318
+ group: string;
319
+ gameCount: number;
320
+ }
321
+ /**
322
+ * Canonical category descriptor from GET /catalog/categories. Use
323
+ * `slug` as the key for ?category= filter. `labelRu`/`labelEn` are
324
+ * storefront-ready display text in each supported UI locale; pick
325
+ * whichever matches the current site locale. Introduced by task 119
326
+ * Phase 3.
327
+ */
328
+ export interface CategoryInfo {
329
+ slug: string;
330
+ labelRu: string;
331
+ labelEn: string;
332
+ group: string;
333
+ gameCount: number;
219
334
  }
220
335
  export interface FulfillmentMeta {
221
336
  needsPlayerId: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamecore-api/sdk",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",