@gamecore-api/sdk 0.9.0 → 0.13.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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TelegramInitResponse, User, SiteConfig, ExchangeRates, LegalDocument, Game, GameDetail, Product, SearchResult, Category, CartItem, CheckoutRequest, CheckoutResponse, Order, UserBalance, LevelStatus, Transaction, Notification, Favorite, Review, ReviewStats, CouponResult, ReferralStats, ReferralLink, ReferralCommission, TopupMethod, TopupResponse, TopupStatus, GiftCard, Announcement, AnnouncementBar, SiteUIConfig, PaymentMethod, CatalogSection, SiteStats, ProductFilters, PaginatedResponse } from "./types";
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";
2
2
  export interface GameCoreOptions {
3
3
  /** Site API key (gc_live_xxx or gc_test_xxx) */
4
4
  apiKey: string;
@@ -47,33 +47,141 @@ export declare class GameCoreClient {
47
47
  }>;
48
48
  /** Get per-site catalog sections (category tabs config) */
49
49
  getCatalogSections: () => Promise<CatalogSection[]>;
50
- /** Get active hero banners for carousel */
50
+ /**
51
+ * Get active hero banners for the storefront carousel.
52
+ * Each banner carries optional visual fields plus a `url`
53
+ * destination — `null` means the banner is display-only and
54
+ * should not be rendered as a link.
55
+ */
51
56
  getBanners: () => Promise<{
52
57
  id: number;
53
58
  title: string | null;
54
59
  description: string | null;
55
60
  imageUrl: string | null;
56
61
  color: string | null;
62
+ url: string | null;
57
63
  scope: string;
58
64
  priority: number;
59
65
  }[]>;
60
66
  /** Get announcement bar settings (text, link, enabled) */
61
67
  getAnnouncementBar: () => Promise<AnnouncementBar>;
68
+ /**
69
+ * Get sitemap data for self-generated storefront sitemaps.
70
+ * Returns `{ slug, updatedAt }` for every game visible on
71
+ * this site. Use `updatedAt` as `lastModified` in Next.js
72
+ * `sitemap.ts` so Google's crawler respects unchanged pages
73
+ * instead of stamping the whole catalog with `new Date()`.
74
+ */
75
+ getSitemapData: () => Promise<{
76
+ slug: string;
77
+ updatedAt: string;
78
+ }[]>;
79
+ /**
80
+ * Submit a "please add this game" request. Works for both
81
+ * authenticated users (the server attaches the user id so
82
+ * support can reach back) and anonymous visitors.
83
+ *
84
+ * Rate-limited server-side. The optional `website` field is
85
+ * a honeypot — leave it `undefined` in real code; the
86
+ * storefront can render an invisible input under that name
87
+ * so scrapers self-identify.
88
+ *
89
+ * @returns The new request id and a success flag. A filled
90
+ * honeypot still returns `success: true` with
91
+ * `requestId: -1` so bots cannot distinguish a real save
92
+ * from a silent drop.
93
+ */
94
+ requestGame: (data: {
95
+ gameName: string;
96
+ comment?: string;
97
+ gameUrl?: string;
98
+ website?: string;
99
+ }) => Promise<{
100
+ success: boolean;
101
+ requestId: number;
102
+ }>;
62
103
  };
63
104
  auth: {
64
105
  /** Start Telegram auth flow → returns bot link for user to click */
65
106
  initTelegram: () => Promise<TelegramInitResponse>;
66
107
  /** Poll Telegram auth status until authenticated or expired */
67
108
  pollTelegramStatus: (token: string, intervalMs?: number) => Promise<User>;
68
- /** Get VK OAuth redirect URL */
69
- getVkAuthUrl: (redirectUri: string) => Promise<{
109
+ /**
110
+ * Verify Telegram Mini App initData and issue a session cookie.
111
+ * Call this when your storefront is opened inside Telegram via the
112
+ * bot's Mini App / Web App button — `window.Telegram.WebApp.initData`
113
+ * is the raw URL-encoded string you should pass in. The backend
114
+ * validates the HMAC signature against the bot token and mints a
115
+ * normal auth cookie, so the rest of the site stays logged in.
116
+ * Optional `ref` threads a referral code through user creation.
117
+ */
118
+ verifyMiniApp: (initData: string, ref?: string) => Promise<TelegramAuthResponse>;
119
+ /**
120
+ * Verify Telegram Login Widget payload (JSON POST variant) and
121
+ * issue a session cookie. Use this when you embed the Login Widget
122
+ * with `data-onauth` (JS callback) instead of `data-auth-url`
123
+ * (full-page redirect) — pass the object the widget handed you
124
+ * directly to this method.
125
+ */
126
+ verifyTelegramWidget: (data: TelegramWidgetUser, ref?: string) => Promise<TelegramAuthResponse>;
127
+ /**
128
+ * Get VK OAuth authorize URL (authorization code flow).
129
+ *
130
+ * Backend generates a single-use state nonce, stores it server-
131
+ * side, and returns a URL pointing at VK with response_type=code.
132
+ * The storefront redirects the user to this URL; VK redirects
133
+ * back to `redirectUri` with ?code=...&state=...; the storefront
134
+ * then calls {@link vkCallback} with those two values.
135
+ *
136
+ * `redirectUri` must exactly match one of the site's allowed
137
+ * origins configured in master-admin — the backend validates
138
+ * this before storing the state. `ref` is an optional referral
139
+ * code that gets threaded through to user registration.
140
+ */
141
+ getVkAuthUrl: (redirectUri: string, ref?: string) => Promise<{
70
142
  url: string;
143
+ state: string;
144
+ }>;
145
+ /**
146
+ * Exchange an authorization code for a session cookie (code flow).
147
+ * Call this after VK redirects back to your storefront with
148
+ * ?code=...&state=... in the query string. The backend validates
149
+ * state (single-use), exchanges the code with VK server-to-server
150
+ * using the site's client_secret, fetches the user profile, and
151
+ * mints a normal auth cookie. The VK access token never reaches
152
+ * the client.
153
+ */
154
+ vkCallback: (code: string, state: string) => Promise<{
155
+ status: string;
156
+ user: User;
71
157
  }>;
72
- /** Verify VK access token and login/register */
158
+ /**
159
+ * Verify a VK access token from the deprecated implicit flow.
160
+ * @deprecated Use {@link getVkAuthUrl} + {@link vkCallback} instead.
161
+ * This endpoint is kept alive for backwards compatibility while
162
+ * storefronts migrate to the code flow. It will be removed in a
163
+ * future major release.
164
+ */
73
165
  verifyVk: (accessToken: string, ref?: string) => Promise<{
74
166
  status: string;
75
167
  user: User;
76
168
  }>;
169
+ /** Register with email + password. No auth required. */
170
+ register: (email: string, password: string, firstName?: string, ref?: string) => Promise<{
171
+ success: boolean;
172
+ token: string;
173
+ user: User;
174
+ }>;
175
+ /** Login with email + password. No auth required. */
176
+ login: (email: string, password: string) => Promise<{
177
+ success: boolean;
178
+ token: string;
179
+ user: User;
180
+ }>;
181
+ /** Change password (requires auth). Revokes all other sessions. */
182
+ changePassword: (currentPassword: string, newPassword: string) => Promise<{
183
+ success: boolean;
184
+ }>;
77
185
  /** Get current authenticated user */
78
186
  getMe: () => Promise<User>;
79
187
  /** Logout and clear session */
@@ -88,6 +196,22 @@ export declare class GameCoreClient {
88
196
  }>;
89
197
  /** Link VK account to current user */
90
198
  linkVk: (accessToken: string) => Promise<void>;
199
+ /**
200
+ * Attach an email + password identity to the currently
201
+ * authenticated user. Use this for Telegram-only / VK-only
202
+ * accounts that want a backup login method. The user must
203
+ * not already have an email set — update flows go through
204
+ * `changePassword()` or a future `changeEmail()` method.
205
+ *
206
+ * Rate-limited to the same bucket as `/auth/register`.
207
+ * Returns the normalised email on success; throws 409 if the
208
+ * user already has an email or if another account on this
209
+ * site owns the address.
210
+ */
211
+ linkEmail: (email: string, password: string) => Promise<{
212
+ success: boolean;
213
+ email: string;
214
+ }>;
91
215
  /** Unlink auth provider */
92
216
  unlinkProvider: (provider: string) => Promise<void>;
93
217
  /** Preview VK account merge (before confirming) */
@@ -102,20 +226,49 @@ export declare class GameCoreClient {
102
226
  }>;
103
227
  };
104
228
  catalog: {
105
- /** Get all games. Use inStockOnly:true for only games with products. include:"meta" for categories/deliveryTypes. */
229
+ /**
230
+ * Get paginated games catalog.
231
+ *
232
+ * Returns `{ data: Game[], pagination: { page, limit, total, totalPages, hasMore } }`.
233
+ *
234
+ * @param params.page - Page number (1-based, default 1)
235
+ * @param params.limit - Items per page (1-200, default 50)
236
+ * @param params.inStockOnly - Only games that have products in stock
237
+ * @param params.include - "meta" to include categories/deliveryTypes
238
+ * @param params.sort - Sort order: "popular" | "name" | "new" | "soldCount"
239
+ * @param params.q - Search query (Meilisearch or fallback)
240
+ */
106
241
  getGames: (params?: {
242
+ page?: number;
243
+ limit?: number;
107
244
  locale?: string;
108
245
  type?: string;
109
246
  deliveryType?: string;
110
247
  include?: string;
111
248
  inStockOnly?: boolean;
112
249
  sort?: "popular" | "name" | "new" | "soldCount";
113
- }) => Promise<Game[]>;
250
+ q?: string;
251
+ }) => Promise<PagedGamesResponse>;
114
252
  /** Get homepage ranked games */
115
253
  getHomepageGames: () => Promise<Game[]>;
116
254
  /** Get single game with categories */
117
255
  getGame: (slug: string, locale?: string) => Promise<GameDetail>;
118
- /** Get recommended games (same type, random) */
256
+ /**
257
+ * Get recommended games for a game detail page
258
+ * ("you might also like" / "related games").
259
+ *
260
+ * Returns games of the same `type` as the source game, randomly
261
+ * selected and filtered by per-site visibility overrides.
262
+ *
263
+ * @param gameSlug - Source game slug. Must be the **canonical**
264
+ * slug — this endpoint does NOT resolve aliases and returns an
265
+ * empty array for alias inputs. If you only have an alias,
266
+ * call `getGame()` first and use
267
+ * `result.canonicalSlug ?? result.slug`.
268
+ * @param limit - Max recommendations (default 8). The backend
269
+ * caps the effective limit; very large values are trimmed
270
+ * server-side.
271
+ */
119
272
  getRecommendations: (gameSlug: string, limit?: number) => Promise<Game[]>;
120
273
  /** Get categories for a game with product counts and delivery metadata */
121
274
  getCategories: (gameSlug: string) => Promise<Category[]>;
@@ -148,17 +301,22 @@ export declare class GameCoreClient {
148
301
  minItems: number;
149
302
  discountPercent: number;
150
303
  }[]>;
151
- /** Get active promo campaigns */
304
+ /**
305
+ * Get active promo campaigns. `targetUrl` is the clickable
306
+ * destination for the banner artwork (nullable — null means
307
+ * display-only).
308
+ */
152
309
  getPromos: () => Promise<{
153
310
  id: number;
154
311
  name: string;
155
312
  type: string;
156
313
  value: number;
157
314
  scope: string;
158
- bannerTitle?: string;
159
- bannerDescription?: string;
160
- bannerImageUrl?: string;
161
- bannerColor?: string;
315
+ bannerTitle?: string | null;
316
+ bannerDescription?: string | null;
317
+ bannerImageUrl?: string | null;
318
+ bannerColor?: string | null;
319
+ targetUrl?: string | null;
162
320
  endsAt?: string;
163
321
  }[]>;
164
322
  /** Get all games with full metadata (categories, delivery types) */
@@ -173,24 +331,64 @@ export declare class GameCoreClient {
173
331
  }>;
174
332
  };
175
333
  cart: {
176
- /** Get cart items for authenticated user */
334
+ /**
335
+ * Get cart items for the authenticated user. Each row comes
336
+ * back with `quantity`, `addedAt`, and `gameIcon` /
337
+ * `productIcon` populated by the server (2 extra lookups for
338
+ * the whole cart, not per row).
339
+ */
177
340
  get: () => Promise<CartItem[]>;
178
- /** Add single item to cart */
341
+ /**
342
+ * Add a single item to the cart. `quantity` is optional and
343
+ * defaults to `1`. The server caps per-row quantity at 999.
344
+ */
179
345
  add: (item: Omit<CartItem, "id">) => Promise<CartItem>;
180
- /** Sync localStorage cart to server (replaces server cart) */
346
+ /**
347
+ * Replace the server cart with the given list. Kept for
348
+ * backwards compatibility with SDK <= 0.12 and for the
349
+ * "clear everything and insert fresh" use case. Prefer
350
+ * `cart.merge()` when handling the guest → authed handoff so
351
+ * an existing server cart is not wiped.
352
+ */
181
353
  sync: (items: Omit<CartItem, "id">[]) => Promise<CartItem[]>;
354
+ /**
355
+ * Merge the given items into the server cart without wiping
356
+ * it. Items colliding on `(productId, deliveryData)` add
357
+ * their quantities (capped at 999 per row); new rows are
358
+ * inserted. Use this when a guest with a localStorage cart
359
+ * logs in and we want to keep both the old server rows and
360
+ * the newly collected ones.
361
+ */
362
+ merge: (items: Omit<CartItem, "id">[]) => Promise<CartItem[]>;
182
363
  /** Remove item from cart by ID */
183
364
  remove: (id: number) => Promise<void>;
184
365
  /** Clear all cart items */
185
366
  clear: () => Promise<void>;
186
367
  };
187
368
  checkout: {
188
- /** Create checkout (payment + orders). Auto-generates idempotency key. */
369
+ /**
370
+ * Create checkout (payment + orders). Auto-generates idempotency key.
371
+ *
372
+ * **Guest checkout:** This method works WITHOUT authentication when
373
+ * `data.email` is provided and `data.paymentMethod` is a gateway
374
+ * (not "balance"). The API creates a guest order tied to the email.
375
+ * No Telegram/VK login required. Balance payments still require auth.
376
+ */
189
377
  create: (data: CheckoutRequest, idempotencyKey?: string) => Promise<CheckoutResponse>;
190
- /** Complete payment with balance (no external gateway) */
191
- completeWithBalance: (paymentCode: string) => Promise<{
192
- success: boolean;
193
- }>;
378
+ /**
379
+ * Complete a pending payment by deducting the amount from the
380
+ * user's balance (bonus first, then permanent). Returns the
381
+ * deduction breakdown and the post-deduction balance so the
382
+ * success page can render the receipt without a second
383
+ * `profile.getBalance()` fetch (which is race-prone right
384
+ * after a completion).
385
+ *
386
+ * Idempotent replay: a second call for an already-completed
387
+ * payment still succeeds and returns the current `newBalance`,
388
+ * but the deduction breakdown is absent because the original
389
+ * move is not re-derivable from persisted state.
390
+ */
391
+ completeWithBalance: (paymentCode: string) => Promise<CompleteWithBalanceResult>;
194
392
  /** Get available payment methods for checkout */
195
393
  getPaymentMethods: () => Promise<PaymentMethod[]>;
196
394
  };
@@ -199,9 +397,18 @@ export declare class GameCoreClient {
199
397
  list: () => Promise<Order[]>;
200
398
  /** Get single order by code (includes items + payment) */
201
399
  get: (code: string) => Promise<Order>;
202
- /** Get orders by payment code (route is under /checkout prefix) */
203
- getByPayment: (paymentCode: string) => Promise<{
204
- payment: unknown;
400
+ /**
401
+ * Get orders by payment code (route is under /checkout prefix).
402
+ *
403
+ * Authenticated users are verified via the session cookie.
404
+ * Guest callers must pass the `guestEmail` used at checkout — the
405
+ * server enforces a case-insensitive match against the payment
406
+ * email before returning any order data. Without it, a guest
407
+ * request returns 404 (the same response as a missing code, so
408
+ * the endpoint does not leak existence of valid codes).
409
+ */
410
+ getByPayment: (paymentCode: string, guestEmail?: string) => Promise<{
411
+ payment: PaymentInfo;
205
412
  orders: Order[];
206
413
  }>;
207
414
  /** Preview cancel — check if cancellable and refund amount */
@@ -263,6 +470,57 @@ export declare class GameCoreClient {
263
470
  getBroadcastStatus: () => Promise<{
264
471
  subscribed: boolean;
265
472
  }>;
473
+ /** List the current user's conversations (most-recent first). */
474
+ getConversations: () => Promise<Conversation[]>;
475
+ /**
476
+ * Fetch a conversation header plus its messages. Marks any
477
+ * unread admin/system messages as read as a side effect.
478
+ */
479
+ getConversationMessages: (conversationId: number, params?: {
480
+ limit?: number;
481
+ offset?: number;
482
+ }) => Promise<ConversationDetail>;
483
+ /**
484
+ * Submit a code to fulfill an open "code requested" request.
485
+ * Rate-limited to 5 submissions/minute per IP.
486
+ */
487
+ submitCode: (conversationId: number, requestId: number, code: string) => Promise<{
488
+ success: boolean;
489
+ data?: {
490
+ requestId: number;
491
+ };
492
+ }>;
493
+ /**
494
+ * Submit a screenshot file to fulfill an open "screenshot
495
+ * requested" request. Accepts a browser `File` object and
496
+ * delivers it via multipart upload. Rate-limited to 3
497
+ * submissions/minute per IP.
498
+ */
499
+ submitScreenshot: (conversationId: number, requestId: number, file: File) => Promise<{
500
+ success: boolean;
501
+ data?: {
502
+ requestId: number;
503
+ };
504
+ }>;
505
+ /** Get the VAPID public key needed to create a PushSubscription. */
506
+ getPushPublicKey: () => Promise<{
507
+ publicKey: string;
508
+ }>;
509
+ /**
510
+ * Register a push subscription for the current user + site.
511
+ * Call `PushSubscription.toJSON()` in the browser and pass the
512
+ * result directly.
513
+ */
514
+ subscribePush: (subscription: WebPushSubscriptionInput) => Promise<{
515
+ success: boolean;
516
+ }>;
517
+ /**
518
+ * Remove a push subscription by its endpoint. Call this when
519
+ * the user opts out or changes devices.
520
+ */
521
+ unsubscribePush: (endpoint: string) => Promise<{
522
+ success: boolean;
523
+ }>;
266
524
  };
267
525
  favorites: {
268
526
  /** List user's favorite products */
@@ -311,6 +569,38 @@ export declare class GameCoreClient {
311
569
  }>;
312
570
  /** Get commission history */
313
571
  getCommissions: () => Promise<ReferralCommission[]>;
572
+ /**
573
+ * Get a small grid of popular products so referrers can
574
+ * create a one-click reflink without hunting through the
575
+ * catalog. Sorted primarily by referral transaction count on
576
+ * the current site — i.e. "games that other referrers are
577
+ * actually earning commission on". Falls back to the homepage
578
+ * ranking on brand-new sites that have no referral history
579
+ * yet, so the grid is never empty for a new referrer.
580
+ *
581
+ * @param limit - Max number of entries to return (default 10,
582
+ * capped at 50 server-side).
583
+ */
584
+ getPopularProducts: (limit?: number) => Promise<{
585
+ slug: string;
586
+ name: string;
587
+ icon: string | null;
588
+ }[]>;
589
+ /**
590
+ * Get performance stats aggregated over an optional date range.
591
+ *
592
+ * Pass `from` / `to` as Date or ISO-8601 string to filter
593
+ * commissions, transactions, and new-member counts. Clicks
594
+ * remain lifetime — see `ReferralPerformance` docstring for
595
+ * the rationale. Omit both to get all-time totals.
596
+ *
597
+ * Typical usage: wire to 7d / 30d / 90d / all buttons on the
598
+ * profile referrals dashboard.
599
+ */
600
+ getPerformance: (params?: {
601
+ from?: Date | string;
602
+ to?: Date | string;
603
+ }) => Promise<ReferralPerformance>;
314
604
  };
315
605
  reviews: {
316
606
  /** Get public reviews (paginated) */
@@ -328,8 +618,14 @@ export declare class GameCoreClient {
328
618
  getMine: () => Promise<Review[]>;
329
619
  /** Get orders eligible for review */
330
620
  getPending: () => Promise<Order[]>;
331
- /** Submit review for a completed order */
332
- create: (orderId: number, rating: number, text?: string) => Promise<Review>;
621
+ /**
622
+ * Submit a review for a completed order. Returns a narrow
623
+ * {@link ReviewCreateResult} with the new id, rating, text echo,
624
+ * and optional bonus grant. To fetch the full review record
625
+ * (including `createdAt`, `authorName`, `adminReply`, etc.),
626
+ * call `reviews.getMine()` after the create completes.
627
+ */
628
+ create: (orderId: number, rating: number, text?: string) => Promise<ReviewCreateResult>;
333
629
  };
334
630
  topup: {
335
631
  /** Get available topup payment methods */
@@ -340,8 +636,19 @@ export declare class GameCoreClient {
340
636
  getStatus: (code: string) => Promise<TopupStatus>;
341
637
  };
342
638
  giftCards: {
343
- /** Purchase a gift card (amount in USD) */
344
- purchase: (amountUsd: number) => Promise<GiftCard>;
639
+ /**
640
+ * Purchase a gift card. The amount is in RUB (despite the
641
+ * legacy SDK parameter name — the column was misnamed and has
642
+ * always held RUB values, see Wave 3 audit tasks/112a). The
643
+ * method accepts either a plain number for backwards
644
+ * compatibility or an options object with an explicit message.
645
+ *
646
+ * @param amountRub - RUB amount to deduct from the user's
647
+ * balance and encode into the gift card.
648
+ * @param message - Optional personal note shown to the
649
+ * redeeming user.
650
+ */
651
+ purchase: (amountRub: number, message?: string) => Promise<GiftCard>;
345
652
  /** Redeem a gift card code */
346
653
  redeem: (code: string) => Promise<{
347
654
  amount: number;
package/dist/index.js CHANGED
@@ -84,7 +84,9 @@ class GameCoreClient {
84
84
  getCookieConsent: () => this.request("GET", "/site/cookie-consent"),
85
85
  getCatalogSections: () => this.request("GET", "/site/catalog-sections"),
86
86
  getBanners: () => this.request("GET", "/site/banners"),
87
- getAnnouncementBar: () => this.request("GET", "/site/announcement-bar")
87
+ getAnnouncementBar: () => this.request("GET", "/site/announcement-bar"),
88
+ getSitemapData: () => this.request("GET", "/seo/sitemap-data"),
89
+ requestGame: (data) => this.request("POST", "/site/request-game", data)
88
90
  };
89
91
  auth = {
90
92
  initTelegram: () => this.request("POST", "/auth/telegram/init"),
@@ -105,12 +107,25 @@ class GameCoreClient {
105
107
  }
106
108
  }, intervalMs);
107
109
  }),
108
- getVkAuthUrl: (redirectUri) => this.request("GET", `/auth/vk/url?redirect=${encodeURIComponent(redirectUri)}`),
110
+ verifyMiniApp: (initData, ref) => this.request("POST", "/auth/telegram/miniapp", { initData, ref }, { rawResponse: true }),
111
+ verifyTelegramWidget: (data, ref) => this.request("POST", "/auth/telegram/widget", { ...data, ref }, { rawResponse: true }),
112
+ getVkAuthUrl: (redirectUri, ref) => {
113
+ const params = new URLSearchParams;
114
+ params.set("redirect", redirectUri);
115
+ if (ref)
116
+ params.set("ref", ref);
117
+ return this.request("GET", `/auth/vk/url?${params.toString()}`, undefined, { rawResponse: true });
118
+ },
119
+ vkCallback: (code, state) => this.request("POST", "/auth/vk/callback", { code, state }, { rawResponse: true }),
109
120
  verifyVk: (accessToken, ref) => this.request("POST", "/auth/vk/verify", { accessToken, ref }, { rawResponse: true }),
121
+ register: (email, password, firstName, ref) => this.request("POST", "/auth/register", { email, password, firstName, ref }, { rawResponse: true }),
122
+ login: (email, password) => this.request("POST", "/auth/login", { email, password }, { rawResponse: true }),
123
+ changePassword: (currentPassword, newPassword) => this.request("POST", "/auth/change-password", { currentPassword, newPassword }),
110
124
  getMe: () => this.request("GET", "/auth/me", undefined, { rawResponse: true }),
111
125
  logout: () => this.request("POST", "/auth/logout"),
112
126
  getIdentities: () => this.request("GET", "/auth/identities", undefined, { rawResponse: true }),
113
127
  linkVk: (accessToken) => this.request("POST", "/auth/link/vk", { accessToken }),
128
+ linkEmail: (email, password) => this.request("POST", "/auth/link-email", { email, password }),
114
129
  unlinkProvider: (provider) => this.request("POST", `/auth/unlink/${provider}`),
115
130
  mergePreview: (accessToken) => this.request("POST", "/auth/merge/preview", { accessToken }),
116
131
  mergeConfirm: (accessToken) => this.request("POST", "/auth/merge/confirm", { accessToken })
@@ -118,6 +133,10 @@ class GameCoreClient {
118
133
  catalog = {
119
134
  getGames: (params) => {
120
135
  const qs = new URLSearchParams;
136
+ if (params?.page)
137
+ qs.set("page", String(params.page));
138
+ if (params?.limit)
139
+ qs.set("limit", String(params.limit));
121
140
  if (params?.locale)
122
141
  qs.set("locale", params.locale);
123
142
  if (params?.type)
@@ -130,6 +149,8 @@ class GameCoreClient {
130
149
  qs.set("inStockOnly", "true");
131
150
  if (params?.sort)
132
151
  qs.set("sort", params.sort);
152
+ if (params?.q)
153
+ qs.set("q", params.q);
133
154
  const q = qs.toString();
134
155
  return this.request("GET", `/catalog/games${q ? `?${q}` : ""}`);
135
156
  },
@@ -173,6 +194,7 @@ class GameCoreClient {
173
194
  get: () => this.request("GET", "/cart"),
174
195
  add: (item) => this.request("POST", "/cart", item),
175
196
  sync: (items) => this.request("POST", "/cart/sync", { items }),
197
+ merge: (items) => this.request("POST", "/cart/merge", { items }),
176
198
  remove: (id) => this.request("DELETE", `/cart/${id}`),
177
199
  clear: () => this.request("DELETE", "/cart")
178
200
  };
@@ -189,7 +211,10 @@ class GameCoreClient {
189
211
  orders = {
190
212
  list: () => this.request("GET", "/orders"),
191
213
  get: (code) => this.request("GET", `/orders/${code}`),
192
- getByPayment: (paymentCode) => this.request("GET", `/checkout/orders/payment/${paymentCode}`, undefined, { rawResponse: true }),
214
+ getByPayment: (paymentCode, guestEmail) => {
215
+ const qs = guestEmail ? `?email=${encodeURIComponent(guestEmail)}` : "";
216
+ return this.request("GET", `/checkout/orders/payment/${paymentCode}${qs}`, undefined, { rawResponse: true });
217
+ },
193
218
  cancelPreview: (code) => this.request("GET", `/orders/${code}/cancel-preview`),
194
219
  cancel: (code) => this.request("POST", `/orders/${code}/cancel`)
195
220
  };
@@ -216,7 +241,27 @@ class GameCoreClient {
216
241
  updateSettings: (data) => this.request("PUT", "/profile/settings", data),
217
242
  subscribeBroadcast: () => this.request("POST", "/profile/broadcast/subscribe"),
218
243
  unsubscribeBroadcast: () => this.request("POST", "/profile/broadcast/unsubscribe"),
219
- getBroadcastStatus: () => this.request("GET", "/profile/broadcast/status")
244
+ getBroadcastStatus: () => this.request("GET", "/profile/broadcast/status"),
245
+ getConversations: () => this.request("GET", "/profile/conversations"),
246
+ getConversationMessages: (conversationId, params) => {
247
+ const qs = new URLSearchParams;
248
+ if (params?.limit)
249
+ qs.set("limit", String(params.limit));
250
+ if (params?.offset)
251
+ qs.set("offset", String(params.offset));
252
+ const q = qs.toString();
253
+ return this.request("GET", `/profile/conversations/${conversationId}/messages${q ? `?${q}` : ""}`);
254
+ },
255
+ submitCode: (conversationId, requestId, code) => this.request("POST", `/profile/conversations/${conversationId}/submit-code`, { requestId, code }),
256
+ submitScreenshot: (conversationId, requestId, file) => {
257
+ const form = new FormData;
258
+ form.append("requestId", String(requestId));
259
+ form.append("file", file);
260
+ return this.request("POST", `/profile/conversations/${conversationId}/submit-screenshot`, form);
261
+ },
262
+ getPushPublicKey: () => this.request("GET", "/profile/push/public-key"),
263
+ subscribePush: (subscription) => this.request("POST", "/profile/push/subscribe", subscription),
264
+ unsubscribePush: (endpoint) => this.request("POST", "/profile/push/unsubscribe", { endpoint })
220
265
  };
221
266
  favorites = {
222
267
  list: () => this.request("GET", "/favorites"),
@@ -236,7 +281,22 @@ class GameCoreClient {
236
281
  updateLink: (id, data) => this.request("PUT", `/referral/links/${id}`, data),
237
282
  deleteLink: (id) => this.request("DELETE", `/referral/links/${id}`),
238
283
  getLinkStats: (id) => this.request("GET", `/referral/links/${id}/stats`),
239
- getCommissions: () => this.request("GET", "/referral/commissions")
284
+ getCommissions: () => this.request("GET", "/referral/commissions"),
285
+ getPopularProducts: (limit) => {
286
+ const qs = limit ? `?limit=${limit}` : "";
287
+ return this.request("GET", `/referral/popular-products${qs}`);
288
+ },
289
+ getPerformance: (params) => {
290
+ const qs = new URLSearchParams;
291
+ if (params?.from) {
292
+ qs.set("from", params.from instanceof Date ? params.from.toISOString() : params.from);
293
+ }
294
+ if (params?.to) {
295
+ qs.set("to", params.to instanceof Date ? params.to.toISOString() : params.to);
296
+ }
297
+ const q = qs.toString();
298
+ return this.request("GET", `/referral/performance${q ? `?${q}` : ""}`);
299
+ }
240
300
  };
241
301
  reviews = {
242
302
  listPublic: (params) => {
@@ -259,7 +319,11 @@ class GameCoreClient {
259
319
  getRandom: (limit = 5) => this.request("GET", `/reviews/public/random?limit=${limit}`),
260
320
  getMine: () => this.request("GET", "/reviews/mine"),
261
321
  getPending: () => this.request("GET", "/reviews/pending"),
262
- create: (orderId, rating, text) => this.request("POST", "/reviews", { orderId, rating, text })
322
+ create: (orderId, rating, text) => this.request("POST", "/reviews", {
323
+ orderId,
324
+ rating,
325
+ text
326
+ })
263
327
  };
264
328
  topup = {
265
329
  getPaymentMethods: () => this.request("GET", "/topup/payment-methods"),
@@ -267,7 +331,10 @@ class GameCoreClient {
267
331
  getStatus: (code) => this.request("GET", `/topup/${code}`)
268
332
  };
269
333
  giftCards = {
270
- purchase: (amountUsd) => this.request("POST", "/gift-cards/purchase", { amountUsd }),
334
+ purchase: (amountRub, message) => this.request("POST", "/gift-cards/purchase", {
335
+ amountRub,
336
+ message
337
+ }),
271
338
  redeem: (code) => this.request("POST", "/gift-cards/redeem", { code }),
272
339
  check: (code) => this.request("GET", `/gift-cards/check/${code}`),
273
340
  getMine: () => this.request("GET", "/profile/gift-cards")
package/dist/types.d.ts CHANGED
@@ -8,6 +8,47 @@ export interface AuthStatusResponse {
8
8
  status: "pending" | "verified" | "authenticated" | "expired" | "used";
9
9
  user?: User;
10
10
  }
11
+ /**
12
+ * Payload returned by Telegram Login Widget when the user completes
13
+ * authentication. Same shape as Telegram's official docs — you can hand
14
+ * this object directly to auth.verifyTelegramWidget().
15
+ * https://core.telegram.org/widgets/login
16
+ */
17
+ export interface TelegramWidgetUser {
18
+ id: number;
19
+ first_name: string;
20
+ last_name?: string;
21
+ username?: string;
22
+ photo_url?: string;
23
+ auth_date: number;
24
+ hash: string;
25
+ }
26
+ /**
27
+ * Narrow user DTO returned by /auth/telegram/miniapp and /auth/telegram/widget.
28
+ * This is intentionally a subset of the full {@link User} type — the auth
29
+ * endpoints only echo identity + role, not balance/level/email fields, so
30
+ * typing it as `User` would lie to SDK consumers. Call auth.getMe() after
31
+ * login if the storefront needs the full profile.
32
+ */
33
+ export interface TelegramAuthUser {
34
+ id: number;
35
+ telegramId: string | null;
36
+ firstName: string | null;
37
+ lastName: string | null;
38
+ username: string | null;
39
+ photoUrl: string | null;
40
+ role: string;
41
+ }
42
+ /**
43
+ * Response from the Mini App / Widget POST verification endpoints.
44
+ * Body includes success, status and (on success) the authenticated user.
45
+ */
46
+ export interface TelegramAuthResponse {
47
+ success: boolean;
48
+ status?: "authenticated";
49
+ user?: TelegramAuthUser;
50
+ error?: string;
51
+ }
11
52
  export interface User {
12
53
  id: number;
13
54
  firstName: string;
@@ -84,13 +125,36 @@ export interface GameDetail {
84
125
  name: string;
85
126
  icon: string | null;
86
127
  localIcon?: string | null;
128
+ /** Landscape hero image (1200x630). Null if no gallery is available. */
87
129
  coverImage?: string | null;
130
+ /** Gallery of product screenshots (resized to 800x600). */
88
131
  images?: string[];
89
132
  description: string | null;
133
+ /** Editorial badges like "hit", "new", "popular", "sale". */
134
+ tags?: string[];
135
+ /** Availability lifecycle. Shares the union from {@link Game}. */
136
+ availabilityStatus?: "available" | "coming_soon" | "maintenance" | "discontinued";
137
+ /** Human-readable message for non-available games. */
138
+ availabilityMessage?: string | null;
90
139
  inStock?: boolean;
91
140
  productCount?: number;
92
141
  categories: Category[];
93
142
  seo?: SeoContent | null;
143
+ /**
144
+ * Present only when the request used an alias slug and the backend
145
+ * resolved it to a different canonical slug. When set, the storefront
146
+ * should issue a 301 redirect to `/games/${canonicalSlug}` instead
147
+ * of rendering the page. Absent when the requested slug is already
148
+ * canonical.
149
+ */
150
+ canonicalSlug?: string;
151
+ /**
152
+ * ISO-8601 timestamp of the last meaningful edit on this canonical
153
+ * game — bumped on catalog changes (products, pricing, metadata).
154
+ * Storefronts should use this as `lastModified` in sitemap entries
155
+ * so Google doesn't refetch pages that haven't changed.
156
+ */
157
+ updatedAt?: string;
94
158
  }
95
159
  /**
96
160
  * Per-site SEO content for a game page.
@@ -188,6 +252,29 @@ export interface CartItem {
188
252
  productName: string;
189
253
  price: number;
190
254
  deliveryData: Record<string, string>;
255
+ /**
256
+ * Quantity for multi-amount products (e.g. N × 500 Robux).
257
+ * Optional when writing (server defaults to `1`); always
258
+ * present on server responses.
259
+ */
260
+ quantity?: number;
261
+ /**
262
+ * ISO-8601 timestamp of when the row was inserted. Absent when
263
+ * the item was freshly constructed client-side and hasn't been
264
+ * sent to the server yet.
265
+ */
266
+ addedAt?: string;
267
+ /**
268
+ * Canonical game icon (localIcon → icon fallback). May be `null`
269
+ * if the underlying game has no artwork. Never present on
270
+ * client-authored items — only populated on server responses.
271
+ */
272
+ gameIcon?: string | null;
273
+ /**
274
+ * Supplier product icon (localIcon → icon fallback). Same
275
+ * semantics as `gameIcon`.
276
+ */
277
+ productIcon?: string | null;
191
278
  }
192
279
  export interface CheckoutRequest {
193
280
  email?: string;
@@ -215,6 +302,46 @@ export interface CheckoutResponse {
215
302
  }>;
216
303
  }
217
304
  export type OrderStatus = "pending" | "paid" | "processing" | "completed" | "cancelled" | "refunded" | "failed";
305
+ /**
306
+ * Payment record returned by `checkout.getByPayment()`.
307
+ *
308
+ * A payment groups one or more orders created in the same checkout
309
+ * call. The `code` is the external identifier exposed to the
310
+ * payment gateway and returned via the success URL query string.
311
+ *
312
+ * `paymentMethod` and `gatewayType` are `null` when the payment is
313
+ * fully settled from the user's balance (no external gateway is
314
+ * involved). `completedAt` is set when the payment transitions to
315
+ * `completed` via webhook confirmation or balance settlement; it
316
+ * remains `null` for `pending`, `failed`, and `cancelled` payments
317
+ * (the failure/cancellation timestamp is tracked separately by
318
+ * the stale-payment scanner and not surfaced on this shape).
319
+ */
320
+ export interface PaymentInfo {
321
+ code: string;
322
+ totalAmount: number;
323
+ status: "pending" | "completed" | "failed" | "cancelled";
324
+ paymentMethod: string | null;
325
+ gatewayType: string | null;
326
+ createdAt: string;
327
+ completedAt: string | null;
328
+ }
329
+ /**
330
+ * Normalised delivery hint attached to an order. Used by the
331
+ * storefront "My Orders" page to decide which CTA to show
332
+ * (robux-via-pass → "Check Roblox transactions"; gift card →
333
+ * "Copy code"; login delivery → "Waiting for credentials"; etc).
334
+ *
335
+ * `helpTextKey` is an i18n key — the storefront owns the
336
+ * translation layer, so copy changes do not require a backend
337
+ * deploy. Unknown `kind` values should render a generic
338
+ * "processing" message as a fallback.
339
+ */
340
+ export interface OrderDeliveryMeta {
341
+ kind: "cdkey" | "login" | "pass_based" | "service" | "manual" | (string & {});
342
+ helpTextKey: string;
343
+ checkTransactionsUrl?: string;
344
+ }
218
345
  export interface Order {
219
346
  id: number;
220
347
  code: string;
@@ -231,6 +358,13 @@ export interface Order {
231
358
  };
232
359
  createdAt: string;
233
360
  completedAt?: string;
361
+ /**
362
+ * Delivery hint for the storefront UI — present on responses
363
+ * from `/profile/orders` and `/orders/:code` starting with
364
+ * SDK 0.13. Absent on legacy responses and on endpoints that
365
+ * do not currently surface it.
366
+ */
367
+ deliveryMeta?: OrderDeliveryMeta;
234
368
  }
235
369
  export interface OrderItem {
236
370
  id: number;
@@ -257,6 +391,20 @@ export interface UserBalance {
257
391
  expiresAt: string;
258
392
  }>;
259
393
  }
394
+ /**
395
+ * One rung of the loyalty level ladder — used by the "how levels
396
+ * work" explainer on the profile page. Shape matches what the
397
+ * server computes from `loyalty_levels` (per-site custom) or the
398
+ * hardcoded default ladder.
399
+ */
400
+ export interface LevelSystemLevel {
401
+ level: number;
402
+ name: string;
403
+ discountPercent: number;
404
+ minSpendingUsd: number;
405
+ minReviews: number;
406
+ minReferrals: number;
407
+ }
260
408
  export interface LevelStatus {
261
409
  currentLevel: number;
262
410
  currentDiscount: number;
@@ -280,6 +428,19 @@ export interface LevelStatus {
280
428
  met: boolean;
281
429
  };
282
430
  };
431
+ /**
432
+ * Highest level in the ladder. Storefronts should use this
433
+ * instead of hardcoding "15" so per-site custom ladders render
434
+ * correctly. Always present on responses from SDK 0.13+.
435
+ */
436
+ maxLevel?: number;
437
+ /** Highest discount percent in the ladder. */
438
+ maxDiscount?: number;
439
+ /**
440
+ * Full ordered ladder. Use this to render the explainer table
441
+ * or a progress timeline. Omitted on very old responses.
442
+ */
443
+ allLevels?: LevelSystemLevel[];
283
444
  }
284
445
  export interface Transaction {
285
446
  id: number;
@@ -311,27 +472,212 @@ export interface Favorite {
311
472
  categorySlug?: string;
312
473
  createdAt: string;
313
474
  }
475
+ /**
476
+ * Review record. Fields are endpoint-specific — this interface is a
477
+ * union of everything any review endpoint may return, and real-world
478
+ * shape depends on which route you call:
479
+ *
480
+ * - `reviews.listPublic()` → `id`, `rating`, `text`, `authorName`,
481
+ * `gameName`, `adminReply`, `adminReplyAt`,
482
+ * `createdAt`
483
+ * - `reviews.mine()` → `id`, `rating`, `text`, `adminReply`,
484
+ * `adminReplyAt`, `createdAt`, `order`
485
+ * - `reviews.create()` → the freshly-created review + `bonus`
486
+ *
487
+ * Fields that are not relevant to a given endpoint are `undefined`. Do
488
+ * not assume any single field is always present.
489
+ */
314
490
  export interface Review {
315
491
  id: number;
316
- userId: number;
317
- userName?: string;
318
- orderId: number;
319
- orderCode?: string;
320
492
  rating: number;
321
493
  text?: string;
494
+ createdAt: string;
495
+ /** Public display name (author). Returned by `listPublic()`. */
496
+ authorName?: string;
497
+ /** Legacy alias for `authorName`. Prefer `authorName` on new code. */
498
+ userName?: string;
499
+ /** Owner of the review. Returned only to authenticated admins. */
500
+ userId?: number;
501
+ /** Link back to the order the review was left for. */
502
+ orderId?: number;
503
+ orderCode?: string;
322
504
  gameName?: string;
505
+ /**
506
+ * Public reply from site support. Rendered under the review body
507
+ * as "Ответ поддержки". `null` if support has not replied or the
508
+ * reply was removed. Included in both the public listing and the
509
+ * authenticated profile reviews list.
510
+ */
511
+ adminReply?: string | null;
512
+ /**
513
+ * ISO-8601 timestamp of when the support reply was created or last
514
+ * updated. `null` when no reply is present.
515
+ */
516
+ adminReplyAt?: string | null;
517
+ /**
518
+ * Nested order summary, returned only by `reviews.mine()` so the
519
+ * profile "My reviews" page can show game + order code without a
520
+ * second fetch. `null` when the underlying order was deleted.
521
+ */
522
+ order?: {
523
+ code: string;
524
+ gameName: string;
525
+ createdAt: string;
526
+ } | null;
527
+ /** Bonus granted for leaving the review, if any. */
323
528
  bonus?: {
324
529
  amount: number;
325
530
  percent: number;
326
531
  expiresAt: string;
327
532
  };
328
- createdAt: string;
329
533
  }
330
534
  export interface ReviewStats {
331
535
  averageRating: number;
332
536
  totalCount: number;
333
537
  distribution?: Record<string, number>;
334
538
  }
539
+ /**
540
+ * Response shape returned by `checkout.completeWithBalance()`.
541
+ *
542
+ * The post-deduction balance (`newBalance`) is always present on a
543
+ * successful response — the storefront success page can render
544
+ * "new balance: X" without a second `profile.getBalance()` fetch
545
+ * (which is race-prone right after completion).
546
+ *
547
+ * The deduction breakdown (`balanceUsed`, `balanceDeduction`) is
548
+ * present **only on the first successful call** — it reflects what
549
+ * was actually moved by this request. If the client retries the
550
+ * endpoint for an already-completed payment (idempotent replay),
551
+ * those fields are `undefined` because the original breakdown is
552
+ * not re-derivable from persisted state. The `newBalance` is still
553
+ * included in that case so the receipt UI has something to render.
554
+ *
555
+ * When present, `balanceUsed` equals
556
+ * `balanceDeduction.fromBonus + balanceDeduction.fromPermanent`.
557
+ */
558
+ export interface CompleteWithBalanceResult {
559
+ paymentCode: string;
560
+ status: "completed";
561
+ balanceUsed?: number;
562
+ balanceDeduction?: {
563
+ fromBonus: number;
564
+ fromPermanent: number;
565
+ };
566
+ newBalance: {
567
+ total: number;
568
+ permanent: number;
569
+ bonus: number;
570
+ };
571
+ }
572
+ /**
573
+ * Short summary of a conversation as returned by
574
+ * `profile.getConversations()` — one row per user-owned order
575
+ * thread. Use the `id` to fetch full messages via
576
+ * `profile.getConversationMessages(id)`.
577
+ */
578
+ export interface Conversation {
579
+ id: number;
580
+ orderId: number;
581
+ orderCode: string | null;
582
+ gameName: string | null;
583
+ orderStatus: string | null;
584
+ /** Number of unread system / admin messages. User messages don't count. */
585
+ unreadCount: number;
586
+ /**
587
+ * Preview of the most recent message. `body` is truncated to ~100
588
+ * chars. `null` if the conversation has no messages yet.
589
+ */
590
+ lastMessage: {
591
+ body: string;
592
+ authorType: "user" | "admin" | "system";
593
+ messageType: "text" | "status_change" | "code_requested" | "screenshot_requested" | (string & {});
594
+ createdAt: string;
595
+ } | null;
596
+ /** Last time either party posted a message, ISO-8601. */
597
+ lastMessageAt: string;
598
+ }
599
+ /**
600
+ * Single message inside a conversation. `metadata` is parsed JSON —
601
+ * system messages may carry status-change payloads, request ids, etc.
602
+ * Free chat (user-authored `text` messages) is currently disabled —
603
+ * expect only `admin`, `system`, and request-response messages.
604
+ */
605
+ export interface ConversationMessage {
606
+ id: number;
607
+ authorType: "user" | "admin" | "system";
608
+ authorId: number | null;
609
+ messageType: "text" | "status_change" | "code_requested" | "screenshot_requested" | (string & {});
610
+ body: string;
611
+ metadata: Record<string, unknown> | null;
612
+ /** Set when the current viewer marked the message as read. */
613
+ readAt: string | null;
614
+ createdAt: string;
615
+ }
616
+ /**
617
+ * Open supplier-initiated request inside a conversation. The UI
618
+ * decides which input to show the user based on `requestType`.
619
+ */
620
+ export interface ConversationRequest {
621
+ id: number;
622
+ conversationId: number;
623
+ orderId: number;
624
+ orderItemId: number;
625
+ requestType: "code" | "screenshot";
626
+ status: "open" | "fulfilled" | "cancelled" | "expired";
627
+ requestedAt: string;
628
+ fulfilledAt: string | null;
629
+ }
630
+ /**
631
+ * Full detail returned by `profile.getConversationMessages(id)`.
632
+ * The conversation header is included so the UI does not need a
633
+ * separate fetch to render the page title / order summary.
634
+ */
635
+ export interface ConversationDetail {
636
+ conversation: {
637
+ id: number;
638
+ orderId: number;
639
+ orderCode: string | null;
640
+ gameName: string | null;
641
+ orderStatus: string | null;
642
+ };
643
+ messages: ConversationMessage[];
644
+ /** Open supplier requests the user can respond to right now. */
645
+ openRequests: ConversationRequest[];
646
+ }
647
+ /**
648
+ * Web Push subscription payload returned by the browser's
649
+ * `PushSubscription.toJSON()`. Pass this to
650
+ * `profile.subscribePush()` to start receiving push notifications
651
+ * for order status updates and conversation replies.
652
+ */
653
+ export interface WebPushSubscriptionInput {
654
+ endpoint: string;
655
+ keys: {
656
+ p256dh: string;
657
+ auth: string;
658
+ };
659
+ }
660
+ /**
661
+ * Narrow response returned by `reviews.create()`.
662
+ *
663
+ * The create endpoint deliberately returns less than a full `Review`
664
+ * — only the fields the storefront needs to acknowledge the
665
+ * submission: the new review id, the numeric rating, the text echo,
666
+ * and the optional bonus grant metadata.
667
+ *
668
+ * Callers who need the full review (with `createdAt`, `authorName`,
669
+ * etc.) should re-fetch via `reviews.getMine()` after the create.
670
+ */
671
+ export interface ReviewCreateResult {
672
+ id: number;
673
+ rating: number;
674
+ text: string | null;
675
+ bonus: {
676
+ amount: number;
677
+ percent: number;
678
+ expiresAt: string;
679
+ } | null;
680
+ }
335
681
  export interface CouponResult {
336
682
  type: string;
337
683
  value: number;
@@ -366,13 +712,50 @@ export interface ReferralCommission {
366
712
  commissionPercent: number;
367
713
  createdAt: string;
368
714
  }
715
+ /**
716
+ * Date-range aggregated performance metrics for a referrer.
717
+ *
718
+ * Commission, transaction and member counts honour the `from` /
719
+ * `to` range passed to `referrals.getPerformance()`. `totalClicks`
720
+ * is **lifetime across all of the referrer's links** and is NOT
721
+ * date-filtered — click events are not individually timestamped in
722
+ * the current schema. Storefronts should label the clicks metric
723
+ * accordingly ("all-time clicks" vs. "period earnings").
724
+ */
725
+ export interface ReferralPerformance {
726
+ totalCommission: number;
727
+ totalClicks: number;
728
+ totalTransactions: number;
729
+ newMembers: number;
730
+ }
369
731
  export interface TopupMethod {
370
732
  type: string;
371
733
  label: string;
372
734
  description?: string;
373
735
  feePercent?: number;
374
736
  gatewayType?: string;
375
- invoiceCurrency?: string;
737
+ invoiceCurrency?: string | null;
738
+ /**
739
+ * Minimum topup amount in RUB. `null` means the storefront
740
+ * should use its global minimum (typically 50 RUB). The topup
741
+ * route enforces this server-side too.
742
+ */
743
+ minAmount?: number | null;
744
+ /**
745
+ * Maximum topup amount in RUB. `null` means "use global cap".
746
+ */
747
+ maxAmount?: number | null;
748
+ /**
749
+ * Grouping bucket for collapsible UIs. Known values: `"direct"`,
750
+ * `"p2p"`, `"intl"`. Unknown values should render as a
751
+ * generic "Other" group.
752
+ */
753
+ group?: "direct" | "p2p" | "intl" | (string & {}) | null;
754
+ /**
755
+ * Optional warning text rendered underneath the method
756
+ * selector (e.g. "Временно нестабильно"). `null` means no note.
757
+ */
758
+ stabilityNote?: string | null;
376
759
  }
377
760
  export interface TopupResponse {
378
761
  code: string;
@@ -385,13 +768,48 @@ export interface TopupStatus {
385
768
  amount: number;
386
769
  createdAt?: string;
387
770
  }
771
+ /**
772
+ * Gift card voucher. Stored in RUB; the field was historically
773
+ * called `denomination` in the SDK and `amount_usd` in the DB
774
+ * column, but both were misnomers — the value has always been RUB
775
+ * (see Wave 3 #34 audit `tasks/112a-giftcard-currency-audit.md`).
776
+ * Use `amountRub` on new code; `amountUsd` and `denomination` are
777
+ * deprecated aliases that echo the same value.
778
+ */
388
779
  export interface GiftCard {
389
780
  id: number;
390
781
  code: string;
391
- denomination: number;
392
- status: string;
782
+ /** Canonical RUB value. Use this on new code. */
783
+ amountRub: number;
784
+ /**
785
+ * @deprecated Legacy alias echoing `amountRub`. The name was
786
+ * wrong historically — the value was always RUB despite the
787
+ * `Usd` suffix. Remove once storefronts upgrade past SDK 0.13.x.
788
+ */
789
+ amountUsd?: number;
790
+ /**
791
+ * @deprecated Legacy alias echoing `amountRub`. Kept for
792
+ * storefronts that were reading this field from typed code; at
793
+ * runtime the backend never emitted `denomination`, so most
794
+ * consumers were already getting `undefined`.
795
+ */
796
+ denomination?: number;
797
+ /** Currency of `amountRub`. Always `"RUB"` for now. */
798
+ currency: "RUB";
799
+ /**
800
+ * Remaining balance if the card supports partial redemption.
801
+ * For active and expired cards this equals `amountRub`. For
802
+ * redeemed or cancelled cards it is `0`. Partial redemption is
803
+ * not implemented yet — the field is reserved so storefronts
804
+ * can start rendering it now without a future breaking change.
805
+ */
806
+ remainingBalance: number;
807
+ status: "active" | "redeemed" | "expired" | "cancelled" | (string & {});
808
+ message?: string | null;
809
+ /** ISO-8601 expiry timestamp; `null` if the card never expires. */
810
+ expiresAt?: string | null;
393
811
  createdAt: string;
394
- redeemedAt?: string;
812
+ redeemedAt?: string | null;
395
813
  }
396
814
  export interface Announcement {
397
815
  id: number;
@@ -496,6 +914,17 @@ export interface PaginatedResponse<T> {
496
914
  hasMore: boolean;
497
915
  };
498
916
  }
917
+ /** Catalog games use page-based pagination (page 1, 2, ...) */
918
+ export interface PagedGamesResponse {
919
+ data: Game[];
920
+ pagination: {
921
+ page: number;
922
+ limit: number;
923
+ total: number;
924
+ totalPages: number;
925
+ hasMore: boolean;
926
+ };
927
+ }
499
928
  export declare class GameCoreError extends Error {
500
929
  status: number;
501
930
  code?: string;
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@gamecore-api/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.13.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",
7
- "module": "dist/index.mjs",
8
7
  "exports": {
9
8
  ".": {
10
9
  "import": "./dist/index.js",