@smartico/public-api 0.0.402 → 0.0.404

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.
Files changed (153) hide show
  1. package/.claude/skills/api-tsdoc/SKILL.md +675 -0
  2. package/.claude/skills/api-tsdoc-final/SKILL.md +266 -0
  3. package/.claude/skills/api-tsdoc-howto/SKILL.md +143 -0
  4. package/dist/MiniGames/SAWTemplate.d.ts +4 -0
  5. package/dist/NodeCache.d.ts +1 -0
  6. package/dist/OCache.d.ts +7 -0
  7. package/dist/SmarticoGlobal.d.ts +0 -6
  8. package/dist/WSAPI/WSAPITypes.d.ts +43 -0
  9. package/dist/WSAPI/WSAPIUser.d.ts +3 -0
  10. package/dist/index.js +92 -33
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.modern.mjs +60 -10
  13. package/dist/index.modern.mjs.map +1 -1
  14. package/docs/api/interfaces/TUserProfile.md +136 -0
  15. package/docs/capabilities/_responses/buyStoreItem.json +4 -0
  16. package/docs/capabilities/_responses/claimBonus.json +4 -0
  17. package/docs/capabilities/_responses/gamePickGetActiveRound.json +95 -0
  18. package/docs/capabilities/_responses/gamePickGetActiveRounds.json +191 -0
  19. package/docs/capabilities/_responses/gamePickGetBoard.json +47 -0
  20. package/docs/capabilities/_responses/gamePickGetGameInfo.json +355 -0
  21. package/docs/capabilities/_responses/gamePickGetHistory.json +191 -0
  22. package/docs/capabilities/_responses/gamePickGetUserInfo.json +14 -0
  23. package/docs/capabilities/_responses/getAchCategories.json +12 -0
  24. package/docs/capabilities/_responses/getActivityLog.json +1 -0
  25. package/docs/capabilities/_responses/getAvatarPrompts.json +16 -0
  26. package/docs/capabilities/_responses/getAvatarsCustomized.json +12 -0
  27. package/docs/capabilities/_responses/getAvatarsList.json +24 -0
  28. package/docs/capabilities/_responses/getBadges.json +98 -0
  29. package/docs/capabilities/_responses/getBonuses.json +18 -0
  30. package/docs/capabilities/_responses/getClanInfo.json +39 -0
  31. package/docs/capabilities/_responses/getClans.json +35 -0
  32. package/docs/capabilities/_responses/getCurrentLevel.json +20 -0
  33. package/docs/capabilities/_responses/getCustomSections.json +25 -0
  34. package/docs/capabilities/_responses/getInboxMessageBody.json +7 -0
  35. package/docs/capabilities/_responses/getInboxMessages.json +18 -0
  36. package/docs/capabilities/_responses/getInboxUnreadCount.json +1 -0
  37. package/docs/capabilities/_responses/getJackpotEligibleGames.json +20 -0
  38. package/docs/capabilities/_responses/getJackpotWinners.json +28 -0
  39. package/docs/capabilities/_responses/getLeaderBoard.json +45 -0
  40. package/docs/capabilities/_responses/getLevels.json +40 -0
  41. package/docs/capabilities/_responses/getMiniGames.json +208 -0
  42. package/docs/capabilities/_responses/getMiniGamesHistory.json +1 -0
  43. package/docs/capabilities/_responses/getMissions.json +117 -0
  44. package/docs/capabilities/_responses/getRaffleDrawRun.json +62 -0
  45. package/docs/capabilities/_responses/getRaffleDrawRunsHistory.json +34 -0
  46. package/docs/capabilities/_responses/getRaffles.json +140 -0
  47. package/docs/capabilities/_responses/getStoreCategories.json +12 -0
  48. package/docs/capabilities/_responses/getStoreItems.json +47 -0
  49. package/docs/capabilities/_responses/getStorePurchasedItems.json +49 -0
  50. package/docs/capabilities/_responses/getTournamentInstanceInfo.json +75 -0
  51. package/docs/capabilities/_responses/getTournamentsList.json +66 -0
  52. package/docs/capabilities/_responses/getTranslations.json +25 -0
  53. package/docs/capabilities/_responses/getUserLevelExtraCounters.json +4 -0
  54. package/docs/capabilities/_responses/getUserProfile.json +34 -0
  55. package/docs/capabilities/_responses/jackpotGet.json +113 -0
  56. package/docs/capabilities/_responses/jackpotOptIn.json +9 -0
  57. package/docs/capabilities/_responses/jackpotOptOut.json +9 -0
  58. package/docs/capabilities/_responses/markAllInboxMessagesAsRead.json +4 -0
  59. package/docs/capabilities/_responses/markInboxMessageAsRead.json +4 -0
  60. package/docs/capabilities/_responses/markUnmarkInboxMessageAsFavorite.json +4 -0
  61. package/docs/capabilities/_responses/miniGameWinAcknowledgeRequest.json +10 -0
  62. package/docs/capabilities/_responses/playMiniGame.json +6 -0
  63. package/docs/capabilities/_responses/playMiniGameBatch.json +9 -0
  64. package/docs/capabilities/_responses/registerInTournament.json +4 -0
  65. package/docs/capabilities/_responses/requestMissionOptIn.json +4 -0
  66. package/docs/capabilities/_responses/setAvatar.json +4 -0
  67. package/docs/capabilities/avatarsCustomize.md +115 -0
  68. package/docs/capabilities/buyStoreItem.md +179 -0
  69. package/docs/capabilities/checkSegmentListMatch.md +69 -0
  70. package/docs/capabilities/checkSegmentMatch.md +74 -0
  71. package/docs/capabilities/claimBonus.md +125 -0
  72. package/docs/capabilities/claimRafflePrize.md +116 -0
  73. package/docs/capabilities/deleteAllInboxMessages.md +66 -0
  74. package/docs/capabilities/deleteInboxMessage.md +60 -0
  75. package/docs/capabilities/gamePickGetActiveRound.md +248 -0
  76. package/docs/capabilities/gamePickGetActiveRounds.md +259 -0
  77. package/docs/capabilities/gamePickGetBoard.md +202 -0
  78. package/docs/capabilities/gamePickGetGameInfo.md +318 -0
  79. package/docs/capabilities/gamePickGetHistory.md +244 -0
  80. package/docs/capabilities/gamePickGetRoundInfoForUser.md +189 -0
  81. package/docs/capabilities/gamePickGetUserInfo.md +91 -0
  82. package/docs/capabilities/gamePickSubmitSelection.md +222 -0
  83. package/docs/capabilities/gamePickSubmitSelectionQuiz.md +198 -0
  84. package/docs/capabilities/getAchCategories.md +119 -0
  85. package/docs/capabilities/getActivityLog.md +108 -0
  86. package/docs/capabilities/getAvatarPrompts.md +81 -0
  87. package/docs/capabilities/getAvatarsCustomized.md +96 -0
  88. package/docs/capabilities/getAvatarsList.md +101 -0
  89. package/docs/capabilities/getBadges.md +237 -0
  90. package/docs/capabilities/getBonuses.md +159 -0
  91. package/docs/capabilities/getClanInfo.md +164 -0
  92. package/docs/capabilities/getClanTournamentPlayers.md +112 -0
  93. package/docs/capabilities/getClans.md +168 -0
  94. package/docs/capabilities/getCurrentLevel.md +123 -0
  95. package/docs/capabilities/getCustomSections.md +96 -0
  96. package/docs/capabilities/getInboxMessageBody.md +87 -0
  97. package/docs/capabilities/getInboxMessages.md +146 -0
  98. package/docs/capabilities/getInboxUnreadCount.md +54 -0
  99. package/docs/capabilities/getJackpotEligibleGames.md +93 -0
  100. package/docs/capabilities/getJackpotWinners.md +113 -0
  101. package/docs/capabilities/getLeaderBoard.md +187 -0
  102. package/docs/capabilities/getLeaderBoards.md +88 -0
  103. package/docs/capabilities/getLevels.md +118 -0
  104. package/docs/capabilities/getMiniGames.md +295 -0
  105. package/docs/capabilities/getMiniGamesHistory.md +183 -0
  106. package/docs/capabilities/getMissions.md +253 -0
  107. package/docs/capabilities/getRaffleDrawRun.md +210 -0
  108. package/docs/capabilities/getRaffleDrawRunsHistory.md +116 -0
  109. package/docs/capabilities/getRaffleWonPrizes.md +153 -0
  110. package/docs/capabilities/getRaffles.md +249 -0
  111. package/docs/capabilities/getRelatedItemsForGame.md +236 -0
  112. package/docs/capabilities/getStoreCategories.md +128 -0
  113. package/docs/capabilities/getStoreItems.md +260 -0
  114. package/docs/capabilities/getStorePurchasedItems.md +280 -0
  115. package/docs/capabilities/getTournamentInstanceInfo.md +301 -0
  116. package/docs/capabilities/getTournamentsList.md +234 -0
  117. package/docs/capabilities/getTranslations.md +95 -0
  118. package/docs/capabilities/getUserLevelExtraCounters.md +92 -0
  119. package/docs/capabilities/getUserProfile.md +184 -0
  120. package/docs/capabilities/jackpotGet.md +173 -0
  121. package/docs/capabilities/jackpotOptIn.md +94 -0
  122. package/docs/capabilities/jackpotOptOut.md +92 -0
  123. package/docs/capabilities/joinClan.md +169 -0
  124. package/docs/capabilities/markAllInboxMessagesAsRead.md +72 -0
  125. package/docs/capabilities/markInboxMessageAsRead.md +69 -0
  126. package/docs/capabilities/markUnmarkInboxMessageAsFavorite.md +75 -0
  127. package/docs/capabilities/miniGameWinAcknowledgeRequest.md +98 -0
  128. package/docs/capabilities/playMiniGame.md +217 -0
  129. package/docs/capabilities/playMiniGameBatch.md +129 -0
  130. package/docs/capabilities/registerInTournament.md +187 -0
  131. package/docs/capabilities/reportClickEvent.md +74 -0
  132. package/docs/capabilities/reportImpressionEvent.md +67 -0
  133. package/docs/capabilities/requestMissionClaimReward.md +117 -0
  134. package/docs/capabilities/requestMissionOptIn.md +98 -0
  135. package/docs/capabilities/requestRaffleOptin.md +108 -0
  136. package/docs/capabilities/setAvatar.md +132 -0
  137. package/docs/capabilities/smarticoCallbacks.md +165 -0
  138. package/docs/capabilities/smarticoGlobal.md +62 -0
  139. package/package.json +3 -2
  140. package/readme-smartico-dev.md +208 -0
  141. package/scripts/capture-responses-browser.mjs +301 -0
  142. package/scripts/gen-capabilities.js +368 -0
  143. package/scripts/run-public-api.mjs +105 -0
  144. package/src/MiniGames/SAWGetTemplatesResponse.ts +2 -0
  145. package/src/MiniGames/SAWTemplate.ts +5 -0
  146. package/src/NodeCache.ts +8 -0
  147. package/src/OCache.ts +17 -0
  148. package/src/SmarticoGlobal.ts +0 -7
  149. package/src/WSAPI/WSAPI.ts +3 -3
  150. package/src/WSAPI/WSAPIJackpots.ts +4 -1
  151. package/src/WSAPI/WSAPIMiniGames.ts +1 -1
  152. package/src/WSAPI/WSAPITypes.ts +44 -0
  153. package/src/WSAPI/WSAPIUser.ts +30 -5
@@ -0,0 +1,368 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * gen-capabilities.js — deterministic capability-page generator.
4
+ *
5
+ * Stitches the existing single-source-of-truth doc inputs into one
6
+ * self-contained "API capability page" per public WSAPI method:
7
+ *
8
+ * method TSDoc (summary / @remarks contract / @param / @example / {@link})
9
+ * + the return TYPE's own field comments (annotate the response shape)
10
+ * + captured REAL response (docs/capabilities/_responses/<method>.json)
11
+ * ────────────────────────────────────────────────────────────────────────
12
+ * → docs/capabilities/<method>.md
13
+ *
14
+ * The capture step (run_public_api → _responses/<method>.json) is a SEPARATE
15
+ * skill-driven step; this script is pure, deterministic assembly.
16
+ *
17
+ * node scripts/gen-capabilities.js
18
+ */
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const ts = require('typescript');
22
+
23
+ const ROOT = path.join(__dirname, '..');
24
+ const SRC = path.join(ROOT, 'src');
25
+ const WSAPI_DIR = path.join(SRC, 'WSAPI');
26
+ const OUT_DIR = path.join(ROOT, 'docs', 'capabilities');
27
+ const RESP_DIR = path.join(OUT_DIR, '_responses');
28
+
29
+ const SKIP_METHODS = new Set(['clearCaches']);
30
+
31
+ // ── helpers ────────────────────────────────────────────────────────────────
32
+
33
+ function listFiles(dir, ext, acc = []) {
34
+ if (!fs.existsSync(dir)) return acc;
35
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
36
+ const p = path.join(dir, e.name);
37
+ if (e.isDirectory()) listFiles(p, ext, acc);
38
+ else if (e.name.endsWith(ext)) acc.push(p);
39
+ }
40
+ return acc;
41
+ }
42
+
43
+ const parse = (file) =>
44
+ ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
45
+
46
+ const LINK_KINDS = new Set([
47
+ ts.SyntaxKind.JSDocLink, ts.SyntaxKind.JSDocLinkCode, ts.SyntaxKind.JSDocLinkPlain,
48
+ ]);
49
+
50
+ /** Stringify a JSDoc comment (string | NodeArray), rendering {@link X} as `X`. */
51
+ function tagText(comment) {
52
+ if (!comment) return '';
53
+ if (typeof comment === 'string') return comment.trim();
54
+ return comment.map(c => {
55
+ if (typeof c === 'string') return c;
56
+ if (LINK_KINDS.has(c.kind)) {
57
+ const nm = c.name ? c.name.getText() : '';
58
+ const tail = (c.text || '').trim();
59
+ return '`' + (nm || tail) + '`' + (nm && tail ? ' ' + tail : '');
60
+ }
61
+ return c.text ?? '';
62
+ }).join('').replace(/[ \t]+/g, ' ').trim();
63
+ }
64
+
65
+ // Example-response trimming so a method returning a huge map/array (e.g.
66
+ // getTranslations → ~800 keys) doesn't dump a 900-line page. Long strings are
67
+ // clipped; arrays show 2 items; "dictionary-sized" objects (> OBJ_CAP keys, i.e.
68
+ // a key→value map rather than a struct) are truncated to a representative sample.
69
+ // Example-response trimming. NO depth collapse (the real nested shape is the point
70
+ // — e.g. gamePickGetGameInfo.sawTemplate / allRounds). Only bound the pathological
71
+ // cases: long strings, wide arrays, and dictionary-sized maps (getTranslations → 800
72
+ // keys). The full structural definition lives in the type-driven Returns section.
73
+ const EX_ARR = 1, EX_OBJ_CAP = 40, EX_OBJ_KEEP = 12;
74
+ // Noise keys collapsed in the example payload: irrelevant-to-consumer config and
75
+ // keyed duplicates. They're the size driver on the biggest pages and add no signal.
76
+ const NOISE_KEYS = {
77
+ labelInfo: '…(label config — typed `any`, omitted)',
78
+ prizesMap: '…(keyed duplicate of `prizes[]`)',
79
+ };
80
+ function trimVal(v) {
81
+ if (typeof v === 'string') return v.length > 160 ? v.slice(0, 157) + '…' : v;
82
+ if (Array.isArray(v)) return v.slice(0, EX_ARR).map(trimVal);
83
+ if (v && typeof v === 'object') {
84
+ const keys = Object.keys(v);
85
+ const capped = keys.length > EX_OBJ_CAP;
86
+ const o = {};
87
+ for (const k of keys.slice(0, capped ? EX_OBJ_KEEP : keys.length)) {
88
+ o[k] = (NOISE_KEYS[k] && v[k] && typeof v[k] === 'object') ? NOISE_KEYS[k] : trimVal(v[k]);
89
+ }
90
+ if (capped) o['…'] = `(+${keys.length - EX_OBJ_KEEP} more keys)`;
91
+ return o;
92
+ }
93
+ return v;
94
+ }
95
+
96
+ /** First capitalized type identifier in a type-text (the base of `TX[]`, `TX | null`, …). */
97
+ const baseTypeName = (txt) =>
98
+ ((txt || '').match(/[A-Z][A-Za-z0-9_]+/g) || [])
99
+ .find(n => !['Promise', 'Partial', 'Array', 'Record', 'GamesApiResponse', 'Date'].includes(n)) || '';
100
+
101
+ // ── type registry: name → { ext:[names], fields:{ name:{comment,typeText} } } ─
102
+
103
+ function buildTypeRegistry() {
104
+ const reg = {};
105
+ const trailing = (sf, node) => {
106
+ const tr = ts.getTrailingCommentRanges(sf.text, node.end) || [];
107
+ return tr.length ? sf.text.slice(tr[0].pos, tr[0].end).replace(/^\/\/\s?/, '').trim() : '';
108
+ };
109
+ for (const file of listFiles(SRC, '.ts')) {
110
+ const sf = parse(file);
111
+ const visit = (node) => {
112
+ if (ts.isInterfaceDeclaration(node) && node.name) {
113
+ const name = node.name.text;
114
+ const ext = (node.heritageClauses || [])
115
+ .flatMap(h => h.types.map(t => baseTypeName(t.expression.getText(sf)))).filter(Boolean);
116
+ const fields = {};
117
+ for (const m of node.members) {
118
+ if (!ts.isPropertySignature(m) || !m.name) continue;
119
+ const fname = m.name.getText(sf);
120
+ const comment = ts.getJSDocCommentsAndTags(m).map(d => tagText(d.comment)).filter(Boolean).join(' ')
121
+ || trailing(sf, m);
122
+ fields[fname] = { comment: comment.replace(/\s+/g, ' ').trim(), typeText: m.type ? m.type.getText(sf) : '' };
123
+ }
124
+ reg[name] = { ext, fields };
125
+ }
126
+ ts.forEachChild(node, visit);
127
+ };
128
+ visit(sf);
129
+ }
130
+ return reg;
131
+ }
132
+
133
+ /** Merge a type's own fields over its inherited fields. */
134
+ function resolveFields(name, reg, seen = new Set()) {
135
+ if (!name || !reg[name] || seen.has(name)) return {};
136
+ seen.add(name);
137
+ const acc = {};
138
+ for (const e of reg[name].ext) Object.assign(acc, resolveFields(e, reg, seen));
139
+ Object.assign(acc, reg[name].fields);
140
+ return acc;
141
+ }
142
+
143
+ // ── method extraction ───────────────────────────────────────────────────────
144
+
145
+ function extractMethods() {
146
+ const methods = [];
147
+ for (const file of listFiles(WSAPI_DIR, '.ts')) {
148
+ const base = path.basename(file);
149
+ if (base === 'WSAPITypes.ts' || base === 'WSAPIBase.ts') continue;
150
+ const domain = base.replace(/^WSAPI/, '').replace(/\.ts$/, '').toLowerCase();
151
+ const sf = parse(file);
152
+ const visit = (node) => {
153
+ if (ts.isMethodDeclaration(node) && node.name) {
154
+ const isPublic = !node.modifiers?.some(m =>
155
+ m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
156
+ const name = node.name.getText(sf);
157
+ if (isPublic && !SKIP_METHODS.has(name) && !name.startsWith('_')) methods.push({ name, domain, node, sf });
158
+ }
159
+ ts.forEachChild(node, visit);
160
+ };
161
+ visit(sf);
162
+ }
163
+ return methods;
164
+ }
165
+
166
+ function readJSDoc(node, sf) {
167
+ const docs = ts.getJSDocCommentsAndTags(node);
168
+ const out = { description: '', remarks: '', params: [], example: '', returns: '', links: [] };
169
+ for (const d of docs) {
170
+ if (ts.isJSDoc(d)) out.description = tagText(d.comment) || out.description;
171
+ for (const t of d.tags || (ts.isJSDoc(d) ? [] : [d])) {
172
+ const tag = t.tagName?.text, txt = tagText(t.comment);
173
+ if (tag === 'remarks') out.remarks += (out.remarks ? '\n\n' : '') + txt;
174
+ else if (tag === 'param') out.params.push({ name: t.name?.getText(sf) ?? '', text: txt });
175
+ else if (tag === 'example') out.example += (out.example ? '\n\n' : '') + txt;
176
+ else if (tag === 'returns') out.returns = txt;
177
+ }
178
+ }
179
+ const raw = docs.map(d => d.getText(sf)).join('\n');
180
+ let m; const re = /\{@link\s+([A-Za-z0-9_.]+)/g;
181
+ while ((m = re.exec(raw))) if (!out.links.includes(m[1])) out.links.push(m[1]);
182
+ return out;
183
+ }
184
+
185
+ // ── page assembly ───────────────────────────────────────────────────────────
186
+
187
+ function splitRemarks(remarks) {
188
+ const m = remarks.match(/\*\*Error codes[\s\S]*?(?=\n\s*\*\*[A-Z]|$)/);
189
+ return m ? { contract: remarks.replace(m[0], '').trim(), errors: m[0].trim() } : { contract: remarks, errors: '' };
190
+ }
191
+
192
+ // Recursively render a type's fields from the registry, expanding nested object /
193
+ // array field types (bounded by depth + a per-branch cycle guard) so the full
194
+ // structure is described, not collapsed.
195
+ const RT_MAX_DEPTH = 4;
196
+ function renderType(typeName, reg, seen, depth) {
197
+ const out = [];
198
+ const pad = ' '.repeat(depth);
199
+ for (const [k, info] of Object.entries(resolveFields(typeName, reg))) {
200
+ const tt = info.typeText || '';
201
+ out.push(`${pad}- \`${k}\` (${tt})${info.comment ? ` — ${info.comment}` : ''}`);
202
+ const base = baseTypeName(tt);
203
+ if (base && reg[base] && !seen.has(base) && depth < RT_MAX_DEPTH) {
204
+ out.push(...renderType(base, reg, new Set([...seen, base]), depth + 1));
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+
210
+ // The full RETURN TYPE definition — unwraps Promise<…> and GamesApiResponse<…> so
211
+ // the page describes the actual payload type (e.g. GamePickGameInfo), not just the
212
+ // wrapper, with nested types expanded.
213
+ function returnsSection(ret, reg) {
214
+ if (!ret || ret === 'void') return '_No return value._';
215
+ let inner = ret.replace(/^Promise\s*<([\s\S]*)>\s*$/, '$1').trim();
216
+ let prefix = '';
217
+ const gm = inner.match(/^GamesApiResponse\s*<([\s\S]*)>\s*$/);
218
+ if (gm) {
219
+ prefix = 'Wrapped in `GamesApiResponse`: `errCode` (number — `0` = success), `errMessage?` (string), `data?` — the payload:\n\n';
220
+ inner = gm[1].trim();
221
+ }
222
+ const isArr = /\[\]\s*$/.test(inner) || /^Array\s*</.test(inner);
223
+ const base = baseTypeName(inner);
224
+ if (!base || !reg[base]) return prefix + `Resolves to \`${inner}\`.`;
225
+ const lines = [prefix + (isArr ? `Array of \`${base}\`. Each item:` : `\`${base}\`:`)];
226
+ lines.push(...renderType(base, reg, new Set([base]), 0));
227
+ return lines.join('\n');
228
+ }
229
+
230
+ // Renamed / shorthand / guessed names the IDE agent actually searched (from a
231
+ // 48h live-session analysis) → added to the page's Search-terms so the search
232
+ // lands first-shot. doSAWAcknowledge (8 hits) was the biggest single miss.
233
+ const ALIASES = {
234
+ miniGameWinAcknowledgeRequest: ['doSAWAcknowledge', 'SAWAcknowledge', 'acknowledge', 'SAWAcknowledgeType'],
235
+ getTournamentsList: ['getTournaments'],
236
+ getTournamentInstanceInfo: ['getTournament'],
237
+ getLeaderBoard: ['getLeaderboard', 'leaderboard'],
238
+ getLeaderBoards: ['getLeaderboards'],
239
+ getAvatarsList: ['getAvatars'],
240
+ getMiniGames: ['getSAW', 'spins', 'minigames', 'wheel', 'SAWPrizeType'],
241
+ playMiniGame: ['doSAWSpin', 'SAWSpin', 'spin'],
242
+ playMiniGameBatch: ['doSAWSpinBatch'],
243
+ getMiniGamesHistory: ['getSAWHistory'],
244
+ getCurrentLevel: ['getUserLevel'],
245
+ getLevels: ['getUserLevels'],
246
+ getMissions: ['getUserMissions', 'achievements'],
247
+ getBadges: ['getUserBadges'],
248
+ getUserProfile: ['getPublicProps', 'getProfile', 'balance', 'points'],
249
+ };
250
+
251
+ /** Enum / sub-type names referenced by a return type's fields (1 level deep) —
252
+ * so a search by a cross-cutting enum name (SAWAcknowledgeType, PointChangeSourceType,
253
+ * GamePickResolutionType, …) lands on the page that owns it. */
254
+ function collectFieldTypeNames(typeName, reg, seen = new Set(), depth = 0) {
255
+ const out = new Set();
256
+ if (!typeName || depth > 1) return out;
257
+ for (const info of Object.values(resolveFields(typeName, reg))) {
258
+ for (const n of (info.typeText || '').match(/[A-Z][A-Za-z0-9_]+/g) || []) {
259
+ if (n.length < 4 || ['Array', 'Record', 'Date', 'Partial', 'Promise', 'This', 'The', 'URL', 'JSON'].includes(n)) continue;
260
+ out.add(n);
261
+ if (reg[n] && !seen.has(n)) { seen.add(n); for (const x of collectFieldTypeNames(n, reg, seen, depth + 1)) out.add(x); }
262
+ }
263
+ }
264
+ return out;
265
+ }
266
+
267
+ function buildPage(method, reg) {
268
+ const { name, domain, node, sf } = method;
269
+ const doc = readJSDoc(node, sf);
270
+ const params = node.parameters.map(p => p.getText(sf)).join(', ');
271
+ const ret = node.type ? node.type.getText(sf) : 'void';
272
+ const typeNames = [...new Set((ret.match(/[A-Z][A-Za-z0-9_]+/g) || [])
273
+ .filter(n => !['Promise', 'Partial', 'Array', 'Record', 'GamesApiResponse'].includes(n)))];
274
+ const typeName = typeNames[0] || '';
275
+ const summary = (doc.description.split(/(?<=[.!?])\s/)[0] || doc.description || name).replace(/\s+/g, ' ').trim();
276
+
277
+ const respPath = path.join(RESP_DIR, `${name}.json`);
278
+ const respJson = fs.existsSync(respPath) ? JSON.parse(fs.readFileSync(respPath, 'utf8')) : null;
279
+ const { contract, errors } = splitRemarks(doc.remarks);
280
+
281
+ const sampleKeys = respJson ? Object.keys((Array.isArray(respJson) ? respJson[0] : respJson) || {}).slice(0, 8) : [];
282
+ const onUpd = /\bonUpdate\b/.test(params) ? ['onUpdate', 'subscription'] : [];
283
+ const fieldTypes = [...collectFieldTypeNames(typeName, reg)].slice(0, 12);
284
+ const searchTerms = [name, domain, ...(ALIASES[name] || []), ...typeNames, ...fieldTypes, ...onUpd, ...sampleKeys]
285
+ .filter((v, i, a) => v && a.indexOf(v) === i).join(', ');
286
+
287
+ const o = [];
288
+ o.push(`# ${name} — API${typeName ? ` (${typeName})` : ''}`, '');
289
+ o.push(`> ${summary}`);
290
+ o.push(`> Import: \`import { ${typeNames.join(', ') || '/* types */'} } from '@smartico/public-api'\``);
291
+ o.push(`> Search terms: ${searchTerms}`, '');
292
+ o.push('## Signature', '```ts', `_smartico.api.${name}(${params}): ${ret}`, '```', '');
293
+ o.push('## Parameters', doc.params.length
294
+ ? doc.params.map(p => `- ${p.name ? `\`${p.name}\` — ` : ''}${p.text}`.replace(/\s+/g, ' ').trim()).join('\n')
295
+ : '_None._', '');
296
+ o.push(`## Returns${typeName ? ` — \`${ret}\`` : ''}`, returnsSection(ret, reg), '');
297
+ o.push('## Behavioral contract', contract || `See the [UI guide](../ui/${domain}/UIGuide_${name}.md) if present.`, '');
298
+ if (doc.example) o.push('## Example', doc.example.trim(), '');
299
+ if (respJson) o.push('### Example response (REAL shape)',
300
+ '> Where this real payload differs from the typed Returns above (TS interface vs raw wire), the REAL shape is the runtime truth.',
301
+ '```json',
302
+ JSON.stringify(Array.isArray(respJson) ? [trimVal(respJson[0])] : trimVal(respJson), null, 2), '```', '');
303
+ o.push('## Errors', errors || "See this method's TSDoc / the mutation pages for `err_code` handling.", '');
304
+ if (doc.links.length) o.push('## Related', doc.links.filter(l => l !== name).map(l => `- \`${l}\``).join('\n'), '');
305
+ return o.join('\n');
306
+ }
307
+
308
+ // ── global `_smartico.*` surface + callbacks (non-.api.* — the live-session
309
+ // analysis showed these were unreachable in the `api` lane and live-probed) ──
310
+
311
+ function buildGlobalSurfacePage() {
312
+ const file = path.join(SRC, 'SmarticoGlobal.ts');
313
+ if (!fs.existsSync(file)) return null;
314
+ const sf = parse(file);
315
+ let iface = null;
316
+ const find = (n) => { if (ts.isInterfaceDeclaration(n) && n.name.text === 'SmarticoGlobal') iface = n; ts.forEachChild(n, find); };
317
+ find(sf);
318
+ if (!iface) return null;
319
+ const members = iface.members.filter(m => m.name).map(m => ({
320
+ name: m.name.getText(sf),
321
+ sig: m.getText(sf).replace(/;$/, '').replace(/\s+/g, ' ').trim(),
322
+ one: ts.getJSDocCommentsAndTags(m).map(d => tagText(d.comment)).filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(),
323
+ }));
324
+ const extra = ['_smartico', 'global', 'window._smartico', 'SmarticoGlobal', 'visitor mode', 'callbacks', 'events',
325
+ 'onWin', 'onReady', 'deep link', 'getWidgetParams', 'IWidgetParams', 'widget params', 'readiness', 'lifecycle', 'push'];
326
+ const terms = [...new Set([...members.map(m => m.name), ...extra])].join(', ');
327
+ const o = [];
328
+ o.push('# _smartico — global SDK surface (non-`.api.*`)', '');
329
+ o.push('> The `window._smartico` control surface: init/identify, events, deep links, widgets, visitor-mode `vapi`, push, and utilities — everything that is NOT `_smartico.api.*` (those have their own capability pages).');
330
+ o.push("> Import: typed via `SmarticoGlobal` from '@smartico/public-api'; at runtime the object is on `window._smartico` (no import).");
331
+ o.push(`> Search terms: ${terms}`, '');
332
+ o.push('## Readiness gate', 'Call `window._smartico?.checkSuccessfullyIdentify()` (synchronous) before any `_smartico.api.*` — `true` once init + identify completed. Or subscribe `_smartico.on(\'identify\', cb)` (see the callbacks page).', '');
333
+ o.push('## Methods');
334
+ for (const m of members) o.push(`- \`_smartico.${m.sig}\`${m.one ? ` — ${m.one}` : ''}`);
335
+ o.push('');
336
+ o.push('## Visitor mode (`vapi`)', '`_smartico.vapi(language)` returns a `WSAPI` scoped to anonymous visitors — same method shapes as `_smartico.api.*`, for unidentified users (pre-login surfaces).', '');
337
+ o.push('## Reading widget params', 'A widget embedded via `showWidget` / `miniGame` receives `IWidgetParams` (theme, height, inline, …). Read them from the embedding snippet / `window.__SMARTICO_LP_PARAMS__`, not from an API call.', '');
338
+ o.push('## Related', '- `_smartico.on — events & callbacks` (the callbacks capability page)', '- Full prose reference: the SmarticoObject doc (search source `api-raw`).');
339
+ return o.join('\n');
340
+ }
341
+
342
+ function buildConceptPage(srcRel, title, terms) {
343
+ const p = path.join(ROOT, srcRel);
344
+ if (!fs.existsSync(p)) return null;
345
+ const body = fs.readFileSync(p, 'utf8').replace(/^#\s+.*\r?\n/, '').trim();
346
+ return [`# ${title}`, '', `> ${title}.`, `> Search terms: ${terms}`, '', body].join('\n');
347
+ }
348
+
349
+ function main() {
350
+ if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true });
351
+ const reg = buildTypeRegistry();
352
+ const methods = extractMethods();
353
+ let written = 0, withResp = 0;
354
+ for (const method of methods) {
355
+ fs.writeFileSync(path.join(OUT_DIR, `${method.name}.md`), buildPage(method, reg));
356
+ written++;
357
+ if (fs.existsSync(path.join(RESP_DIR, `${method.name}.json`))) withResp++;
358
+ }
359
+ let extra = 0;
360
+ const g = buildGlobalSurfacePage();
361
+ if (g) { fs.writeFileSync(path.join(OUT_DIR, 'smarticoGlobal.md'), g); extra++; }
362
+ const cb = buildConceptPage('docs/Callbacks.md', '_smartico.on — events & callbacks',
363
+ 'on, off, callbacks, events, subscribe, subscribeUpdate, onWin, onReady, props_change, identify, EXTERNAL_CALLBACK, lifecycle, _smartico.on');
364
+ if (cb) { fs.writeFileSync(path.join(OUT_DIR, 'smarticoCallbacks.md'), cb); extra++; }
365
+ console.log(`[gen-capabilities] wrote ${written} method pages + ${extra} global/concept pages (${withResp} with captured response); type registry: ${Object.keys(reg).length} interfaces`);
366
+ }
367
+
368
+ main();
@@ -0,0 +1,105 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * run-public-api.mjs — run arbitrary public-API code as a REAL identified user, headless.
4
+ *
5
+ * A reusable test harness for verifying SDK behaviour against a live env (e.g.
6
+ * a new server field that's only deployed on one env). It loads the real
7
+ * smartico.js tracker bundle, init + identify as the given player, then evaluates
8
+ * SM_CODE inside the page against `window._smartico` and prints the JSON result.
9
+ *
10
+ * Unlike the server-side `run_public_api` MCP tool (which runs the PUBLISHED SDK
11
+ * in visitor mode), this runs the real browser SDK as an identified user, and can
12
+ * reach the raw protocol via `_smartico.api.api.*` — so it surfaces server fields
13
+ * the published transform/types don't yet map (e.g. SAW spin-expiration).
14
+ *
15
+ * Setup (one-time): npm i -D playwright && npx playwright install chromium
16
+ *
17
+ * Run the default test (SAW spin-expiration on env8 DEMO):
18
+ * node scripts/run-public-api.mjs
19
+ *
20
+ * Run your own snippet (async-function body; `_smartico` + `window._smartico_user_id` in scope):
21
+ * SM_CODE='return await _smartico.api.getMissions();' node scripts/run-public-api.mjs
22
+ * SM_CODE="$(cat my-test.js)" node scripts/run-public-api.mjs
23
+ *
24
+ * Point at another env/label/user:
25
+ * SM_LABEL=<label-key ending in env digit> SM_BRAND=<hex> SM_USER=<ext id> \
26
+ * SM_SALT=<label salt | 'null'> node scripts/run-public-api.mjs
27
+ */
28
+ import { chromium } from 'playwright';
29
+ import crypto from 'crypto';
30
+
31
+ // Defaults: env8 DEMO label (SpaceX brand) with a test player that HAS expirable
32
+ // SAW spins — running bare validates SMR-48946 (mini-game spin expiration).
33
+ const LABEL = process.env.SM_LABEL || 'd78934f3-1d8b-461b-9e8f-287aae43c860-8';
34
+ const BRAND = process.env.SM_BRAND || 'c24a284e';
35
+ const USER = process.env.SM_USER || 'test98681514';
36
+ const SALT = process.env.SM_SALT || 'null';
37
+ const ORIGIN = process.env.SM_ORIGIN || 'https://app.smartico.ai/';
38
+ const SCRIPT_URL = process.env.SM_SCRIPT_URL || 'https://libs.smartico.ai/smartico.js';
39
+
40
+ // Default snippet: dump every SAW template that has spin-expiration set for this
41
+ // user, via the RAW command-700 response (the published transform drops the fields).
42
+ const DEFAULT_CODE = `
43
+ const ext = window._smartico_user_id;
44
+ const resp = await window._smartico.api.api.sawGetTemplates(ext);
45
+ const list = (resp && resp.templates) || [];
46
+ return {
47
+ user_ext_id: ext,
48
+ total_templates: list.length,
49
+ templates_with_spin_expiration: list
50
+ .filter(t => t.earliest_expiration_dt != null || t.latest_expiration_dt != null)
51
+ .map(t => ({ saw_template_id: t.saw_template_id, spin_count: t.spin_count,
52
+ earliest_expiration_dt: t.earliest_expiration_dt, latest_expiration_dt: t.latest_expiration_dt })),
53
+ };
54
+ `;
55
+ const CODE = process.env.SM_CODE || DEFAULT_CODE;
56
+
57
+ function userHash(user, salt) {
58
+ const ts = Math.floor(Date.now() / 1000) * 1000 + 24 * 3600 * 1000;
59
+ return crypto.createHash('md5').update(`${user.toLowerCase()}:${salt}:${ts}`.toLowerCase()).digest('hex') + ':' + ts;
60
+ }
61
+
62
+ (async () => {
63
+ const hash = userHash(USER, SALT);
64
+ const browser = await chromium.launch({ headless: true });
65
+ const ctx = await browser.newContext({
66
+ userAgent: process.env.SM_UA || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
67
+ viewport: { width: 1280, height: 800 }, locale: 'en-US',
68
+ });
69
+ const page = await ctx.newPage();
70
+ page.on('console', m => { if (m.type() === 'error') console.error(' [page error]', m.text().slice(0, 200)); });
71
+
72
+ // Self-served CSP-free origin (libs.smartico.ai 403s headless + CSP-blocks injection).
73
+ await page.route(ORIGIN + '**', r => r.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><html><head></head><body></body></html>' }));
74
+ await page.goto(ORIGIN, { waitUntil: 'domcontentloaded' }).catch(() => {});
75
+
76
+ await page.evaluate((u) => new Promise((res) => {
77
+ const s = document.createElement('script'); s.src = u; s.onload = () => res('loaded'); s.onerror = () => res('error'); document.head.appendChild(s);
78
+ }), SCRIPT_URL);
79
+ await page.waitForFunction(() => typeof window._smartico !== 'undefined', { timeout: 15000 });
80
+
81
+ await page.evaluate(({ LABEL, BRAND, USER, hash }) => {
82
+ window._smartico_user_id = USER;
83
+ window._smartico.init(LABEL, { brand_key: BRAND });
84
+ window._smartico.identify(USER, hash, {});
85
+ }, { LABEL, BRAND, USER, hash });
86
+
87
+ const ok = await page.waitForFunction(() => window._smartico?.checkSuccessfullyIdentify?.() === true, { timeout: 40000 }).then(() => true).catch(() => false);
88
+ if (!ok) { console.error(`[run] identify failed for ${USER} on ${LABEL} (check SM_SALT / allowed origin).`); await browser.close(); process.exit(3); }
89
+ console.error(`[run] identified ${USER} on ${LABEL} ✓`);
90
+
91
+ // Execute the operator-supplied snippet against window._smartico. SM_CODE is
92
+ // TRUSTED — the same trust model as `node -e`, `npx`, or the run_public_api MCP
93
+ // tool: it is the test author's own code, not external/untrusted input. There is
94
+ // no privilege boundary here (a "malicious SM_CODE" is the operator attacking
95
+ // their own dev machine). Do NOT feed untrusted/remote strings into SM_CODE.
96
+ const result = await page.evaluate(async (code) => {
97
+ const _smartico = window._smartico; // in scope for the snippet
98
+ try { return { ok: true, value: await (new Function('_smartico', `return (async () => { ${code} })()`))(_smartico) }; }
99
+ catch (e) { return { ok: false, error: String(e && e.message || e) }; }
100
+ }, CODE);
101
+
102
+ console.log(JSON.stringify(result.ok ? result.value : { ERROR: result.error }, null, 2));
103
+ await browser.close();
104
+ process.exit(result.ok ? 0 : 1);
105
+ })().catch(e => { console.error('[run] fatal:', e.message); process.exit(1); });
@@ -44,6 +44,8 @@ export const SAWTemplatesTransform = (items: SAWTemplate[]): TMiniGameTemplate[]
44
44
  activeFromDate: r.activeFromDate,
45
45
  activeTillDate: r.activeTillDate,
46
46
  next_available_spin_ts: r.next_available_spin_ts,
47
+ earliest_expiration_dt: r.earliest_expiration_dt,
48
+ latest_expiration_dt: r.latest_expiration_dt,
47
49
  steps_to_finish_game: r.saw_template_ui_definition.steps_to_finish_game,
48
50
  custom_section_id: r.saw_template_ui_definition.custom_section_id,
49
51
  saw_template_ui_definition: r.saw_template_ui_definition,
@@ -31,6 +31,11 @@ export interface SAWTemplate {
31
31
 
32
32
  next_available_spin_ts?: number;
33
33
 
34
+ /** Soonest-expiring spin's expiration time for this user (epoch ms); `null`/absent when no expirable spins. */
35
+ earliest_expiration_dt?: number | null;
36
+ /** Latest-expiring spin's expiration time for this user (epoch ms); `null`/absent when no expirable spins. */
37
+ latest_expiration_dt?: number | null;
38
+
34
39
  saw_skin_key: string;
35
40
  saw_skin_ui_definition: {
36
41
  skin_folder: string;
package/src/NodeCache.ts CHANGED
@@ -39,6 +39,14 @@ class NodeCache {
39
39
  }
40
40
  }
41
41
 
42
+ public removeByPrefix(prefix: string) {
43
+ for (const key in this.cache) {
44
+ if (this.cache.hasOwnProperty(key) && key.indexOf(prefix) === 0) {
45
+ delete this.cache[key];
46
+ }
47
+ }
48
+ }
49
+
42
50
  public flushAll() {
43
51
  this.cache = {};
44
52
  if (this.ttlChecker) {
package/src/OCache.ts CHANGED
@@ -56,6 +56,23 @@ export class OCache {
56
56
  }
57
57
  }
58
58
 
59
+ /**
60
+ * Remove every entry whose string key starts with `oKeyBase` — the bare key
61
+ * AND all per-parameter composite variants (`${oKeyBase}:…` / `${oKeyBase}<id>`).
62
+ * Use when a cache is keyed per parameter (paginated, per-template, per-window):
63
+ * an exact-key `clear(oKeyBase)` would miss every variant. `oKeyBase` must be a string.
64
+ */
65
+ public static async clearByPrefix(cacheContext: ECacheContext, oKeyBase: string) {
66
+ // `set`/`get` build keys as `<context>_<JSON.stringify(oKey)>`; JSON.stringify
67
+ // wraps a string in quotes, so dropping the trailing quote yields a prefix that
68
+ // matches both the bare `"base"` key and every `"base…"` composite key.
69
+ const prefix = cacheContext.toString() + '_' + JSON.stringify(oKeyBase).slice(0, -1);
70
+
71
+ if (this.cache[cacheContext]) {
72
+ this.cache[cacheContext].removeByPrefix(prefix);
73
+ }
74
+ }
75
+
59
76
  public static async clearContext(cacheContext: ECacheContext) {
60
77
  if (this.cache[cacheContext]) {
61
78
  this.cache[cacheContext].flushAll();
@@ -192,13 +192,6 @@ export interface SmarticoGlobal {
192
192
 
193
193
  declare global {
194
194
  interface Window {
195
- /**
196
- * Params injected by the Smartico widget runtime (`widget.js`) when a widget
197
- * is embedded via `_smartico.showWidget` / a widget script tag. Keys mirror
198
- * the params passed in the embedding snippet (e.g. `id`).
199
- */
200
- __SMARTICO_LP_PARAMS__?: Record<string, string | number>;
201
-
202
195
  /**
203
196
  * The global Smartico SDK object, present once the embed snippet has loaded.
204
197
  * Optional because it is `undefined` until the script initializes — guard
@@ -34,11 +34,11 @@ export class WSAPI extends WSAPIInbox {
34
34
  on(ClassId.SAW_SHOW_SPIN_PUSH, () => this.reloadMiniGameTemplate());
35
35
  on(ClassId.SAW_AKNOWLEDGE_RESPONSE, () => {
36
36
  this.reloadMiniGameTemplate();
37
- OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
37
+ OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
38
38
  });
39
39
  on(ClassId.SAW_DO_SPIN_RESPONSE, () => {
40
40
  this.reloadMiniGameTemplate();
41
- OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
41
+ OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
42
42
  });
43
43
  on(ClassId.MISSION_OPTIN_RESPONSE, () => this.updateMissions());
44
44
  on(ClassId.ACHIEVEMENT_CLAIM_PRIZE_RESPONSE, () => this.updateMissions());
@@ -57,7 +57,7 @@ export class WSAPI extends WSAPIInbox {
57
57
  on(ClassId.CLAIM_BONUS_RESPONSE, () => this.updateBonuses());
58
58
  on(ClassId.SAW_DO_SPIN_BATCH_RESPONSE, () => {
59
59
  this.reloadMiniGameTemplate();
60
- OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
60
+ OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
61
61
  });
62
62
  on(ClassId.RAF_CLAIM_PRIZE_RESPONSE, () => this.updateRaffles());
63
63
  on(ClassId.GET_INBOX_MESSAGES_RESPONSE, (res) => {
@@ -25,7 +25,10 @@ export class WSAPIJackpots extends WSAPIClans {
25
25
  protected async jackpotClearCache() {
26
26
  OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.Jackpots);
27
27
  OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.Pots);
28
- OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.JackpotWinners);
28
+ // Winners + eligible-games are cached per jp_template_id (composite keys),
29
+ // so a bare clear misses every variant — clear by prefix.
30
+ OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.JackpotWinners);
31
+ OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.JackpotEligibleGames);
29
32
  }
30
33
 
31
34
  /**
@@ -198,7 +198,7 @@ export class WSAPIMiniGames extends WSAPIRaffles {
198
198
  saw_template_id?: number;
199
199
  }): Promise<TSawHistory[]> {
200
200
  return OCache.use(
201
- onUpdateContextKey.SAWHistory,
201
+ `${onUpdateContextKey.SAWHistory}:${saw_template_id ?? ''}:${limit ?? ''}:${offset ?? ''}`,
202
202
  ECacheContext.WSAPI,
203
203
  () => this.api.getSawWinningHistoryT(this.userExtId, limit, offset, saw_template_id),
204
204
  CACHE_DATA_SEC,
@@ -171,6 +171,16 @@ export interface TMiniGameTemplate {
171
171
  * */
172
172
  next_available_spin_ts: number;
173
173
 
174
+ /** Soonest-expiring spin's expiration time for the current user, as an epoch-ms timestamp.
175
+ * `null` when the user has no expirable spins for this template — spins only expire when the
176
+ * template defines a spin-expiration rule (Wheel of Fortune, Loot Boxes, etc.). Pair with
177
+ * `latest_expiration_dt` to render a "spins expire between X and Y" window. */
178
+ earliest_expiration_dt?: number | null;
179
+ /** Latest-expiring spin's expiration time for the current user, as an epoch-ms timestamp.
180
+ * `null` when the user has no expirable spins; equals `earliest_expiration_dt` when a single
181
+ * expiration applies. */
182
+ latest_expiration_dt?: number | null;
183
+
174
184
  /** The message that should be shown to the user when he cannot play the game, server rejected attempt with error code SAWSpinErrorCode.SAW_FAILED_MAX_SPINS_REACHED */
175
185
  over_limit_message: string;
176
186
  /** The message that should be shown to the user when he cannot play the game because he doesn't have spin attempts or points. */
@@ -261,6 +271,40 @@ export interface TUserProfile {
261
271
  /** AI-recommended casino bet amount for this user. Undefined when no
262
272
  * recommendation is currently available. */
263
273
  core_recommended_casino_bet_amount?: number;
274
+ /** Display name of the user's current level (e.g. `"Silver"`); resolve the id via `getCurrentLevel()`. */
275
+ ach_level_current?: string;
276
+ /** `true` when the user is in the gamification A/B control group (gamification UI suppressed). */
277
+ ach_gamification_in_control_group?: boolean;
278
+ /** Smartico-internal numeric user id. */
279
+ user_id?: number;
280
+ /** ISO country code of the user (e.g. `"BG"`). */
281
+ user_country?: string;
282
+ /** Wallet currency code (e.g. `"EUR"`). */
283
+ core_wallet_currency?: string;
284
+ /** Registration timestamp (epoch ms); `0` when unknown. */
285
+ core_registration_date?: number;
286
+ /** Last-session browser push-permission state (e.g. `"BLOCKED"`, `"GRANTED"`). */
287
+ user_last_session_push_state?: string;
288
+ /** `true` when the account is flagged as a bonus abuser. */
289
+ acc_bonus_abuser?: boolean;
290
+ /** Selected avatar id (catalogue avatar or AI-variant base). */
291
+ avatar_id?: string;
292
+ /** `avatar_real_id` of the selected avatar; `0` when none. */
293
+ avatar_real_id?: number;
294
+ /** `avatar_real_id` of the user's core avatar; null when unset. */
295
+ core_avatar_real_id?: number | null;
296
+ /** Current clan id (string); empty/null when not in a clan. */
297
+ core_clan_id?: string;
298
+ /** `true` when the user was kicked from their clan; null when not applicable. */
299
+ core_clan_is_kicked?: boolean | null;
300
+ /** Id of the clan the user was kicked from; null when not applicable. */
301
+ core_clan_kicked_out_id?: number | null;
302
+ /** ext_user_id of the friend who referred this user; null when none. */
303
+ aff_referred_by_friend_ext_user_id?: string | null;
304
+ /** Refer-a-friend share URL; null when the feature is disabled. */
305
+ aff_refer_friend_url?: string | null;
306
+ /** Count of friends this user has referred. */
307
+ aff_refered_friends_count?: number;
264
308
  }
265
309
 
266
310
  /**