@kaikybrofc/omnizap-system 2.2.4 → 2.2.6

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 (55) hide show
  1. package/.env.example +5 -0
  2. package/.prettierrc +16 -0
  3. package/README.md +13 -13
  4. package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
  5. package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
  6. package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
  7. package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
  8. package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
  9. package/app/modules/stickerPackModule/catalogRouter.js +79 -0
  10. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
  11. package/app/modules/stickerPackModule/domainEvents.js +61 -0
  12. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
  13. package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
  14. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
  15. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
  16. package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
  17. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
  18. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  19. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +570 -536
  20. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
  21. package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
  22. package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
  23. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
  24. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
  25. package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
  26. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
  27. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
  28. package/app/observability/metrics.js +169 -0
  29. package/app/services/featureFlagService.js +137 -0
  30. package/database/index.js +5 -0
  31. package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
  32. package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
  33. package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
  34. package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
  35. package/database/migrations/20260228_0026_feature_flags.sql +21 -0
  36. package/ecosystem.prod.config.cjs +70 -9
  37. package/index.js +26 -0
  38. package/kaikybrofc-omnizap-system-2.2.6.tgz +0 -0
  39. package/observability/sticker-catalog-slo.md +83 -0
  40. package/observability/sticker-scale-hardening-rollout.md +128 -0
  41. package/package.json +7 -35
  42. package/public/assets/images/brand-icon-192.png +0 -0
  43. package/public/assets/images/brand-logo-128.webp +0 -0
  44. package/public/assets/images/hero-banner-1280.avif +0 -0
  45. package/public/assets/images/hero-banner-1280.jpg +0 -0
  46. package/public/assets/images/hero-banner-1280.webp +0 -0
  47. package/public/assets/images/hero-banner-720.avif +0 -0
  48. package/public/assets/images/hero-banner-720.webp +0 -0
  49. package/public/index.html +120 -18
  50. package/public/js/apps/homeApp.js +469 -353
  51. package/public/robots.txt +9 -0
  52. package/public/sitemap.xml +28 -0
  53. package/scripts/sticker-catalog-loadtest.mjs +208 -0
  54. package/scripts/sticker-worker-task.mjs +122 -0
  55. package/observability/mysql-exporter.cnf +0 -5
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
4
+ import { URL, URLSearchParams } from 'node:url';
4
5
  import axios from 'axios';
5
6
 
6
7
  import { executeQuery, pool, TABLES } from '../../../database/index.js';
@@ -64,6 +65,8 @@ import {
64
65
  buildViewerTagAffinity,
65
66
  computePackSignals,
66
67
  } from './stickerPackMarketplaceService.js';
68
+ import { listStickerPackScoreSnapshotsByPackIds } from './stickerPackScoreSnapshotRepository.js';
69
+ import { createCatalogApiRouter } from './catalogRouter.js';
67
70
  import {
68
71
  buildAdminMenu,
69
72
  buildAiMenu,
@@ -75,11 +78,17 @@ import {
75
78
  buildStickerMenu,
76
79
  } from '../menuModule/common.js';
77
80
  import { getMarketplaceDriftSnapshot } from './stickerMarketplaceDriftService.js';
78
- import { getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from './stickerStorageService.js';
81
+ import {
82
+ getStickerAssetExternalUrl,
83
+ getStickerStorageConfig,
84
+ readStickerAssetBuffer,
85
+ saveStickerAssetFromBuffer,
86
+ } from './stickerStorageService.js';
79
87
  import { convertToWebp } from '../stickerModule/convertToWebp.js';
80
88
  import { sanitizeText } from './stickerPackUtils.js';
81
89
  import stickerPackService from './stickerPackServiceRuntime.js';
82
90
  import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
91
+ import { isFeatureEnabled } from '../../services/featureFlagService.js';
83
92
 
84
93
  const parseEnvBool = (value, fallback) => {
85
94
  if (value === undefined || value === null || value === '') return fallback;
@@ -148,6 +157,7 @@ const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_P
148
157
  const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
149
158
  const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
150
159
  const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
160
+ const STICKER_WEB_ASSET_VERSION = sanitizeText(process.env.STICKER_WEB_ASSET_VERSION || '', 64, { allowEmpty: true }) || '';
151
161
  const CATALOG_PUBLIC_DIR = path.resolve(process.cwd(), 'public');
152
162
  const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'index.html');
153
163
  const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'create', 'index.html');
@@ -162,7 +172,19 @@ const MAX_ORPHAN_LIST_LIMIT = clampInt(process.env.STICKER_ORPHAN_LIST_MAX_LIMIT
162
172
  const DEFAULT_DATA_LIST_LIMIT = clampInt(process.env.STICKER_DATA_LIST_LIMIT, 50, 1, 200);
163
173
  const MAX_DATA_LIST_LIMIT = clampInt(process.env.STICKER_DATA_LIST_MAX_LIMIT, 200, 1, 500);
164
174
  const MAX_DATA_SCAN_FILES = clampInt(process.env.STICKER_DATA_SCAN_MAX_FILES, 10000, 100, 50000);
165
- const ASSET_CACHE_SECONDS = clampInt(process.env.STICKER_WEB_ASSET_CACHE_SECONDS, 60 * 10, 0, 60 * 60 * 24 * 7);
175
+ const ASSET_CACHE_SECONDS = clampInt(
176
+ process.env.STICKER_WEB_ASSET_CACHE_SECONDS,
177
+ 60 * 60 * 24 * 30,
178
+ 60 * 60,
179
+ 60 * 60 * 24 * 365,
180
+ );
181
+ const STATIC_TEXT_CACHE_SECONDS = clampInt(process.env.STICKER_WEB_STATIC_TEXT_CACHE_SECONDS, 60 * 60, 60, 60 * 60 * 24 * 30);
182
+ const IMMUTABLE_ASSET_CACHE_SECONDS = clampInt(
183
+ process.env.STICKER_WEB_IMMUTABLE_ASSET_CACHE_SECONDS,
184
+ 60 * 60 * 24 * 365,
185
+ 60 * 60,
186
+ 60 * 60 * 24 * 365,
187
+ );
166
188
  const STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE =
167
189
  String(process.env.STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE || '/pack send {{pack_key}}').trim() ||
168
190
  '/pack send {{pack_key}}';
@@ -203,6 +225,19 @@ const GITHUB_REPOSITORY = String(process.env.GITHUB_REPOSITORY || 'Kaikygr/omniz
203
225
  const GITHUB_TOKEN = String(process.env.GITHUB_TOKEN || '').trim();
204
226
  const GITHUB_PROJECT_CACHE_SECONDS = clampInt(process.env.GITHUB_PROJECT_CACHE_SECONDS, 300, 30, 3600);
205
227
  const GLOBAL_RANK_REFRESH_SECONDS = clampInt(process.env.GLOBAL_RANK_REFRESH_SECONDS, 600, 60, 3600);
228
+ const CATALOG_LIST_CACHE_SECONDS = clampInt(process.env.STICKER_CATALOG_LIST_CACHE_SECONDS, 90, 15, 900);
229
+ const CATALOG_CREATOR_RANKING_CACHE_SECONDS = clampInt(
230
+ process.env.STICKER_CATALOG_CREATOR_RANKING_CACHE_SECONDS,
231
+ 120,
232
+ 15,
233
+ 900,
234
+ );
235
+ const CATALOG_PACK_PAYLOAD_CACHE_SECONDS = clampInt(
236
+ process.env.STICKER_CATALOG_PACK_PAYLOAD_CACHE_SECONDS,
237
+ 300,
238
+ 30,
239
+ 1800,
240
+ );
206
241
  const MARKETPLACE_GLOBAL_STATS_API_PATH = '/api/marketplace/stats';
207
242
  const MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS = clampInt(process.env.MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS, 45, 30, 60);
208
243
  const HOME_MARKETPLACE_STATS_CACHE_SECONDS = clampInt(process.env.HOME_MARKETPLACE_STATS_CACHE_SECONDS, 45, 10, 300);
@@ -218,6 +253,14 @@ const SITE_CANONICAL_REDIRECT_ENABLED = parseEnvBool(process.env.SITE_CANONICAL_
218
253
  const SITE_ORIGIN = String(process.env.SITE_ORIGIN || `${SITE_CANONICAL_SCHEME}://${SITE_CANONICAL_HOST}`)
219
254
  .trim()
220
255
  .replace(/\/+$/, '');
256
+ const SITE_COOKIE_DOMAIN = String(process.env.SITE_COOKIE_DOMAIN || SITE_CANONICAL_HOST)
257
+ .trim()
258
+ .toLowerCase()
259
+ .replace(/^https?:\/\//, '')
260
+ .split('/')[0]
261
+ .split(':')[0]
262
+ .replace(/^\.+/, '')
263
+ .replace(/\.+$/, '');
221
264
  const SITEMAP_MAX_PACKS = clampInt(process.env.STICKER_SITEMAP_MAX_PACKS, 45000, 100, 50000);
222
265
  const SITEMAP_CACHE_SECONDS = clampInt(process.env.STICKER_SITEMAP_CACHE_SECONDS, 180, 30, 3600);
223
266
  const SEO_DISCOVERY_LINK_LIMIT = clampInt(process.env.STICKER_SEO_DISCOVERY_LINK_LIMIT, 60, 10, 200);
@@ -295,6 +338,9 @@ const MARKETPLACE_GLOBAL_STATS_CACHE = {
295
338
  pending: null,
296
339
  };
297
340
  const HOME_MARKETPLACE_STATS_CACHE = new Map();
341
+ const CATALOG_LIST_CACHE = new Map();
342
+ const CATALOG_CREATOR_RANKING_CACHE = new Map();
343
+ const CATALOG_PACK_PAYLOAD_CACHE = new Map();
298
344
  const SYSTEM_SUMMARY_CACHE = {
299
345
  expiresAt: 0,
300
346
  value: null,
@@ -324,6 +370,69 @@ const formatDuration = (totalSeconds) => {
324
370
  return days > 0 ? `${days}d ${hhmmss}` : hhmmss;
325
371
  };
326
372
 
373
+ const buildCacheKey = (parts) => JSON.stringify(parts);
374
+
375
+ const getCacheBucket = (cacheMap, key) => {
376
+ let bucket = cacheMap.get(key);
377
+ if (!bucket) {
378
+ bucket = {
379
+ expiresAt: 0,
380
+ value: null,
381
+ pending: null,
382
+ };
383
+ cacheMap.set(key, bucket);
384
+ }
385
+ return bucket;
386
+ };
387
+
388
+ const getCachedSnapshot = async ({
389
+ cacheMap,
390
+ key,
391
+ ttlSeconds,
392
+ staleWhileRefresh = true,
393
+ staleOnError = true,
394
+ load,
395
+ }) => {
396
+ const bucket = getCacheBucket(cacheMap, key);
397
+ const now = Date.now();
398
+ const hasValue = bucket.value !== null;
399
+ const hasFreshValue = hasValue && now < bucket.expiresAt;
400
+
401
+ if (hasFreshValue) {
402
+ return bucket.value;
403
+ }
404
+
405
+ if (!bucket.pending) {
406
+ bucket.pending = Promise.resolve()
407
+ .then(load)
408
+ .then((value) => {
409
+ bucket.value = value;
410
+ bucket.expiresAt = Date.now() + ttlSeconds * 1000;
411
+ return value;
412
+ })
413
+ .finally(() => {
414
+ bucket.pending = null;
415
+ });
416
+ }
417
+
418
+ if (hasValue && staleWhileRefresh) {
419
+ return bucket.value;
420
+ }
421
+
422
+ try {
423
+ return await bucket.pending;
424
+ } catch (error) {
425
+ if (hasValue && staleOnError) return bucket.value;
426
+ throw error;
427
+ }
428
+ };
429
+
430
+ const canUseRankingSnapshotRead = async (subjectKey = 'catalog') =>
431
+ isFeatureEnabled('enable_ranking_snapshot_read', {
432
+ fallback: true,
433
+ subjectKey,
434
+ });
435
+
327
436
  const sendJson = (req, res, statusCode, payload) => {
328
437
  const body = JSON.stringify(payload);
329
438
  res.statusCode = statusCode;
@@ -362,6 +471,22 @@ const toRequestHost = (req) =>
362
471
  .replace(/\.$/, '')
363
472
  .split(':')[0];
364
473
 
474
+ const isIpLiteralHost = (value) => {
475
+ const host = String(value || '').trim().toLowerCase();
476
+ if (!host) return false;
477
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return true;
478
+ return host.includes(':');
479
+ };
480
+
481
+ const resolveCookieDomainForRequest = (req) => {
482
+ if (!SITE_COOKIE_DOMAIN || isIpLiteralHost(SITE_COOKIE_DOMAIN)) return '';
483
+ const requestHost = toRequestHost(req);
484
+ if (!requestHost || isIpLiteralHost(requestHost) || requestHost === 'localhost') return '';
485
+ if (requestHost === SITE_COOKIE_DOMAIN) return SITE_COOKIE_DOMAIN;
486
+ if (requestHost.endsWith(`.${SITE_COOKIE_DOMAIN}`)) return SITE_COOKIE_DOMAIN;
487
+ return '';
488
+ };
489
+
365
490
  const maybeRedirectToCanonicalHost = (req, res, url) => {
366
491
  if (!SITE_CANONICAL_REDIRECT_ENABLED) return false;
367
492
  if (!['GET', 'HEAD'].includes(req.method || '')) return false;
@@ -395,6 +520,33 @@ const parseCookies = (req) => {
395
520
  }, {});
396
521
  };
397
522
 
523
+ const getCookieValuesFromRequest = (req, cookieName) => {
524
+ const target = String(cookieName || '').trim();
525
+ if (!target) return [];
526
+ const raw = String(req?.headers?.cookie || '');
527
+ if (!raw) return [];
528
+
529
+ const values = [];
530
+ for (const chunk of raw.split(';')) {
531
+ const trimmed = String(chunk || '').trim();
532
+ if (!trimmed) continue;
533
+ const separatorIndex = trimmed.indexOf('=');
534
+ if (separatorIndex <= 0) continue;
535
+ const key = trimmed.slice(0, separatorIndex).trim();
536
+ if (key !== target) continue;
537
+ const encodedValue = trimmed.slice(separatorIndex + 1).trim();
538
+ if (!encodedValue) continue;
539
+ let decodedValue = encodedValue;
540
+ try {
541
+ decodedValue = decodeURIComponent(encodedValue);
542
+ } catch {}
543
+ const normalizedValue = String(decodedValue || '').trim();
544
+ if (!normalizedValue) continue;
545
+ if (!values.includes(normalizedValue)) values.push(normalizedValue);
546
+ }
547
+ return values;
548
+ };
549
+
398
550
  const isRequestSecure = (req) => {
399
551
  const proto = String(req?.headers?.['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
400
552
  if (proto) return proto === 'https';
@@ -503,6 +655,11 @@ const runSqlTransaction = async (handler) => {
503
655
  const buildCookieString = (name, value, req, options = {}) => {
504
656
  const parts = [`${name}=${encodeURIComponent(String(value ?? ''))}`];
505
657
  parts.push(`Path=${options.path || '/'}`);
658
+ const cookieDomain =
659
+ options.domain === false
660
+ ? ''
661
+ : String(options.domain || resolveCookieDomainForRequest(req)).trim();
662
+ if (cookieDomain) parts.push(`Domain=${cookieDomain}`);
506
663
  if (options.httpOnly !== false) parts.push('HttpOnly');
507
664
  parts.push(`SameSite=${options.sameSite || 'Lax'}`);
508
665
  if (isRequestSecure(req)) parts.push('Secure');
@@ -914,11 +1071,16 @@ const pruneExpiredGoogleSessions = () => {
914
1071
  }
915
1072
  };
916
1073
 
917
- const getGoogleWebSessionTokenFromRequest = (req) => {
1074
+ const getGoogleWebSessionTokensFromRequest = (req) => {
1075
+ const direct = getCookieValuesFromRequest(req, GOOGLE_WEB_SESSION_COOKIE_NAME);
1076
+ if (direct.length > 0) return direct;
918
1077
  const cookies = parseCookies(req);
919
- return String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
1078
+ const fallback = String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
1079
+ return fallback ? [fallback] : [];
920
1080
  };
921
1081
 
1082
+ const getGoogleWebSessionTokenFromRequest = (req) => getGoogleWebSessionTokensFromRequest(req)[0] || '';
1083
+
922
1084
  const normalizeGoogleWebSessionRow = (row) => {
923
1085
  if (!row || typeof row !== 'object') return null;
924
1086
  const token = String(row.session_token || '').trim();
@@ -1574,6 +1736,8 @@ const pruneExpiredAdminPanelSessions = () => {
1574
1736
  };
1575
1737
 
1576
1738
  const getAdminPanelSessionTokenFromRequest = (req) => {
1739
+ const direct = getCookieValuesFromRequest(req, ADMIN_PANEL_SESSION_COOKIE_NAME);
1740
+ if (direct.length > 0) return direct[0];
1577
1741
  const cookies = parseCookies(req);
1578
1742
  return String(cookies[ADMIN_PANEL_SESSION_COOKIE_NAME] || '').trim();
1579
1743
  };
@@ -1585,6 +1749,14 @@ const clearAdminPanelSessionCookie = (req, res) => {
1585
1749
  maxAgeSeconds: 0,
1586
1750
  }),
1587
1751
  );
1752
+ // Also clear host-only variant (legacy cookie written without Domain).
1753
+ appendSetCookie(
1754
+ res,
1755
+ buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, '', req, {
1756
+ maxAgeSeconds: 0,
1757
+ domain: false,
1758
+ }),
1759
+ );
1588
1760
  };
1589
1761
 
1590
1762
  const createAdminPanelSession = (googleSession, { role = 'owner' } = {}) => {
@@ -1732,61 +1904,67 @@ const activateGoogleWebSession = (session) => {
1732
1904
 
1733
1905
  const resolveGoogleWebSessionFromRequest = async (req) => {
1734
1906
  pruneExpiredGoogleSessions();
1735
- const sessionToken = getGoogleWebSessionTokenFromRequest(req);
1736
- if (!sessionToken) return null;
1737
- const session = webGoogleSessionMap.get(sessionToken);
1738
- if (session) {
1907
+ const sessionTokens = getGoogleWebSessionTokensFromRequest(req);
1908
+ if (!sessionTokens.length) return null;
1909
+
1910
+ for (const sessionToken of sessionTokens) {
1911
+ const session = webGoogleSessionMap.get(sessionToken);
1912
+ if (!session) continue;
1739
1913
  if (Number(session.expiresAt || 0) <= Date.now()) {
1740
1914
  webGoogleSessionMap.delete(sessionToken);
1741
- } else {
1742
- const now = Date.now();
1743
- session.lastSeenAt = now;
1744
- if (now - Number(session.lastDbTouchAt || 0) >= GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS) {
1745
- session.lastDbTouchAt = now;
1746
- void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
1747
- logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
1748
- action: 'sticker_pack_google_web_session_touch_failed',
1749
- error: error?.message,
1750
- });
1751
- });
1752
- void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1753
- }
1754
- try {
1755
- await assertGoogleIdentityNotBanned({
1756
- sub: session.sub,
1757
- email: session.email,
1758
- ownerJid: session.ownerJid,
1915
+ continue;
1916
+ }
1917
+
1918
+ const now = Date.now();
1919
+ session.lastSeenAt = now;
1920
+ if (now - Number(session.lastDbTouchAt || 0) >= GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS) {
1921
+ session.lastDbTouchAt = now;
1922
+ void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
1923
+ logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
1924
+ action: 'sticker_pack_google_web_session_touch_failed',
1925
+ error: error?.message,
1759
1926
  });
1760
- } catch (banError) {
1761
- webGoogleSessionMap.delete(sessionToken);
1762
- void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1763
- return null;
1764
- }
1765
- return session;
1927
+ });
1928
+ void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1766
1929
  }
1767
- }
1768
- try {
1769
- const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
1770
- if (!persistedSession) return null;
1771
1930
  try {
1772
1931
  await assertGoogleIdentityNotBanned({
1773
- sub: persistedSession.sub,
1774
- email: persistedSession.email,
1775
- ownerJid: persistedSession.ownerJid,
1932
+ sub: session.sub,
1933
+ email: session.email,
1934
+ ownerJid: session.ownerJid,
1776
1935
  });
1936
+ return session;
1777
1937
  } catch {
1778
- await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1779
- return null;
1938
+ webGoogleSessionMap.delete(sessionToken);
1939
+ void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1780
1940
  }
1781
- webGoogleSessionMap.set(sessionToken, persistedSession);
1782
- return persistedSession;
1783
- } catch (error) {
1784
- logger.warn('Falha ao resolver sessão Google web no banco.', {
1785
- action: 'sticker_pack_google_web_session_db_resolve_failed',
1786
- error: error?.message,
1787
- });
1788
- return null;
1789
1941
  }
1942
+
1943
+ for (const sessionToken of sessionTokens) {
1944
+ try {
1945
+ const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
1946
+ if (!persistedSession) continue;
1947
+ try {
1948
+ await assertGoogleIdentityNotBanned({
1949
+ sub: persistedSession.sub,
1950
+ email: persistedSession.email,
1951
+ ownerJid: persistedSession.ownerJid,
1952
+ });
1953
+ } catch {
1954
+ await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1955
+ continue;
1956
+ }
1957
+ webGoogleSessionMap.set(sessionToken, persistedSession);
1958
+ return persistedSession;
1959
+ } catch (error) {
1960
+ logger.warn('Falha ao resolver sessão Google web no banco.', {
1961
+ action: 'sticker_pack_google_web_session_db_resolve_failed',
1962
+ error: error?.message,
1963
+ });
1964
+ }
1965
+ }
1966
+
1967
+ return null;
1790
1968
  };
1791
1969
 
1792
1970
  const clearGoogleWebSessionCookie = (req, res) => {
@@ -1796,13 +1974,26 @@ const clearGoogleWebSessionCookie = (req, res) => {
1796
1974
  maxAgeSeconds: 0,
1797
1975
  }),
1798
1976
  );
1977
+ // Also clear host-only variant (legacy cookie written without Domain).
1978
+ appendSetCookie(
1979
+ res,
1980
+ buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, '', req, {
1981
+ maxAgeSeconds: 0,
1982
+ domain: false,
1983
+ }),
1984
+ );
1799
1985
  };
1800
1986
 
1801
1987
  const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
1988
+ const maxAgeSeconds = Math.max(60 * 60 * 24, ASSET_CACHE_SECONDS);
1989
+ const staleWhileRevalidateSeconds = Math.min(60 * 60 * 24 * 7, Math.max(300, maxAgeSeconds));
1802
1990
  res.statusCode = 200;
1803
1991
  res.setHeader('Content-Type', mimetype);
1804
1992
  res.setHeader('Content-Length', String(buffer.length));
1805
- res.setHeader('Cache-Control', `public, max-age=${ASSET_CACHE_SECONDS}`);
1993
+ res.setHeader(
1994
+ 'Cache-Control',
1995
+ `public, max-age=${maxAgeSeconds}, stale-while-revalidate=${staleWhileRevalidateSeconds}`,
1996
+ );
1806
1997
  if (req.method === 'HEAD') {
1807
1998
  res.end();
1808
1999
  return;
@@ -2041,8 +2232,12 @@ const buildStickerAssetUrl = (packKey, stickerId) =>
2041
2232
  `${STICKER_API_BASE_PATH}/${encodeURIComponent(packKey)}/stickers/${encodeURIComponent(stickerId)}.webp`;
2042
2233
  const buildOrphanStickersApiUrl = () => STICKER_ORPHAN_API_PATH;
2043
2234
  const buildDataAssetApiBaseUrl = () => `${STICKER_API_BASE_PATH}/data-files`;
2044
- const buildCatalogStylesUrl = () => `${STICKER_WEB_PATH}/assets/styles.css`;
2045
- const buildCatalogScriptUrl = () => `${STICKER_WEB_PATH}/assets/catalog.js`;
2235
+ const CATALOG_STYLES_WEB_PATH = `${STICKER_WEB_PATH}/assets/styles.css`;
2236
+ const CATALOG_SCRIPT_WEB_PATH = `${STICKER_WEB_PATH}/assets/catalog.js`;
2237
+ const appendAssetVersionQuery = (assetPath) =>
2238
+ STICKER_WEB_ASSET_VERSION ? `${assetPath}?v=${encodeURIComponent(STICKER_WEB_ASSET_VERSION)}` : assetPath;
2239
+ const buildCatalogStylesUrl = () => appendAssetVersionQuery(CATALOG_STYLES_WEB_PATH);
2240
+ const buildCatalogScriptUrl = () => appendAssetVersionQuery(CATALOG_SCRIPT_WEB_PATH);
2046
2241
  const buildDataAssetUrl = (relativePath) =>
2047
2242
  `${STICKER_DATA_PUBLIC_PATH}/${String(relativePath)
2048
2243
  .split('/')
@@ -2146,7 +2341,9 @@ const resolveWebCreateOwnerJid = async (explicitOwner = '') => {
2146
2341
  const resolvedAdminJid = await resolveAdminJid();
2147
2342
  const fromAdmin = toOwnerJid(resolvedAdminJid);
2148
2343
  if (fromAdmin) return fromAdmin;
2149
- } catch {}
2344
+ } catch {
2345
+ // Ignore fallback errors while resolving owner identity.
2346
+ }
2150
2347
 
2151
2348
  const adminCandidates = [
2152
2349
  getAdminRawValue(),
@@ -2316,14 +2513,18 @@ const resolveSupportAdminPhone = async () => {
2316
2513
  const resolvedFromLidMap = await resolveUserId(extractUserIdInfo(adminRaw));
2317
2514
  const resolvedPhoneFromLidMap = isPlausibleWhatsAppPhone(getJidUser(resolvedFromLidMap || ''));
2318
2515
  if (resolvedPhoneFromLidMap) return resolvedPhoneFromLidMap;
2319
- } catch {}
2516
+ } catch {
2517
+ // Ignore and fallback to other admin sources.
2518
+ }
2320
2519
  }
2321
2520
 
2322
2521
  try {
2323
2522
  const resolvedAdminJid = await resolveAdminJid();
2324
2523
  const resolvedPhone = isPlausibleWhatsAppPhone(getJidUser(resolvedAdminJid || ''));
2325
2524
  if (resolvedPhone) return resolvedPhone;
2326
- } catch {}
2525
+ } catch {
2526
+ // Ignore and fallback to static admin phone sources.
2527
+ }
2327
2528
 
2328
2529
  const rawPhone = isPlausibleWhatsAppPhone(getJidUser(adminRaw) || adminRaw);
2329
2530
  if (rawPhone) return rawPhone;
@@ -2725,6 +2926,10 @@ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnap
2725
2926
  const packIds = packs.map((pack) => pack.id);
2726
2927
  const engagementByPackId = await listStickerPackEngagementByPackIds(packIds);
2727
2928
  const interactionStatsByPackId = await listStickerPackInteractionStatsByPackIds(packIds);
2929
+ const useSnapshot = await canUseRankingSnapshotRead(`hydrate:${packIds.length}:${includeItems ? 1 : 0}`);
2930
+ const snapshotByPackId = useSnapshot
2931
+ ? await listStickerPackScoreSnapshotsByPackIds(packIds).catch(() => new Map())
2932
+ : new Map();
2728
2933
 
2729
2934
  const entries = [];
2730
2935
  const packClassificationById = new Map();
@@ -2743,14 +2948,24 @@ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnap
2743
2948
  const packMetadata = parsePackDescriptionMetadata(pack.description);
2744
2949
  const decoratedClassification = decoratePackClassificationSummary(packClassification);
2745
2950
  const mergedPackTags = mergeUniqueTags(decoratedClassification?.tags || [], packMetadata.tags);
2746
- const signals = computePackSignals({
2747
- pack: { ...pack, items },
2748
- engagement,
2749
- packClassification,
2750
- itemClassifications: orderedClassifications,
2751
- interactionStats,
2752
- scoringWeights: driftSnapshot?.weights || null,
2753
- });
2951
+ const snapshot = snapshotByPackId.get(pack.id);
2952
+ const signals = snapshot?.signals
2953
+ ? {
2954
+ ...snapshot.signals,
2955
+ ranking_score: Number(snapshot?.signals?.ranking_score || 0),
2956
+ pack_score: Number(snapshot?.signals?.pack_score || 0),
2957
+ trend_score: Number(snapshot?.signals?.trend_score || 0),
2958
+ nsfw_level: String(snapshot?.signals?.nsfw_level || 'safe'),
2959
+ sensitive_content: Boolean(snapshot?.signals?.sensitive_content),
2960
+ }
2961
+ : computePackSignals({
2962
+ pack: { ...pack, items },
2963
+ engagement,
2964
+ packClassification,
2965
+ itemClassifications: orderedClassifications,
2966
+ interactionStats,
2967
+ scoringWeights: driftSnapshot?.weights || null,
2968
+ });
2754
2969
 
2755
2970
  const entry = {
2756
2971
  pack,
@@ -3274,9 +3489,13 @@ const handleSitemapRequest = async (req, res) => {
3274
3489
  const sendStaticTextFile = async (req, res, filePath, contentType) => {
3275
3490
  try {
3276
3491
  const body = await fs.readFile(filePath, 'utf8');
3492
+ const hasVersionQuery = /(?:\?|&)v=/.test(String(req.url || ''));
3493
+ const cacheControl = hasVersionQuery
3494
+ ? `public, max-age=${IMMUTABLE_ASSET_CACHE_SECONDS}, immutable`
3495
+ : `public, max-age=${STATIC_TEXT_CACHE_SECONDS}, stale-while-revalidate=${Math.min(86400, STATIC_TEXT_CACHE_SECONDS * 4)}`;
3277
3496
  res.statusCode = 200;
3278
3497
  res.setHeader('Content-Type', contentType);
3279
- res.setHeader('Cache-Control', 'public, max-age=300');
3498
+ res.setHeader('Cache-Control', cacheControl);
3280
3499
  if (req.method === 'HEAD') {
3281
3500
  res.end();
3282
3501
  return true;
@@ -3300,11 +3519,11 @@ const sendStaticTextFile = async (req, res, filePath, contentType) => {
3300
3519
  };
3301
3520
 
3302
3521
  const handleCatalogStaticAssetRequest = async (req, res, pathname) => {
3303
- if (pathname === buildCatalogStylesUrl()) {
3522
+ if (pathname === CATALOG_STYLES_WEB_PATH) {
3304
3523
  return sendStaticTextFile(req, res, CATALOG_STYLES_FILE_PATH, 'text/css; charset=utf-8');
3305
3524
  }
3306
3525
 
3307
- if (pathname === buildCatalogScriptUrl()) {
3526
+ if (pathname === CATALOG_SCRIPT_WEB_PATH) {
3308
3527
  return sendStaticTextFile(req, res, CATALOG_SCRIPT_FILE_PATH, 'application/javascript; charset=utf-8');
3309
3528
  }
3310
3529
 
@@ -3321,95 +3540,118 @@ const handleListRequest = async (req, res, url) => {
3321
3540
  const limit = clampInt(url.searchParams.get('limit'), DEFAULT_LIST_LIMIT, 1, MAX_LIST_LIMIT);
3322
3541
  const offset = clampInt(url.searchParams.get('offset'), 0, 0, 100000);
3323
3542
  const normalizedIntent = normalizeCategoryToken(intent).replace(/-/g, '_');
3324
- const batchLimit = Math.max(limit, Math.min(MAX_LIST_LIMIT, 24));
3325
- const maxPagesToScan = 8;
3326
3543
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
3327
3544
  const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
3328
- const seenPackIds = new Set();
3329
- const collectedEntries = [];
3330
- const driftSnapshot = await getMarketplaceDriftSnapshot();
3331
- let sourceHasMore = true;
3332
- let cursorOffset = offset;
3333
- let pagesScanned = 0;
3545
+ const cacheKey = buildCacheKey([
3546
+ 'list',
3547
+ q,
3548
+ visibility,
3549
+ sort,
3550
+ categories.join(','),
3551
+ normalizedIntent,
3552
+ includeSensitive ? 1 : 0,
3553
+ limit,
3554
+ offset,
3555
+ hasNsfwAccess ? 1 : 0,
3556
+ ]);
3557
+ const payload = await getCachedSnapshot({
3558
+ cacheMap: CATALOG_LIST_CACHE,
3559
+ key: cacheKey,
3560
+ ttlSeconds: CATALOG_LIST_CACHE_SECONDS,
3561
+ staleWhileRefresh: true,
3562
+ staleOnError: true,
3563
+ load: async () => {
3564
+ const batchLimit = Math.max(limit, Math.min(MAX_LIST_LIMIT, 24));
3565
+ const maxPagesToScan = 8;
3566
+ const seenPackIds = new Set();
3567
+ const collectedEntries = [];
3568
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
3569
+ let sourceHasMore = true;
3570
+ let cursorOffset = offset;
3571
+ let pagesScanned = 0;
3572
+
3573
+ while (collectedEntries.length < limit && sourceHasMore && pagesScanned < maxPagesToScan) {
3574
+ pagesScanned += 1;
3575
+ const { packs, hasMore } = await listStickerPacksForCatalog({
3576
+ visibility,
3577
+ search: q,
3578
+ limit: batchLimit,
3579
+ offset: cursorOffset,
3580
+ });
3581
+ sourceHasMore = hasMore;
3582
+ cursorOffset += batchLimit;
3583
+ if (!packs.length) break;
3584
+
3585
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
3586
+ const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED
3587
+ ? entries.filter((entry) => isPackClassified(entry.packClassification))
3588
+ : entries;
3589
+ const entriesByCategory = categories.length
3590
+ ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories))
3591
+ : entriesClassified;
3592
+ const entriesBySensitivity = includeSensitive
3593
+ ? entriesByCategory
3594
+ : entriesByCategory.filter((entry) => entry.signals?.nsfw_level === 'safe');
3595
+ const entriesByIntent = intent
3596
+ ? entriesBySensitivity.filter((entry) => classifyPackIntent(entry) === normalizedIntent)
3597
+ : entriesBySensitivity;
3598
+ const sortedEntries = [...entriesByIntent].sort((left, right) => {
3599
+ const completenessDelta = compareEntriesByPackCompleteness(left, right);
3600
+ if (completenessDelta !== 0) return completenessDelta;
3601
+ if (sort === 'recent') {
3602
+ return Date.parse(right?.pack?.created_at || right?.pack?.updated_at || 0) - Date.parse(left?.pack?.created_at || left?.pack?.updated_at || 0);
3603
+ }
3604
+ if (sort === 'likes') {
3605
+ return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3606
+ }
3607
+ if (sort === 'downloads') {
3608
+ return Number(right?.engagement?.open_count || 0) - Number(left?.engagement?.open_count || 0);
3609
+ }
3610
+ if (sort === 'comments') {
3611
+ const commentDelta = Number(right?.engagement?.comment_count || 0) - Number(left?.engagement?.comment_count || 0);
3612
+ if (commentDelta !== 0) return commentDelta;
3613
+ return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3614
+ }
3615
+ if (sort === 'trending') {
3616
+ const trendDelta = Number(right?.signals?.trend_score || 0) - Number(left?.signals?.trend_score || 0);
3617
+ if (trendDelta !== 0) return trendDelta;
3618
+ }
3619
+ const leftScore = Number(left?.signals?.ranking_score || 0);
3620
+ const rightScore = Number(right?.signals?.ranking_score || 0);
3621
+ if (rightScore !== leftScore) return rightScore - leftScore;
3622
+ return Date.parse(right?.pack?.updated_at || 0) - Date.parse(left?.pack?.updated_at || 0);
3623
+ });
3334
3624
 
3335
- while (collectedEntries.length < limit && sourceHasMore && pagesScanned < maxPagesToScan) {
3336
- pagesScanned += 1;
3337
- const { packs, hasMore } = await listStickerPacksForCatalog({
3338
- visibility,
3339
- search: q,
3340
- limit: batchLimit,
3341
- offset: cursorOffset,
3342
- });
3343
- sourceHasMore = hasMore;
3344
- cursorOffset += batchLimit;
3345
- if (!packs.length) break;
3346
-
3347
- const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
3348
- const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED
3349
- ? entries.filter((entry) => isPackClassified(entry.packClassification))
3350
- : entries;
3351
- const entriesByCategory = categories.length
3352
- ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories))
3353
- : entriesClassified;
3354
- const entriesBySensitivity = includeSensitive
3355
- ? entriesByCategory
3356
- : entriesByCategory.filter((entry) => entry.signals?.nsfw_level === 'safe');
3357
- const entriesByIntent = intent
3358
- ? entriesBySensitivity.filter((entry) => classifyPackIntent(entry) === normalizedIntent)
3359
- : entriesBySensitivity;
3360
- const sortedEntries = [...entriesByIntent].sort((left, right) => {
3361
- const completenessDelta = compareEntriesByPackCompleteness(left, right);
3362
- if (completenessDelta !== 0) return completenessDelta;
3363
- if (sort === 'recent') {
3364
- return Date.parse(right?.pack?.created_at || right?.pack?.updated_at || 0) - Date.parse(left?.pack?.created_at || left?.pack?.updated_at || 0);
3365
- }
3366
- if (sort === 'likes') {
3367
- return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3368
- }
3369
- if (sort === 'downloads') {
3370
- return Number(right?.engagement?.open_count || 0) - Number(left?.engagement?.open_count || 0);
3371
- }
3372
- if (sort === 'comments') {
3373
- const commentDelta = Number(right?.engagement?.comment_count || 0) - Number(left?.engagement?.comment_count || 0);
3374
- if (commentDelta !== 0) return commentDelta;
3375
- return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
3376
- }
3377
- if (sort === 'trending') {
3378
- const trendDelta = Number(right?.signals?.trend_score || 0) - Number(left?.signals?.trend_score || 0);
3379
- if (trendDelta !== 0) return trendDelta;
3625
+ for (const entry of sortedEntries) {
3626
+ if (!entry?.pack?.id) continue;
3627
+ if (seenPackIds.has(entry.pack.id)) continue;
3628
+ seenPackIds.add(entry.pack.id);
3629
+ collectedEntries.push(entry);
3630
+ if (collectedEntries.length >= limit) break;
3631
+ }
3380
3632
  }
3381
- const leftScore = Number(left?.signals?.ranking_score || 0);
3382
- const rightScore = Number(right?.signals?.ranking_score || 0);
3383
- if (rightScore !== leftScore) return rightScore - leftScore;
3384
- return Date.parse(right?.pack?.updated_at || 0) - Date.parse(left?.pack?.updated_at || 0);
3385
- });
3386
-
3387
- for (const entry of sortedEntries) {
3388
- if (!entry?.pack?.id) continue;
3389
- if (seenPackIds.has(entry.pack.id)) continue;
3390
- seenPackIds.add(entry.pack.id);
3391
- collectedEntries.push(entry);
3392
- if (collectedEntries.length >= limit) break;
3393
- }
3394
- }
3395
3633
 
3396
- sendJson(req, res, 200, {
3397
- data: collectedEntries.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
3398
- pagination: {
3399
- limit,
3400
- offset,
3401
- has_more: sourceHasMore,
3402
- next_offset: sourceHasMore ? cursorOffset : null,
3403
- },
3404
- filters: {
3405
- q,
3406
- visibility,
3407
- sort,
3408
- categories,
3409
- intent: intent || null,
3410
- include_sensitive: includeSensitive,
3634
+ return {
3635
+ data: collectedEntries.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
3636
+ pagination: {
3637
+ limit,
3638
+ offset,
3639
+ has_more: sourceHasMore,
3640
+ next_offset: sourceHasMore ? cursorOffset : null,
3641
+ },
3642
+ filters: {
3643
+ q,
3644
+ visibility,
3645
+ sort,
3646
+ categories,
3647
+ intent: intent || null,
3648
+ include_sensitive: includeSensitive,
3649
+ },
3650
+ };
3411
3651
  },
3412
3652
  });
3653
+
3654
+ sendJson(req, res, 200, payload);
3413
3655
  };
3414
3656
 
3415
3657
  const handleIntentCollectionsRequest = async (req, res, url) => {
@@ -3647,9 +3889,9 @@ const handleGoogleAuthSessionRequest = async (req, res) => {
3647
3889
  }
3648
3890
 
3649
3891
  if (req.method === 'DELETE') {
3650
- const token = getGoogleWebSessionTokenFromRequest(req);
3651
- if (token) webGoogleSessionMap.delete(token);
3652
- if (token) {
3892
+ const tokens = getGoogleWebSessionTokensFromRequest(req);
3893
+ for (const token of tokens) {
3894
+ webGoogleSessionMap.delete(token);
3653
3895
  await deleteGoogleWebSessionFromDb(token).catch((error) => {
3654
3896
  logger.warn('Falha ao remover sessão Google web do banco.', {
3655
3897
  action: 'sticker_pack_google_web_session_db_delete_failed',
@@ -4090,6 +4332,9 @@ const invalidateStickerCatalogDerivedCaches = () => {
4090
4332
  GLOBAL_RANK_CACHE.value = null;
4091
4333
  GLOBAL_RANK_CACHE.pending = null;
4092
4334
  HOME_MARKETPLACE_STATS_CACHE.clear();
4335
+ CATALOG_LIST_CACHE.clear();
4336
+ CATALOG_CREATOR_RANKING_CACHE.clear();
4337
+ CATALOG_PACK_PAYLOAD_CACHE.clear();
4093
4338
  SYSTEM_SUMMARY_CACHE.expiresAt = 0;
4094
4339
  SYSTEM_SUMMARY_CACHE.value = null;
4095
4340
  SYSTEM_SUMMARY_CACHE.pending = null;
@@ -5753,46 +5998,63 @@ const handleCreatorRankingRequest = async (req, res, url) => {
5753
5998
  const limit = clampInt(url.searchParams.get('limit'), 50, 5, 200);
5754
5999
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
5755
6000
  const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
5756
-
5757
- const { packs } = await listStickerPacksForCatalog({
6001
+ const cacheKey = buildCacheKey([
6002
+ 'creator_ranking',
5758
6003
  visibility,
5759
- search: q,
5760
- limit: 120,
5761
- offset: 0,
5762
- });
5763
- const driftSnapshot = await getMarketplaceDriftSnapshot();
5764
- const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
5765
- const ranking = buildCreatorRanking(
5766
- STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries,
5767
- { limit },
5768
- );
6004
+ q,
6005
+ limit,
6006
+ hasNsfwAccess ? 1 : 0,
6007
+ ]);
6008
+ const payload = await getCachedSnapshot({
6009
+ cacheMap: CATALOG_CREATOR_RANKING_CACHE,
6010
+ key: cacheKey,
6011
+ ttlSeconds: CATALOG_CREATOR_RANKING_CACHE_SECONDS,
6012
+ staleWhileRefresh: true,
6013
+ staleOnError: true,
6014
+ load: async () => {
6015
+ const { packs } = await listStickerPacksForCatalog({
6016
+ visibility,
6017
+ search: q,
6018
+ limit: 120,
6019
+ offset: 0,
6020
+ });
6021
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
6022
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
6023
+ const ranking = buildCreatorRanking(
6024
+ STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries,
6025
+ { limit },
6026
+ );
5769
6027
 
5770
- sendJson(req, res, 200, {
5771
- data: ranking.map((creator) => ({
5772
- creator_score: Number(
5773
- (
5774
- Number(creator.avg_pack_score || 0) * 0.45 +
5775
- Number(creator.total_likes || 0) * 0.0008 +
5776
- Number(creator.total_opens || 0) * 0.00015
5777
- ).toFixed(6),
5778
- ),
5779
- publisher: creator.publisher,
5780
- verified: Boolean(creator.verified),
5781
- badges: creator.verified ? ['verified_creator'] : [],
5782
- stats: {
5783
- packs_count: Number(creator.packs_count || 0),
5784
- total_likes: Number(creator.total_likes || 0),
5785
- total_opens: Number(creator.total_opens || 0),
5786
- avg_pack_score: Number(creator.avg_pack_score || 0),
5787
- },
5788
- top_pack: creator.top_pack ? toSummaryEntry(creator.top_pack, { hideSensitiveCover: !hasNsfwAccess }) : null,
5789
- })),
5790
- filters: {
5791
- visibility,
5792
- q,
5793
- limit,
6028
+ return {
6029
+ data: ranking.map((creator) => ({
6030
+ creator_score: Number(
6031
+ (
6032
+ Number(creator.avg_pack_score || 0) * 0.45 +
6033
+ Number(creator.total_likes || 0) * 0.0008 +
6034
+ Number(creator.total_opens || 0) * 0.00015
6035
+ ).toFixed(6),
6036
+ ),
6037
+ publisher: creator.publisher,
6038
+ verified: Boolean(creator.verified),
6039
+ badges: creator.verified ? ['verified_creator'] : [],
6040
+ stats: {
6041
+ packs_count: Number(creator.packs_count || 0),
6042
+ total_likes: Number(creator.total_likes || 0),
6043
+ total_opens: Number(creator.total_opens || 0),
6044
+ avg_pack_score: Number(creator.avg_pack_score || 0),
6045
+ },
6046
+ top_pack: creator.top_pack ? toSummaryEntry(creator.top_pack, { hideSensitiveCover: !hasNsfwAccess }) : null,
6047
+ })),
6048
+ filters: {
6049
+ visibility,
6050
+ q,
6051
+ limit,
6052
+ },
6053
+ };
5794
6054
  },
5795
6055
  });
6056
+
6057
+ sendJson(req, res, 200, payload);
5796
6058
  };
5797
6059
 
5798
6060
  const handleRecommendationsRequest = async (req, res, url) => {
@@ -6934,42 +7196,60 @@ const handlePublicDataAssetRequest = async (req, res, pathname) => {
6934
7196
  };
6935
7197
 
6936
7198
  const fetchPublicPackPayload = async (normalizedPackKey) => {
6937
- const pack = await findStickerPackByPackKey(normalizedPackKey);
6938
- if (!pack || !isPackPubliclyVisible(pack)) return null;
6939
-
6940
- const items = await listStickerPackItems(pack.id);
6941
- const stickerIds = items.map((item) => item.sticker_id);
6942
- const [classifications, packClassification, engagement] = await Promise.all([
6943
- listStickerClassificationsByAssetIds(stickerIds),
6944
- getPackClassificationSummaryByAssetIds(stickerIds),
6945
- getStickerPackEngagementByPackId(pack.id),
6946
- ]);
7199
+ const cacheKey = buildCacheKey(['pack_payload', normalizedPackKey]);
7200
+ return getCachedSnapshot({
7201
+ cacheMap: CATALOG_PACK_PAYLOAD_CACHE,
7202
+ key: cacheKey,
7203
+ ttlSeconds: CATALOG_PACK_PAYLOAD_CACHE_SECONDS,
7204
+ staleWhileRefresh: true,
7205
+ staleOnError: true,
7206
+ load: async () => {
7207
+ const pack = await findStickerPackByPackKey(normalizedPackKey);
7208
+ if (!pack || !isPackPubliclyVisible(pack)) return null;
7209
+
7210
+ const items = await listStickerPackItems(pack.id);
7211
+ const stickerIds = items.map((item) => item.sticker_id);
7212
+ const [classifications, packClassification, engagement] = await Promise.all([
7213
+ listStickerClassificationsByAssetIds(stickerIds),
7214
+ getPackClassificationSummaryByAssetIds(stickerIds),
7215
+ getStickerPackEngagementByPackId(pack.id),
7216
+ ]);
7217
+
7218
+ if (STICKER_CATALOG_ONLY_CLASSIFIED && !isPackClassified(packClassification)) {
7219
+ return null;
7220
+ }
6947
7221
 
6948
- if (STICKER_CATALOG_ONLY_CLASSIFIED && !isPackClassified(packClassification)) {
6949
- return null;
6950
- }
7222
+ const [interactionStatsByPack, driftSnapshot, snapshotByPackId] = await Promise.all([
7223
+ listStickerPackInteractionStatsByPackIds([pack.id]),
7224
+ getMarketplaceDriftSnapshot(),
7225
+ canUseRankingSnapshotRead(`pack_payload:${pack.id}`)
7226
+ .then((enabled) => (enabled ? listStickerPackScoreSnapshotsByPackIds([pack.id]) : new Map()))
7227
+ .catch(() => new Map()),
7228
+ ]);
7229
+ const byAssetClassification = new Map(classifications.map((entry) => [entry.asset_id, entry]));
7230
+ const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
7231
+ const snapshot = snapshotByPackId.get(pack.id);
7232
+ const signals = snapshot?.signals
7233
+ ? snapshot.signals
7234
+ : computePackSignals({
7235
+ pack: { ...pack, items },
7236
+ engagement,
7237
+ packClassification,
7238
+ itemClassifications: orderedClassifications,
7239
+ interactionStats: interactionStatsByPack.get(pack.id) || null,
7240
+ scoringWeights: driftSnapshot.weights,
7241
+ });
6951
7242
 
6952
- const interactionStatsByPack = await listStickerPackInteractionStatsByPackIds([pack.id]);
6953
- const driftSnapshot = await getMarketplaceDriftSnapshot();
6954
- const byAssetClassification = new Map(classifications.map((entry) => [entry.asset_id, entry]));
6955
- const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
6956
- const signals = computePackSignals({
6957
- pack: { ...pack, items },
6958
- engagement,
6959
- packClassification,
6960
- itemClassifications: orderedClassifications,
6961
- interactionStats: interactionStatsByPack.get(pack.id) || null,
6962
- scoringWeights: driftSnapshot.weights,
7243
+ return {
7244
+ pack,
7245
+ items,
7246
+ byAssetClassification,
7247
+ packClassification,
7248
+ engagement,
7249
+ signals,
7250
+ };
7251
+ },
6963
7252
  });
6964
-
6965
- return {
6966
- pack,
6967
- items,
6968
- byAssetClassification,
6969
- packClassification,
6970
- engagement,
6971
- signals,
6972
- };
6973
7253
  };
6974
7254
 
6975
7255
  const handleDetailsRequest = async (req, res, packKey, url) => {
@@ -7060,6 +7340,23 @@ const handleAssetRequest = async (req, res, packKey, stickerToken) => {
7060
7340
  return;
7061
7341
  }
7062
7342
  }
7343
+
7344
+ const externalAssetUrl = await getStickerAssetExternalUrl(item.asset, {
7345
+ secure: true,
7346
+ expiresInSeconds: Math.max(60, Math.min(3600, Number(process.env.STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS) || 300)),
7347
+ }).catch(() => null);
7348
+ if (externalAssetUrl) {
7349
+ res.statusCode = 302;
7350
+ res.setHeader('Location', externalAssetUrl);
7351
+ res.setHeader('Cache-Control', 'private, max-age=45');
7352
+ if (req.method === 'HEAD') {
7353
+ res.end();
7354
+ return;
7355
+ }
7356
+ res.end();
7357
+ return;
7358
+ }
7359
+
7063
7360
  if (decorated) {
7064
7361
  res.setHeader('X-Sticker-Category', String(decorated?.category || 'unknown'));
7065
7362
  res.setHeader('X-Sticker-NSFW', decorated?.is_nsfw ? '1' : '0');
@@ -7667,323 +7964,60 @@ const handleAdminBanRevokeRequest = async (req, res, banId) => {
7667
7964
  }
7668
7965
  };
7669
7966
 
7670
- const handleCatalogApiRequest = async (req, res, pathname, url) => {
7671
- if (pathname === `${STICKER_API_BASE_PATH}/create`) {
7672
- if (req.method !== 'POST') {
7673
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7674
- return true;
7675
- }
7676
- await handleCreatePackRequest(req, res);
7677
- return true;
7678
- }
7679
-
7680
- if (pathname === `${STICKER_API_BASE_PATH}/auth/google/session`) {
7681
- await handleGoogleAuthSessionRequest(req, res);
7682
- return true;
7683
- }
7684
-
7685
- if (pathname === `${STICKER_API_BASE_PATH}/me`) {
7686
- await handleMyProfileRequest(req, res, url);
7687
- return true;
7688
- }
7689
-
7690
- if (pathname === `${STICKER_API_BASE_PATH}/admin/session`) {
7691
- await handleAdminPanelSessionRequest(req, res);
7692
- return true;
7693
- }
7694
-
7695
- if (pathname === STICKER_API_BASE_PATH) {
7696
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7697
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7698
- return true;
7699
- }
7700
- await handleListRequest(req, res, url);
7701
- return true;
7702
- }
7703
-
7704
- if (pathname === `${STICKER_API_BASE_PATH}/intents`) {
7705
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7706
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7707
- return true;
7708
- }
7709
- await handleIntentCollectionsRequest(req, res, url);
7710
- return true;
7711
- }
7712
-
7713
- if (pathname === `${STICKER_API_BASE_PATH}/creators`) {
7714
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7715
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7716
- return true;
7717
- }
7718
- await handleCreatorRankingRequest(req, res, url);
7719
- return true;
7720
- }
7721
-
7722
- if (pathname === `${STICKER_API_BASE_PATH}/recommendations`) {
7723
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7724
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7725
- return true;
7726
- }
7727
- await handleRecommendationsRequest(req, res, url);
7728
- return true;
7729
- }
7730
-
7731
- if (pathname === `${STICKER_API_BASE_PATH}/stats`) {
7732
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7733
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7734
- return true;
7735
- }
7736
- await handleMarketplaceStatsRequest(req, res, url);
7737
- return true;
7738
- }
7739
-
7740
- if (pathname === `${STICKER_API_BASE_PATH}/create-config`) {
7741
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7742
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7743
- return true;
7744
- }
7745
- await handleCreatePackConfigRequest(req, res);
7746
- return true;
7747
- }
7748
-
7749
- if (pathname === STICKER_ORPHAN_API_PATH) {
7750
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7751
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7752
- return true;
7753
- }
7754
- await handleOrphanStickerListRequest(req, res, url);
7755
- return true;
7756
- }
7757
-
7758
- const suffix = pathname.slice(STICKER_API_BASE_PATH.length).replace(/^\/+/, '');
7759
- if (!suffix) return false;
7760
-
7761
- const segments = suffix.split('/').filter(Boolean).map((segment) => {
7762
- try {
7763
- return decodeURIComponent(segment);
7764
- } catch {
7765
- return segment;
7766
- }
7767
- });
7768
-
7769
- if (segments.length === 1 && segments[0] === 'data-files') {
7770
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7771
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7772
- return true;
7773
- }
7774
- await handleDataFileListRequest(req, res, url);
7775
- return true;
7776
- }
7777
-
7778
- if (segments.length === 1 && segments[0] === 'system-summary') {
7779
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7780
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7781
- return true;
7782
- }
7783
- await handleSystemSummaryRequest(req, res);
7784
- return true;
7785
- }
7786
-
7787
- if (segments.length === 1 && segments[0] === 'project-summary') {
7788
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7789
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7790
- return true;
7791
- }
7792
- await handleGitHubProjectSummaryRequest(req, res);
7793
- return true;
7794
- }
7795
-
7796
- if (segments.length === 1 && segments[0] === 'global-ranking-summary') {
7797
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7798
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7799
- return true;
7800
- }
7801
- await handleGlobalRankingSummaryRequest(req, res);
7802
- return true;
7803
- }
7804
-
7805
- if (segments.length === 1 && segments[0] === 'readme-summary') {
7806
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7807
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7808
- return true;
7809
- }
7810
- await handleReadmeSummaryRequest(req, res);
7811
- return true;
7812
- }
7813
-
7814
- if (segments.length === 1 && segments[0] === 'readme-markdown') {
7815
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7816
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7817
- return true;
7818
- }
7819
- await handleReadmeMarkdownRequest(req, res);
7820
- return true;
7821
- }
7822
-
7823
- if (segments.length === 1 && segments[0] === 'support') {
7824
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7825
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7826
- return true;
7827
- }
7828
- await handleSupportInfoRequest(req, res);
7829
- return true;
7830
- }
7831
-
7832
- if (segments.length === 1 && segments[0] === 'bot-contact') {
7833
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7834
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7835
- return true;
7836
- }
7837
- await handleBotContactInfoRequest(req, res);
7838
- return true;
7839
- }
7840
-
7841
- if (segments[0] === 'admin') {
7842
- if (segments.length === 2 && segments[1] === 'overview') {
7843
- await handleAdminOverviewRequest(req, res);
7844
- return true;
7845
- }
7846
- if (segments.length === 2 && segments[1] === 'users') {
7847
- await handleAdminUsersRequest(req, res, url);
7848
- return true;
7849
- }
7850
- if (segments.length === 2 && segments[1] === 'moderators') {
7851
- await handleAdminModeratorsRequest(req, res);
7852
- return true;
7853
- }
7854
- if (segments.length === 3 && segments[1] === 'moderators') {
7855
- await handleAdminModeratorDeleteRequest(req, res, segments[2]);
7856
- return true;
7857
- }
7858
- if (segments.length === 2 && segments[1] === 'packs') {
7859
- await handleAdminPacksRequest(req, res, url);
7860
- return true;
7861
- }
7862
- if (segments.length === 3 && segments[1] === 'packs') {
7863
- await handleAdminPackDetailsRequest(req, res, segments[2]);
7864
- return true;
7865
- }
7866
- if (segments.length === 4 && segments[1] === 'packs' && segments[3] === 'delete') {
7867
- await handleAdminPackDeleteRequest(req, res, segments[2]);
7868
- return true;
7869
- }
7870
- if (segments.length === 6 && segments[1] === 'packs' && segments[3] === 'stickers' && segments[5] === 'delete') {
7871
- await handleAdminPackStickerDeleteRequest(req, res, segments[2], segments[4]);
7872
- return true;
7873
- }
7874
- if (segments.length === 4 && segments[1] === 'stickers' && segments[3] === 'delete') {
7875
- await handleAdminGlobalStickerDeleteRequest(req, res, segments[2]);
7876
- return true;
7877
- }
7878
- if (segments.length === 2 && segments[1] === 'bans') {
7879
- await handleAdminBansRequest(req, res);
7880
- return true;
7881
- }
7882
- if (segments.length === 4 && segments[1] === 'bans' && segments[3] === 'revoke') {
7883
- await handleAdminBanRevokeRequest(req, res, segments[2]);
7884
- return true;
7885
- }
7886
- sendJson(req, res, 404, { error: 'Rota admin nao encontrada.' });
7887
- return true;
7888
- }
7889
-
7890
- if (segments.length === 1) {
7891
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7892
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7893
- return true;
7894
- }
7895
- await handleDetailsRequest(req, res, segments[0], url);
7896
- return true;
7897
- }
7898
-
7899
- if (segments.length === 2 && ['open', 'like', 'dislike'].includes(segments[1])) {
7900
- if (req.method !== 'POST') {
7901
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7902
- return true;
7903
- }
7904
- await handlePackInteractionRequest(req, res, segments[0], segments[1], url);
7905
- return true;
7906
- }
7907
-
7908
- if (segments.length === 2 && segments[1] === 'manage') {
7909
- await handleManagedPackRequest(req, res, segments[0]);
7910
- return true;
7911
- }
7912
-
7913
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'clone') {
7914
- await handleManagedPackCloneRequest(req, res, segments[0]);
7915
- return true;
7916
- }
7917
-
7918
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'cover') {
7919
- await handleManagedPackCoverRequest(req, res, segments[0]);
7920
- return true;
7921
- }
7922
-
7923
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'reorder') {
7924
- await handleManagedPackReorderRequest(req, res, segments[0]);
7925
- return true;
7926
- }
7927
-
7928
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'analytics') {
7929
- await handleManagedPackAnalyticsRequest(req, res, segments[0]);
7930
- return true;
7931
- }
7932
-
7933
- if (segments.length === 3 && segments[1] === 'manage' && segments[2] === 'stickers') {
7934
- await handleManagedPackStickerCreateRequest(req, res, segments[0]);
7935
- return true;
7936
- }
7937
-
7938
- if (segments.length === 4 && segments[1] === 'manage' && segments[2] === 'stickers') {
7939
- await handleManagedPackStickerDeleteRequest(req, res, segments[0], segments[3]);
7940
- return true;
7941
- }
7942
-
7943
- if (segments.length === 5 && segments[1] === 'manage' && segments[2] === 'stickers' && segments[4] === 'replace') {
7944
- await handleManagedPackStickerReplaceRequest(req, res, segments[0], segments[3]);
7945
- return true;
7946
- }
7947
-
7948
- if (segments.length === 2 && segments[1] === 'publish-state') {
7949
- if (!['GET', 'HEAD', 'POST'].includes(req.method || '')) {
7950
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7951
- return true;
7952
- }
7953
- await handlePackPublishStateRequest(req, res, segments[0], url);
7954
- return true;
7955
- }
7956
-
7957
- if (segments.length === 2 && segments[1] === 'finalize') {
7958
- if (req.method !== 'POST') {
7959
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7960
- return true;
7961
- }
7962
- await handleFinalizePackRequest(req, res, segments[0]);
7963
- return true;
7964
- }
7965
-
7966
- if (segments.length === 2 && segments[1] === 'stickers-upload') {
7967
- if (req.method !== 'POST') {
7968
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7969
- return true;
7970
- }
7971
- await handleUploadStickerToPackRequest(req, res, segments[0]);
7972
- return true;
7973
- }
7974
-
7975
- if (segments.length === 3 && segments[1] === 'stickers') {
7976
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7977
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7978
- return true;
7979
- }
7980
- await handleAssetRequest(req, res, segments[0], segments[2]);
7981
- return true;
7982
- }
7967
+ const catalogApiRouter = createCatalogApiRouter({
7968
+ apiBasePath: STICKER_API_BASE_PATH,
7969
+ orphanApiPath: STICKER_ORPHAN_API_PATH,
7970
+ sendJson,
7971
+ handlers: {
7972
+ handleCreatePackRequest,
7973
+ handleGoogleAuthSessionRequest,
7974
+ handleMyProfileRequest,
7975
+ handleAdminPanelSessionRequest,
7976
+ handleListRequest,
7977
+ handleIntentCollectionsRequest,
7978
+ handleCreatorRankingRequest,
7979
+ handleRecommendationsRequest,
7980
+ handleMarketplaceStatsRequest,
7981
+ handleCreatePackConfigRequest,
7982
+ handleOrphanStickerListRequest,
7983
+ handleDataFileListRequest,
7984
+ handleSystemSummaryRequest,
7985
+ handleGitHubProjectSummaryRequest,
7986
+ handleGlobalRankingSummaryRequest,
7987
+ handleReadmeSummaryRequest,
7988
+ handleReadmeMarkdownRequest,
7989
+ handleSupportInfoRequest,
7990
+ handleBotContactInfoRequest,
7991
+ handleAdminOverviewRequest,
7992
+ handleAdminUsersRequest,
7993
+ handleAdminModeratorsRequest,
7994
+ handleAdminModeratorDeleteRequest,
7995
+ handleAdminPacksRequest,
7996
+ handleAdminPackDetailsRequest,
7997
+ handleAdminPackDeleteRequest,
7998
+ handleAdminPackStickerDeleteRequest,
7999
+ handleAdminGlobalStickerDeleteRequest,
8000
+ handleAdminBansRequest,
8001
+ handleAdminBanRevokeRequest,
8002
+ handleDetailsRequest,
8003
+ handlePackInteractionRequest,
8004
+ handleManagedPackRequest,
8005
+ handleManagedPackCloneRequest,
8006
+ handleManagedPackCoverRequest,
8007
+ handleManagedPackReorderRequest,
8008
+ handleManagedPackAnalyticsRequest,
8009
+ handleManagedPackStickerCreateRequest,
8010
+ handleManagedPackStickerDeleteRequest,
8011
+ handleManagedPackStickerReplaceRequest,
8012
+ handlePackPublishStateRequest,
8013
+ handleFinalizePackRequest,
8014
+ handleUploadStickerToPackRequest,
8015
+ handleAssetRequest,
8016
+ },
8017
+ });
7983
8018
 
7984
- sendJson(req, res, 404, { error: 'Rota de sticker pack nao encontrada.' });
7985
- return true;
7986
- };
8019
+ const handleCatalogApiRequest = async (req, res, pathname, url) =>
8020
+ catalogApiRouter({ req, res, pathname, url });
7987
8021
 
7988
8022
  const handleCatalogPageRequest = async (req, res, pathname) => {
7989
8023
  const normalizedPath = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;