@monetize.software/sdk-extension 3.0.0-alpha.2 → 3.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -20
- package/dist/chunks/ar-7cgIM-Vl.js +2 -0
- package/dist/chunks/ar-7cgIM-Vl.js.map +1 -0
- package/dist/chunks/ar-B2Wg_IrC.js +126 -0
- package/dist/chunks/ar-B2Wg_IrC.js.map +1 -0
- package/dist/chunks/chrome-port-BEMjZQAH.js +2 -0
- package/dist/chunks/chrome-port-BEMjZQAH.js.map +1 -0
- package/dist/chunks/{chrome-port-DPFUj1MP.js → chrome-port-bfTUUDz_.js} +332 -211
- package/dist/chunks/chrome-port-bfTUUDz_.js.map +1 -0
- package/dist/chunks/cs-BNo9Dx0Q.js +122 -0
- package/dist/chunks/cs-BNo9Dx0Q.js.map +1 -0
- package/dist/chunks/cs-S05PC5AC.js +2 -0
- package/dist/chunks/cs-S05PC5AC.js.map +1 -0
- package/dist/chunks/da-Bi4zBG14.js +2 -0
- package/dist/chunks/da-Bi4zBG14.js.map +1 -0
- package/dist/chunks/da-Do9Lq6En.js +122 -0
- package/dist/chunks/da-Do9Lq6En.js.map +1 -0
- package/dist/chunks/de-C8pDZNvx.js +141 -0
- package/dist/chunks/de-C8pDZNvx.js.map +1 -0
- package/dist/chunks/de-nCDB6D2W.js +2 -0
- package/dist/chunks/de-nCDB6D2W.js.map +1 -0
- package/dist/chunks/el-BrKaa978.js +2 -0
- package/dist/chunks/el-BrKaa978.js.map +1 -0
- package/dist/chunks/el-DzMNX-_P.js +126 -0
- package/dist/chunks/el-DzMNX-_P.js.map +1 -0
- package/dist/chunks/es-B-Wtyzrl.js +2 -0
- package/dist/chunks/es-B-Wtyzrl.js.map +1 -0
- package/dist/chunks/es-YrKt-q4w.js +141 -0
- package/dist/chunks/es-YrKt-q4w.js.map +1 -0
- package/dist/chunks/fi-Bh44pwZ4.js +122 -0
- package/dist/chunks/fi-Bh44pwZ4.js.map +1 -0
- package/dist/chunks/fi-D1SGXjnO.js +2 -0
- package/dist/chunks/fi-D1SGXjnO.js.map +1 -0
- package/dist/chunks/fr-Bc0pw4ws.js +141 -0
- package/dist/chunks/fr-Bc0pw4ws.js.map +1 -0
- package/dist/chunks/fr-BhYf-iKk.js +2 -0
- package/dist/chunks/fr-BhYf-iKk.js.map +1 -0
- package/dist/chunks/he-BXAaFv6Y.js +2 -0
- package/dist/chunks/he-BXAaFv6Y.js.map +1 -0
- package/dist/chunks/he-Bfm-bhe3.js +126 -0
- package/dist/chunks/he-Bfm-bhe3.js.map +1 -0
- package/dist/chunks/hi-D-O-B9Dn.js +126 -0
- package/dist/chunks/hi-D-O-B9Dn.js.map +1 -0
- package/dist/chunks/hi-xblDO0O7.js +2 -0
- package/dist/chunks/hi-xblDO0O7.js.map +1 -0
- package/dist/chunks/hu-CmIuAbLL.js +122 -0
- package/dist/chunks/hu-CmIuAbLL.js.map +1 -0
- package/dist/chunks/hu-Wa46p0y4.js +2 -0
- package/dist/chunks/hu-Wa46p0y4.js.map +1 -0
- package/dist/chunks/id-CQEo5X94.js +2 -0
- package/dist/chunks/id-CQEo5X94.js.map +1 -0
- package/dist/chunks/id-DN7IES-A.js +122 -0
- package/dist/chunks/id-DN7IES-A.js.map +1 -0
- package/dist/chunks/it-8AYCm0xz.js +2 -0
- package/dist/chunks/it-8AYCm0xz.js.map +1 -0
- package/dist/chunks/it-Cz5Nmqx5.js +141 -0
- package/dist/chunks/it-Cz5Nmqx5.js.map +1 -0
- package/dist/chunks/ja-BH9BlBh2.js +145 -0
- package/dist/chunks/ja-BH9BlBh2.js.map +1 -0
- package/dist/chunks/ja-q-COVayn.js +2 -0
- package/dist/chunks/ja-q-COVayn.js.map +1 -0
- package/dist/chunks/ko-B6HRCscZ.js +2 -0
- package/dist/chunks/ko-B6HRCscZ.js.map +1 -0
- package/dist/chunks/ko-CYV9QuYs.js +145 -0
- package/dist/chunks/ko-CYV9QuYs.js.map +1 -0
- package/dist/chunks/nl-BvkB900D.js +141 -0
- package/dist/chunks/nl-BvkB900D.js.map +1 -0
- package/dist/chunks/nl-CAd6_xlm.js +2 -0
- package/dist/chunks/nl-CAd6_xlm.js.map +1 -0
- package/dist/chunks/no-3s9_ormb.js +122 -0
- package/dist/chunks/no-3s9_ormb.js.map +1 -0
- package/dist/chunks/no-CAmz6bz6.js +2 -0
- package/dist/chunks/no-CAmz6bz6.js.map +1 -0
- package/dist/chunks/pl-C9WTGQtb.js +122 -0
- package/dist/chunks/pl-C9WTGQtb.js.map +1 -0
- package/dist/chunks/pl-DqUSTCaF.js +2 -0
- package/dist/chunks/pl-DqUSTCaF.js.map +1 -0
- package/dist/chunks/port-name-CF4WQQ3-.js +2 -0
- package/dist/chunks/port-name-CF4WQQ3-.js.map +1 -0
- package/dist/chunks/port-name-ervLBWAQ.js +6 -0
- package/dist/chunks/port-name-ervLBWAQ.js.map +1 -0
- package/dist/chunks/pt-8ARZnH0_.js +2 -0
- package/dist/chunks/pt-8ARZnH0_.js.map +1 -0
- package/dist/chunks/pt-uFVUv_Op.js +141 -0
- package/dist/chunks/pt-uFVUv_Op.js.map +1 -0
- package/dist/chunks/ro-BrqQ8Au-.js +122 -0
- package/dist/chunks/ro-BrqQ8Au-.js.map +1 -0
- package/dist/chunks/ro-D-NMbp2F.js +2 -0
- package/dist/chunks/ro-D-NMbp2F.js.map +1 -0
- package/dist/chunks/ru-8gbHPh0g.js +2 -0
- package/dist/chunks/ru-8gbHPh0g.js.map +1 -0
- package/dist/chunks/ru-DK594dA8.js +144 -0
- package/dist/chunks/ru-DK594dA8.js.map +1 -0
- package/dist/chunks/sv-CHNH8-mq.js +122 -0
- package/dist/chunks/sv-CHNH8-mq.js.map +1 -0
- package/dist/chunks/sv-D8a8hmx9.js +2 -0
- package/dist/chunks/sv-D8a8hmx9.js.map +1 -0
- package/dist/chunks/th-DfjUK0Y7.js +2 -0
- package/dist/chunks/th-DfjUK0Y7.js.map +1 -0
- package/dist/chunks/th-l24Pm5q-.js +126 -0
- package/dist/chunks/th-l24Pm5q-.js.map +1 -0
- package/dist/chunks/tr-ADpigSY5.js +122 -0
- package/dist/chunks/tr-ADpigSY5.js.map +1 -0
- package/dist/chunks/tr-BdBpz4tL.js +2 -0
- package/dist/chunks/tr-BdBpz4tL.js.map +1 -0
- package/dist/chunks/uk-CGqo4jek.js +144 -0
- package/dist/chunks/uk-CGqo4jek.js.map +1 -0
- package/dist/chunks/uk-Cx1zv1ao.js +2 -0
- package/dist/chunks/uk-Cx1zv1ao.js.map +1 -0
- package/dist/chunks/vi-Dk9bTu6f.js +122 -0
- package/dist/chunks/vi-Dk9bTu6f.js.map +1 -0
- package/dist/chunks/vi-oe2dW21I.js +2 -0
- package/dist/chunks/vi-oe2dW21I.js.map +1 -0
- package/dist/chunks/zh-CwczPMPp.js +2 -0
- package/dist/chunks/zh-CwczPMPp.js.map +1 -0
- package/dist/chunks/zh-LDkEV2D9.js +145 -0
- package/dist/chunks/zh-LDkEV2D9.js.map +1 -0
- package/dist/content/PaywallUI.d.ts +1 -1
- package/dist/content/RemoteAuthClient.d.ts +8 -4
- package/dist/content/RemoteAuthClient.d.ts.map +1 -1
- package/dist/content/RemoteAuthClient.test-d.d.ts +2 -0
- package/dist/content/RemoteAuthClient.test-d.d.ts.map +1 -0
- package/dist/content/RemoteBillingClient.d.ts +36 -3
- package/dist/content/RemoteBillingClient.d.ts.map +1 -1
- package/dist/content/RemoteBillingClient.test-d.d.ts +2 -0
- package/dist/content/RemoteBillingClient.test-d.d.ts.map +1 -0
- package/dist/content/RemoteTrialStore.d.ts +2 -2
- package/dist/content.cjs +3 -3
- package/dist/content.cjs.map +1 -1
- package/dist/content.js +2441 -1059
- package/dist/content.js.map +1 -1
- package/dist/offscreen/server.d.ts +3 -3
- package/dist/offscreen/server.d.ts.map +1 -1
- package/dist/offscreen.cjs +1 -1
- package/dist/offscreen.cjs.map +1 -1
- package/dist/offscreen.js +18 -15
- package/dist/offscreen.js.map +1 -1
- package/dist/shared/messages.d.ts +28 -5
- package/dist/shared/messages.d.ts.map +1 -1
- package/dist/shared/port-name.d.ts +1 -0
- package/dist/shared/port-name.d.ts.map +1 -1
- package/dist/shared/protocol.d.ts +1 -1
- package/dist/shared/protocol.d.ts.map +1 -1
- package/dist/sw.cjs +1 -1
- package/dist/sw.cjs.map +1 -1
- package/dist/sw.js +14 -14
- package/dist/sw.js.map +1 -1
- package/package.json +39 -21
- package/dist/chunks/chrome-port-DPFUj1MP.js.map +0 -1
- package/dist/chunks/chrome-port-MoMohiHB.js +0 -2
- package/dist/chunks/chrome-port-MoMohiHB.js.map +0 -1
- package/dist/chunks/port-name-BPfQKtdb.js +0 -5
- package/dist/chunks/port-name-BPfQKtdb.js.map +0 -1
- package/dist/chunks/port-name-qwB109u9.js +0 -2
- package/dist/chunks/port-name-qwB109u9.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chrome-port-MoMohiHB.js","sources":["../../../sdk/src/core/types.ts","../../../sdk/src/core/api.ts","../../../sdk/src/core/storage.ts","../../../sdk/src/core/pkce.ts","../../../sdk/src/core/auth.ts","../../../sdk/src/core/ApiGatewayClient.ts","../../../sdk/src/core/BillingClient.ts","../../../sdk/src/core/EventTracker.ts","../../../sdk/src/core/trial/LocalTrialStore.ts","../../../sdk/src/core/trial/ServerTrialStore.ts","../../../sdk/src/core/trial/index.ts","../../src/shared/protocol.ts","../../src/shared/errors.ts","../../src/shared/chrome-port.ts"],"sourcesContent":["export interface Identity {\n email?: string;\n userId?: string;\n anonymousId?: string;\n}\n\nexport interface PaywallPrice {\n id: string;\n currency: string;\n amount: number;\n interval: 'month' | 'year' | 'week' | 'day' | 'lifetime' | null;\n interval_count: number | null;\n trial_days: number | null;\n label?: string | null;\n description?: string | null;\n local?: { currency: string; amount: number } | null;\n}\n\nexport interface PaywallOffer {\n id: string;\n discount_percent: number | null;\n expires_at: string | null;\n price_id: string | null;\n label?: string | null;\n}\n\nexport interface PaywallSettings {\n id: string;\n name: string;\n brand_color?: string | null;\n custom_css?: string | null;\n locale_default?: string | null;\n runtime_mode?: 'client' | 'hybrid' | 'server' | 'client-native' | 'hybrid-native';\n /** true, если эквайринг пейвола в test-mode — SDK рисует TEST MODE бейдж. */\n is_test_mode?: boolean;\n /** Auth-flow относительно checkout. `guest` (default) — без auth перед оплатой;\n * `preauth` — клик по cta_button=checkout сначала открывает AuthPanel-gate,\n * после signIn auto-resume исходного createCheckout. Поле общее с legacy v2. */\n checkout_mode?: 'guest' | 'preauth';\n /** OAuth-провайдеры для preauth-gate в порядке отображения. Бэк сейчас отдаёт\n * фиксированный список (google + apple); если поле не задано — gate рисует\n * только email-форму. Не путать с `block.providers` у inline-блока auth_panel. */\n auth_providers?: Array<'google' | 'apple' | 'github' | 'facebook'>;\n /** Разрешён ли вход без email — анонимный юзер. `paywall.signInAnonymously()`\n * падает с code='anonymous_disabled', если флаг = false. Поле дублирует\n * `paywall_settings.allow_anonymous` из БД, то же что используется в legacy\n * v2 (PayWallIframeOpener.tsx). Защита от abuse — на стороне сервера (Supabase\n * rate-limit per real-IP + CF Bot Fight Mode), capтча в SDK не используется. */\n allow_anonymous?: boolean;\n /** Можно ли закрыть модалку (крестик, клик по overlay, ESC). По умолчанию true.\n * false — модалка показывается до успешной покупки или явного host-close().\n * v2-аналог `allow_close`. */\n allow_close?: boolean;\n /** Авто-подгонка размера шрифта heading-блока, чтобы заголовок влезал в 2\n * строки. v2-аналог `title_auto_fit`. По умолчанию false. */\n title_auto_fit?: boolean;\n /** URL, куда редиректить вкладку после успешной покупки (server-confirmed\n * через UserWatcher). null/undefined — остаёмся на месте, показываем\n * PurchaseSuccessView. v2-аналог `success_redirect_url`. */\n success_redirect_url?: string | null;\n /** URL \"Вернуться в магазин\" — пробрасывается в createCheckout как `shopUrl`\n * для Stripe/Paddle страницы оплаты. v2-аналог `checkout_shop_url`. */\n checkout_shop_url?: string | null;\n /** Имя продукта на странице оплаты Stripe/Paddle (line_item.name). Бэк\n * использует при создании checkout-сессии. v2-аналог `checkout_product_name`. */\n checkout_product_name?: string | null;\n /** Конфиг pre-paywall триала (паывол не показывается, пока триал активен).\n * null/undefined — триал отключён, `paywall.open()` сразу открывает модалку.\n * v2-аналог пары `trial` + `trial_payload` в paywall_settings. Не путать с\n * card-trial (PaywallPrice.trial_days) — это автосписание после оплаты. */\n trial?: TrialConfig | null;\n /** Server-computed targeting-gate: матчится ли текущий юзер (страна/девайс)\n * под настройки таргетинга пейвола, плюс общий on/off-флаг. SDK перед open()\n * читает `visible`: false → эмитит `visibility_blocked` и не монтирует\n * модалку. country/tier выдаются всегда — host'ы используют для аналитики.\n * v2-аналог `visibilityEnabledAndTargetingMatch` + `detectInvisible` в\n * PaywallClient.tsx + StateService. */\n visibility?: VisibilityStatus;\n}\n\nexport interface VisibilityStatus {\n /** true — паывол можно открывать. false — какой-то таргетинг не сошёлся,\n * смотри `reason`. */\n visible: boolean;\n /** Почему `visible=false`. null когда `visible=true`.\n * - `disabled` — владелец выключил visibility-флаг.\n * - `country_not_match` — страна юзера не в whitelist (countries_tier +\n * extra_countries).\n * - `device_not_match` — extension-канал (device_target=true), юзер не на\n * macOS. Имеет приоритет над country, потому что в этом канале device —\n * главное условие.\n */\n reason: 'country_not_match' | 'device_not_match' | 'disabled' | null;\n /** ISO-код страны юзера (по IP). null — не удалось определить. */\n country: string | null;\n /** Тир страны 1/2/3 (см. legacy `new_country_code_to_tier`). null — страна\n * не определилась. Все unmapped страны → 3. */\n tier: 1 | 2 | 3 | null;\n}\n\nexport interface TrialConfig {\n /** `time` — паывол скрыт N часов после первого open(); `opens` — N первых\n * open() закрываются молча, N+1-й уже показывает паывол. */\n mode: 'time' | 'opens';\n /** Часы для `time`, количество открытий для `opens`. */\n payload: number;\n /** Где живёт состояние триала. `client` — localStorage (default, мгновенно,\n * юзер может сбросить очисткой storage). `server` — серверный endpoint\n * (сейчас стаб; включится, когда будет серверный handler). */\n storage: 'client' | 'server';\n}\n\n/** Статус триала на момент `paywall.open()`. SDK эмитит в payload событий\n * `trial_blocked`, и возвращает синхронно из `paywall.getTrialStatus()`. */\nexport type TrialStatus =\n | { mode: 'none'; blocked: false }\n | TimeTrialStatus\n | OpensTrialStatus;\n\nexport interface TimeTrialStatus {\n mode: 'time';\n /** true — триал ещё активен, паывол не показывается. */\n blocked: boolean;\n /** Unix ms первого `open()`. null — триал ещё не стартовал. */\n startedAt: number | null;\n /** Unix ms окончания триала. null — триал ещё не стартовал. */\n expiresAt: number | null;\n /** Сколько ещё ms триал активен. 0 — истёк или не активен. */\n remainingMs: number;\n /** Полная длина триала в ms (payload часов × 3_600_000). */\n totalMs: number;\n}\n\nexport interface OpensTrialStatus {\n mode: 'opens';\n /** true — триал ещё активен, паывол не показывается. */\n blocked: boolean;\n /** Сколько ещё «бесплатных» открытий осталось. 0 — триал истёк. */\n remainingActions: number;\n /** Полное число «бесплатных» открытий (payload). */\n totalActions: number;\n}\n\nexport type LayoutBlock =\n | { type: 'heading'; text: string; level?: 1 | 2 | 3 }\n | { type: 'text'; text: string }\n | {\n type: 'price_grid';\n priceIds?: string[];\n /** Раскладка карточек цен. `vertical` (default) — стек сверху вниз;\n * `horizontal` — ряд side-by-side. v2-аналог `view: 'default' | 'telegram'`. */\n view?: 'vertical' | 'horizontal';\n /** ID цены, которая помечается лейблом «популярный план». v2-аналог\n * пары `price_label_id` + `price_label`. */\n popular_price_id?: string;\n /** Текст лейбла «популярный план». По умолчанию \"Most popular\".\n * v2-аналог `price_label_text`. Локализация — через bootstrap.locales. */\n popular_label?: string;\n }\n | { type: 'cta_button'; label: string; action: 'checkout' | 'close'; priceId?: string }\n | {\n /** Footer-блок под cta_button: залогинен — рисует \"Signed in as <email> | Sign out\",\n * иначе — кнопку \"Restore purchases\", которая открывает auth-gate без pendingCheckout\n * (после signIn gate просто схлопывается, юзер видит свой signed-in state). */\n type: 'current_session';\n }\n | {\n type: 'auth_panel';\n /** OAuth-провайдеры в порядке отображения. Пусто/опущено — только email-форма. */\n providers?: Array<'google' | 'apple' | 'github' | 'facebook'>;\n /** Показывать toggle \"Sign up\". По умолчанию true. */\n allow_signup?: boolean;\n /** Показывать ссылку \"Forgot password?\". По умолчанию true. */\n allow_password_reset?: boolean;\n /** Скрывать панель, если юзер уже залогинен. По умолчанию true.\n * false — показываем \"Signed in as ... [Sign out]\" даже после логина. */\n hide_when_authenticated?: boolean;\n /** Заголовок над формой (h2). Если опущен — заголовок не рендерится. */\n heading?: string;\n }\n | {\n /** Список фич/преимуществ продукта. v2-аналог `features_list` + `features_view`.\n * До 5 элементов — рендерим как чек-лист с заголовком и описанием. */\n type: 'features_list';\n items: Array<{ id: string; name: string; desc?: string }>;\n }\n | {\n /** Информационный список «что включено в выбранный план» — рендерится\n * под price_grid, без интерактивности. v2-аналог `tokenization` +\n * `tokenization_queries`. Для каждого query показываем count,\n * умноженный на множитель интервала выбранной цены (`week=0.25`,\n * `month=1`, `year=12`) — т.е. count в БД хранится как месячная\n * норма. Заголовок реактивно отражает текущий interval. */\n type: 'tokenization_gate';\n queries: Array<{ id: string; name: string; desc: string; count: number }>;\n };\n\nexport interface Layout {\n type: 'modal';\n blocks: LayoutBlock[];\n}\n\n/** Локализационные оверрайды для одного языка. Накатываются поверх дефолтного\n * layout/prices при матче `navigator.language` ↔ ключа в `bootstrap.locales`.\n * v2-аналог поля `translations` JSON в paywall_settings. */\nexport interface LocaleOverrides {\n /** Полная замена layout для языка. Если опущен — берётся дефолтный\n * bootstrap.layout. */\n layout?: Layout;\n /** Точечные оверрайды текстовых полей цен. Ключ — price.id, значения\n * накатываются на label/description. */\n prices?: Record<string, { label?: string; description?: string }>;\n}\n\n/** Снимок language-resolution для синхронизации i18n host-приложения с тем, что\n * показывает пейвол. Возвращается из `BillingClient.getUserLanguage()` /\n * `PaywallUI.getUserLanguage()`. */\nexport interface UserLanguageInfo {\n /** Best-guess BCP-47 тэг для host'а. Приоритет: `applied` → `browserLanguage`\n * → `countryLanguage`. null — bootstrap ещё не загружен и navigator\n * недоступен (например, ранний вызов в service worker). */\n tag: string | null;\n /** Ключ из `bootstrap.locales`, который SDK фактически применил к\n * layout/prices. null = match'а не было, рендерится база из layout/prices\n * без оверрайдов. */\n applied: string | null;\n /** `navigator.language` — что репортит браузер. null в окружениях без\n * navigator (service worker до пропатчивания, Node). */\n browserLanguage: string | null;\n /** Server-resolved язык по стране юзера (IP). Берётся из\n * `bootstrap.settings.locale_default` — AT→de, RU→ru, LV→en, и т.д.\n * null — bootstrap ещё не загружен или сервер не отдал поле. */\n countryLanguage: string | null;\n}\n\nexport interface PaywallUserPurchase {\n id: string;\n status: string | null;\n current_period_end: string | null;\n cancel_at_period_end: boolean | null;\n}\n\n/** Rich-shape от `/api/v1/paywall/[id]/user` для customer-portal UX (cancel,\n * renew, история платежей). В отличие от `PaywallUserPurchase` (которая\n * идёт из `/user-state` и имеет минимум для access-gate'а), этот shape\n * включает цену/валюту/discount — чтобы host мог нарисовать список подписок\n * как в legacy customer portal'е. */\nexport interface PaywallPurchaseDetailed {\n id: string;\n status: string | null;\n cancel_at: string | null;\n cancel_at_period_end: boolean;\n canceled_at: string | null;\n created: string;\n ended_at: string | null;\n current_period_end: string | null;\n current_period_start: string | null;\n /** Цена в minor units (центах). Для legacy совместимости — sometimes из\n * `paywall_internal_prices.unit_amount * 100`, иногда из local_amount. */\n unit_amount: number;\n currency: string;\n interval: string | null;\n /** Скидка в процентах от offer (если был применён). undefined — без offer'а. */\n discount?: number;\n}\n\nexport interface PaywallUser {\n /** Главный флаг для большинства интеграций. true, если есть активная подписка\n * ИЛИ оплаченный lifetime ИЛИ активный trial. */\n has_active_subscription: boolean;\n purchases: PaywallUserPurchase[];\n trial: { started_at: string | null; expires_at: string | null } | null;\n}\n\nexport interface PaywallBootstrap {\n settings: PaywallSettings;\n prices: PaywallPrice[];\n offers: PaywallOffer[];\n layout?: Layout;\n /** Snapshot user-state на момент bootstrap'а. Без identity (гость) — всё пусто.\n * Дальше обновляется через BillingClient.getUser() / PaywallUI.onUserChange. */\n user?: PaywallUser;\n /** Локализационные оверрайды по BCP-47 кодам (`en`, `en-US`, `ru`, ...).\n * BillingClient.bootstrap() матчит `navigator.language` с fallback на\n * `settings.locale_default` и применяет оверрайды поверх layout/prices. */\n locales?: Record<string, LocaleOverrides>;\n /** Stable content-hash структурной части bootstrap'а (без user). SDK\n * персистит payload в StorageAdapter и шлёт `?if_version=<v>` на\n * ревалидации — бэк отвечает `{unchanged:true, version, user}` без\n * полного payload, если version совпала. Optional для совместимости\n * с старыми бэками. */\n version?: string;\n}\n\nexport type Acquiring = 'stripe' | 'paddle' | 'chargebee' | 'overpay' | 'freemius';\n\nexport interface CheckoutResult {\n url: string;\n sessionId?: string;\n /** Платёжный процессор, к которому ушёл checkout. Полезно для аналитики\n * конверсии по эквайрингам (host может ветвить UX по acquiring). */\n acquiring?: Acquiring;\n}\n\nexport class PaywallError extends Error {\n readonly code: string;\n readonly status?: number;\n readonly cause?: unknown;\n\n constructor(code: string, message: string, opts: { status?: number; cause?: unknown } = {}) {\n super(message);\n this.name = 'PaywallError';\n this.code = code;\n this.status = opts.status;\n this.cause = opts.cause;\n }\n}\n\n/** Балансы AI-провайдеров пейвола: один элемент на `query_type` из\n * `paywall_settings.tokenization_queries`. count = доступно вызовов. */\nexport interface Balance {\n type: string;\n count: number;\n}\n\n/** 402 от api-gateway: квота закончилась. UI ловит и открывает paywall;\n * headless caller — обрабатывает сам. balances/queryType/currentBalance —\n * то же, что отдаёт бэк в `details`. */\nexport class QuotaExceededError extends PaywallError {\n readonly balances: Balance[];\n readonly queryType: string;\n readonly currentBalance: Balance | null;\n\n constructor(input: {\n balances: Balance[];\n queryType: string;\n currentBalance: Balance | null;\n message?: string;\n }) {\n super('not_enough_queries', input.message ?? 'Not enough queries', {\n status: 402\n });\n this.name = 'QuotaExceededError';\n this.balances = input.balances;\n this.queryType = input.queryType;\n this.currentBalance = input.currentBalance;\n }\n}\n","import { PaywallError } from './types';\n\nexport const SDK_VERSION = '3.0.0-alpha.0';\n\nexport interface ApiClientOptions {\n apiOrigin: string;\n paywallId: string;\n getAuthToken?: () => string | null | Promise<string | null>;\n capabilities?: string[];\n fetch?: typeof fetch;\n}\n\nexport class ApiClient {\n private opts: ApiClientOptions;\n\n constructor(opts: ApiClientOptions) {\n this.opts = opts;\n }\n\n async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n const url = new URL(path, this.opts.apiOrigin).toString();\n const fetchImpl = this.opts.fetch ?? fetch;\n\n const headers = new Headers(init.headers);\n headers.set('Accept', 'application/json');\n headers.set('X-SDK-Version', SDK_VERSION);\n headers.set('X-Paywall-Id', this.opts.paywallId);\n\n if (this.opts.capabilities?.length) {\n headers.set('X-SDK-Capabilities', this.opts.capabilities.join(','));\n }\n\n const token = await this.opts.getAuthToken?.();\n if (token) headers.set('Authorization', `Bearer ${token}`);\n\n // FormData/Blob/URLSearchParams требуют, чтобы браузер сам назначил\n // Content-Type (multipart с boundary, x-www-form-urlencoded). Дефолт\n // application/json применяем только для обычных body (JSON-строки).\n const isFormBody =\n typeof FormData !== 'undefined' && init.body instanceof FormData;\n if (init.body && !headers.has('Content-Type') && !isFormBody) {\n headers.set('Content-Type', 'application/json');\n }\n\n let response: Response;\n try {\n response = await fetchImpl(url, {\n ...init,\n headers,\n credentials: 'omit'\n });\n } catch (cause) {\n // AbortError — отдельный код, чтобы host мог отличить «юзер закрыл\n // модалку» от реальной сетевой проблемы. DOMException не всегда\n // instanceof Error в edge runtimes (Cloudflare Workers); проверяем\n // через duck-typed `.name`.\n const name =\n cause && typeof cause === 'object' && 'name' in cause\n ? (cause as { name: unknown }).name\n : undefined;\n if (name === 'AbortError') {\n throw new PaywallError('aborted', 'Request aborted', { cause });\n }\n throw new PaywallError('network_error', 'Network request failed', { cause });\n }\n\n const ct = response.headers.get('content-type') ?? '';\n const isJson = ct.includes('application/json');\n const payload: unknown = isJson ? await response.json().catch((): null => null) : null;\n\n if (!response.ok) {\n const code =\n (payload && typeof payload === 'object' && 'code' in payload && String(payload.code)) ||\n `http_${response.status}`;\n const message =\n (payload && typeof payload === 'object' && 'message' in payload && String(payload.message)) ||\n response.statusText ||\n 'Request failed';\n // payload в cause — выше по стеку (BillingClient/AuthClient) могут читать\n // структурные поля из тела ошибки (например `hasActivePurchase: true`\n // от /start-checkout 409) и менять обработку.\n throw new PaywallError(code, message, { status: response.status, cause: payload });\n }\n\n return payload as T;\n }\n}\n","export interface StorageAdapter {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n /**\n * Опционально: подписка на изменение `key` извне (другая вкладка /\n * background-контекст extension'а). Возвращает unsubscribe.\n *\n * Контракт callback'а:\n * - `value` — новое значение (string) или `null` (удалили / нет).\n * - Вызывается ТОЛЬКО для cross-context изменений; собственный setItem\n * / removeItem callback дёргать НЕ обязан (для chrome.storage.onChanged\n * он дёрнется и так — потребитель обязан фильтровать сам).\n *\n * Адаптеры без поддержки (memory) опускают это поле, потребитель должен\n * проверять `typeof storage.watch === 'function'`.\n */\n watch?(key: string, cb: (value: string | null) => void): () => void;\n}\n\ninterface ChromeStorageChange {\n oldValue?: unknown;\n newValue?: unknown;\n}\n\ndeclare const chrome: {\n storage?: {\n local?: {\n get(keys: string[], cb: (items: Record<string, unknown>) => void): void;\n set(items: Record<string, unknown>, cb?: () => void): void;\n remove(keys: string[], cb?: () => void): void;\n };\n onChanged?: {\n addListener(\n cb: (changes: Record<string, ChromeStorageChange>, area: string) => void\n ): void;\n removeListener(\n cb: (changes: Record<string, ChromeStorageChange>, area: string) => void\n ): void;\n };\n };\n runtime?: { id?: string };\n} | undefined;\n\nfunction hasChromeStorage(): boolean {\n return (\n typeof chrome !== 'undefined' &&\n !!chrome?.storage?.local &&\n !!chrome?.runtime?.id\n );\n}\n\nconst chromeLocal: StorageAdapter = {\n getItem(key) {\n return new Promise((resolve) => {\n chrome!.storage!.local!.get([key], (items) => {\n const v = items[key];\n resolve(typeof v === 'string' ? v : null);\n });\n });\n },\n setItem(key, value) {\n return new Promise((resolve) => {\n chrome!.storage!.local!.set({ [key]: value }, () => resolve());\n });\n },\n removeItem(key) {\n return new Promise((resolve) => {\n chrome!.storage!.local!.remove([key], () => resolve());\n });\n },\n watch(key, cb) {\n const onChanged = chrome?.storage?.onChanged;\n if (!onChanged) return () => {};\n // chrome.storage.onChanged fires across все контексты расширения\n // (popup / background / options / content script с storage-permission).\n // Для popup'а и background'а подписка идёт на один и тот же event\n // emitter; defence через own-write filter — на стороне consumer'а\n // (AuthClient сравнит content hash).\n const handler = (\n changes: Record<string, ChromeStorageChange>,\n area: string\n ) => {\n if (area !== 'local') return;\n const change = changes[key];\n if (!change) return;\n cb(typeof change.newValue === 'string' ? change.newValue : null);\n };\n onChanged.addListener(handler);\n return () => onChanged.removeListener(handler);\n }\n};\n\nconst webLocal: StorageAdapter = {\n async getItem(key) {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n async setItem(key, value) {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n /* quota / disabled */\n }\n },\n async removeItem(key) {\n try {\n window.localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n },\n watch(key, cb) {\n if (typeof window === 'undefined') return () => {};\n // Native `storage` event фаерится только в ДРУГИХ вкладках того же\n // origin'а — собственная вкладка свой setItem не получает (это и нужно\n // для cross-tab sync, без петель).\n const handler = (e: StorageEvent) => {\n if (e.storageArea !== window.localStorage) return;\n if (e.key !== key) return;\n cb(e.newValue);\n };\n window.addEventListener('storage', handler);\n return () => window.removeEventListener('storage', handler);\n }\n};\n\nconst memoryMap = new Map<string, string>();\nconst memoryLocal: StorageAdapter = {\n async getItem(key) {\n return memoryMap.get(key) ?? null;\n },\n async setItem(key, value) {\n memoryMap.set(key, value);\n },\n async removeItem(key) {\n memoryMap.delete(key);\n }\n};\n\nexport function createStorage(override?: StorageAdapter): StorageAdapter {\n if (override) return override;\n if (hasChromeStorage()) return chromeLocal;\n if (typeof window !== 'undefined' && 'localStorage' in window) return webLocal;\n return memoryLocal;\n}\n\nexport const STORAGE_KEYS = {\n visitorId: 'pw-visitor-id',\n lastLoginMethod: (paywallId: string) => `pw-${paywallId}-last-login-method`,\n lastLoginEmail: (paywallId: string) => `pw-${paywallId}-last-login-email`,\n // last-known PaywallUser. Используется как offline-fallback на старте, пока\n // первый getUser() не вернётся. Ключ зависит от paywallId+identity hash —\n // переключение identity не должно отдавать чужой user.\n userState: (paywallId: string, identityKey: string) =>\n `pw-${paywallId}-${identityKey}-user-v1`,\n // Persisted auth bundle (access_token, refresh_token, expires_at, user) для\n // одного пейвола. Ключ привязан к paywallId — мульти-пейвольное приложение\n // не пересекает сессии. Bump '-v1' на breaking shape change.\n authSession: (paywallId: string) => `pw-${paywallId}-auth-v1`,\n // Refresh-token последнего анонимного юзера. Хранится отдельно от authSession,\n // потому что должен пережить signOut: после signOut() юзер может опять\n // зайти как тот же аноним — без капчи, через этот токен. signIn другим\n // методом (email/oauth) тоже его не трогает. Чистится только явным\n // signOut({forgetAnonymous: true}) или 401 от refresh-эндпоинта (значит\n // токен отозван, дальше держать бессмысленно).\n anonRefreshToken: (paywallId: string) => `pw-${paywallId}-anon-rt-v1`,\n // Persisted bootstrap (settings/prices/offers/layout/locales/version) для\n // stale-while-revalidate. Не зависит от identity — layout одинаков для всех\n // юзеров одного пейвола; user-state живёт отдельно под `userState(...)`.\n // Bump '-v1' на breaking shape change.\n bootstrap: (paywallId: string) => `pw-${paywallId}-bootstrap-v1`,\n // Persisted balances (AI-провайдеры × tokenization_queries). Identity-bound,\n // т.к. balance считается per-Bearer-юзеру; при re-login ключ меняется и\n // чужие balances не видны. Меняются после оплаты (бэк) и API-вызовов\n // (оптимистично через `decrementBalanceLocal`).\n balances: (paywallId: string, identityKey: string) =>\n `pw-${paywallId}-${identityKey}-balances-v1`\n};\n\n// UUID v4 — stable visitor identifier для аналитики. Не PII, не привязан к\n// identity/email. Используется в EventTracker. Fallback на Math.random нужен\n// для старых рантаймов без crypto.randomUUID (редко, но бывает в e2e-моках).\nexport function generateVisitorId(): string {\n const c = typeof globalThis !== 'undefined' ? (globalThis as { crypto?: Crypto }).crypto : undefined;\n if (c && typeof c.randomUUID === 'function') return c.randomUUID();\n\n const bytes = new Uint8Array(16);\n if (c && typeof c.getRandomValues === 'function') {\n c.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n// Резолвит stable visitor_id: читает из storage по ключу STORAGE_KEYS.visitorId,\n// генерит и сохраняет если его там нет. Promise — потому что storage async\n// (chrome.storage.local — callback-based).\nexport async function ensureVisitorId(storage: StorageAdapter): Promise<string> {\n try {\n const existing = await storage.getItem(STORAGE_KEYS.visitorId);\n if (existing && typeof existing === 'string' && existing.length >= 16) return existing;\n } catch {\n /* fall through to generation */\n }\n const id = generateVisitorId();\n try {\n await storage.setItem(STORAGE_KEYS.visitorId, id);\n } catch {\n /* quota / disabled — id всё равно используем в этой сессии */\n }\n return id;\n}\n","// PKCE helper для OAuth-флоу AuthClient. Стандарт RFC 7636:\n// - verifier — random URL-safe string длины 43..128 (мы берём 64).\n// - challenge — base64url(SHA-256(verifier)).\n// SDK хранит verifier в памяти (Map<state, verifier>) до возврата\n// popup'а. Verifier никогда не уходит на бэк до /oauth/exchange.\n\nfunction randomBytes(len: number): Uint8Array {\n const bytes = new Uint8Array(len);\n const c =\n typeof globalThis !== 'undefined'\n ? (globalThis as { crypto?: Crypto }).crypto\n : undefined;\n if (c && typeof c.getRandomValues === 'function') {\n c.getRandomValues(bytes);\n } else {\n // Fallback на Math.random — нужен только для exotic-рантаймов\n // (старые e2e-моки). В extension/web рантайме crypto всегда есть.\n for (let i = 0; i < len; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return bytes;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let bin = '';\n for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function generateCodeVerifier(): string {\n // 64 байта → 86 base64url-символов, попадает в [43, 128].\n return base64url(randomBytes(64));\n}\n\nexport async function deriveCodeChallenge(verifier: string): Promise<string> {\n const enc = new TextEncoder().encode(verifier);\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c?.subtle?.digest) {\n // PKCE без SubtleCrypto не работает: мы не можем посчитать SHA-256\n // детерминированно. В рантаймах SDK 3.0 (web + chrome.extension MV3)\n // SubtleCrypto есть всегда. Если кто-то воткнёт SDK в node без polyfill,\n // нужно быть честным и упасть, а не молча даунгрейдить challenge_method.\n throw new Error('crypto.subtle is required for PKCE');\n }\n const hash = await c.subtle.digest('SHA-256', enc);\n return base64url(new Uint8Array(hash));\n}\n\nexport function generateState(): string {\n return base64url(randomBytes(16));\n}\n","import { ApiClient } from './api';\nimport { createStorage, type StorageAdapter, STORAGE_KEYS } from './storage';\nimport { PaywallError } from './types';\nimport {\n deriveCodeChallenge,\n generateCodeVerifier,\n generateState\n} from './pkce';\n\n// AuthClient — клиент SDK 3.0 для эндпоинтов /api/v1/paywall/[id]/auth/*.\n// Хранит auth-сессию в StorageAdapter (localStorage / chrome.storage.local /\n// memory), отдаёт access_token для Authorization-хедера в `api.ts` через\n// getAccessToken() с lazy refresh, дедупит параллельные refresh'ы, эмитит\n// onAuthChange при login/logout/refresh.\n//\n// Не зависит от BillingClient: AuthClient можно использовать standalone\n// (например, для собственного ui без bundled-пейвола). BillingClient в свою\n// очередь принимает AuthClient опционально и подключает Bearer + auto-sync\n// identity.\n\nconst DEFAULT_API_ORIGIN = 'https://appbox.space';\n// За REFRESH_LEEWAY_MS до expiry начинаем refresh — буфер на сетевую\n// задержку и часовой дрейф клиента. 60s достаточно: GoTrue access живёт 1ч,\n// шанс реально подсунуть истёкший токен в API-запрос ≈ 0.\nconst REFRESH_LEEWAY_MS = 60_000;\n// TTL для pending OAuth-flow между startOAuthFlow и completeOAuthFlow.\n// 10мин — больше чем юзеру нужно прокликать Google/Apple/etc; меньше чем\n// reasonable «отошёл и вернулся».\nconst OAUTH_FLOW_TTL_MS = 10 * 60 * 1000;\n\nexport interface AuthUser {\n id: string;\n /** null для анонимного юзера (signInAnonymously). Для всех остальных flow — заполнен. */\n email: string | null;\n country?: string | null;\n /** true — Supabase anonymous user. UI использует, чтобы решать «sign in» vs\n * «signed in as ...», и чтобы при OAuth-апгрейде звать linkIdentity вместо\n * signInWithOAuth (зеркалит легаси StartAuthPage.tsx). */\n is_anonymous?: boolean;\n}\n\nexport interface AuthSession {\n access_token: string;\n refresh_token: string;\n /** Absolute timestamp в ms (Date.now() сравнимо). null/0 не пишем. */\n expires_at: number;\n user: AuthUser;\n}\n\nexport type SignUpResult =\n | { kind: 'signed_in'; session: AuthSession }\n | { kind: 'confirmation_required'; user: { id: string; email: string } };\n\n/** Результат `upgradeAnonymousToEmail`. `updated` — confirmation off либо\n * прошёл; session.user.email уже обновлён, is_anonymous=false. `confirmation_required` —\n * GoTrue отправил confirmation на новый email; session всё ещё анонимная,\n * юзер должен кликнуть ссылку (после чего может вызвать `auth.refresh()` —\n * токены обновятся с email'ом и is_anonymous=false). */\nexport type UpgradeAnonymousResult =\n | { kind: 'updated'; session: AuthSession }\n | { kind: 'confirmation_required'; email: string };\n\nexport type OtpVerifyType = 'email' | 'recovery' | 'signup' | 'magiclink' | 'invite';\n\nexport type OAuthProvider = 'google' | 'apple' | 'github' | 'facebook';\n\nexport type AuthChangeListener = (session: AuthSession | null) => void;\n\nexport interface AuthClientOptions {\n paywallId: string;\n apiOrigin?: string;\n storage?: StorageAdapter;\n fetch?: typeof fetch;\n // Inject для тестов и для Chrome-extension'ов (там popup можно открыть\n // через chrome.windows.create, а не window.open). По дефолту — window.open.\n openPopup?: (url: string, name: string) => Window | null;\n}\n\ninterface RawTokens {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n expires_at: number | null;\n token_type: 'bearer';\n}\n\nexport class AuthClient {\n readonly paywallId: string;\n readonly apiOrigin: string;\n private storage: StorageAdapter;\n private api: ApiClient;\n private openPopup: (url: string, name: string) => Window | null;\n\n private session: AuthSession | null = null;\n private hydrated: Promise<void>;\n private inflightRefresh: Promise<AuthSession | null> | null = null;\n /** Дедупликация параллельных signInAnonymously: два click'а на «Войти как\n * гость» должны попасть в одного юзера, не плодить двух (двойная капча +\n * второй /signup создал бы вторую запись с потерянным trial-балансом). */\n private inflightAnonSignin: Promise<AuthSession> | null = null;\n private listeners = new Set<AuthChangeListener>();\n private storageUnwatch: (() => void) | null = null;\n private destroyed = false;\n /** Pending OAuth flows: state → {verifier, userMeta, startedAt}. Между\n * startOAuthFlow и completeOAuthFlow. GC'атся через OAUTH_FLOW_TTL_MS. */\n private oauthFlows = new Map<\n string,\n { verifier: string; userMeta: Record<string, string> | undefined; startedAt: number }\n >();\n\n constructor(opts: AuthClientOptions) {\n if (!opts.paywallId) {\n throw new PaywallError('invalid_config', 'paywallId is required');\n }\n this.paywallId = opts.paywallId;\n this.apiOrigin = opts.apiOrigin ?? DEFAULT_API_ORIGIN;\n this.storage = createStorage(opts.storage);\n // Без getAuthToken — auth-эндпоинты либо публичные, либо мы кладём\n // Authorization вручную в headers (signOut). ApiClient не перетрёт его,\n // если getAuthToken отсутствует.\n this.api = new ApiClient({\n apiOrigin: this.apiOrigin,\n paywallId: opts.paywallId,\n fetch: opts.fetch\n });\n this.openPopup =\n opts.openPopup ??\n ((url, name) => {\n if (typeof window === 'undefined') return null;\n return window.open(url, name, 'width=480,height=640,popup=yes');\n });\n this.hydrated = this.hydrate();\n this.startStorageWatch();\n }\n\n /**\n * Подписывается на изменения session-ключа в storage из других контекстов:\n * - Chrome Extension: `chrome.storage.onChanged` шарится popup ↔ background ↔\n * options ↔ content script. Логин в одном контексте → остальные сразу\n * эмитят onAuthChange и в getAccessToken отдают свежий Bearer.\n * - Web: `window.storage` event фаерится в ДРУГИХ вкладках того же origin'а\n * (своя вкладка свой setItem не получает — петель нет).\n *\n * Loop-guard: сравниваем content по полям session перед applySession, чтобы\n * не фрить лишних onAuthChange при идентичной перезаписи. Вызовы из других\n * контекстов с тем же содержимым (пересохранение) — no-op.\n */\n private startStorageWatch(): void {\n if (typeof this.storage.watch !== 'function') return;\n this.storageUnwatch = this.storage.watch(this.storageKey(), (raw) => {\n void this.applyExternalSession(raw);\n });\n }\n\n private async applyExternalSession(raw: string | null): Promise<void> {\n if (this.destroyed) return;\n // Дожидаемся первичной hydrate'а — иначе можем перетереть session, которая\n // ещё не успела загрузиться при construction'е.\n await this.hydrated;\n if (this.destroyed) return;\n if (raw == null) {\n // Удалили в другом контексте → logout всем подписчикам.\n if (this.session) this.setSession(null, { skipPersist: true });\n return;\n }\n try {\n const parsed = JSON.parse(raw) as AuthSession | null;\n if (\n !parsed ||\n typeof parsed.access_token !== 'string' ||\n typeof parsed.refresh_token !== 'string' ||\n typeof parsed.expires_at !== 'number' ||\n !parsed.user\n ) {\n return;\n }\n this.setSession(parsed, { skipPersist: true });\n } catch {\n /* corrupted payload — игнорируем */\n }\n }\n\n /**\n * Promise гидратации session из storage. До его resolve getCachedSession()\n * может ещё вернуть null. getAccessToken/refresh/signOut/sign* awaitят его\n * сами, наружу выставляем для UI'я, чтобы он мог дождаться initial state\n * прежде чем рисовать «logged-out» вспышку.\n */\n ready(): Promise<void> {\n return this.hydrated;\n }\n\n /** Sync snapshot без сетевых запросов. null = разлогинен или ещё не гидрировались. */\n getCachedSession(): AuthSession | null {\n return this.session;\n }\n\n getCachedUser(): AuthUser | null {\n return this.session?.user ?? null;\n }\n\n /**\n * access_token для Authorization-хедера. Если до expiry < REFRESH_LEEWAY_MS,\n * делает lazy refresh. null = разлогинен или refresh упал на 401 (refresh\n * token revoked) — вызывающему стоит редиректить на логин.\n *\n * Сетевые/5xx ошибки refresh бросаются — текущий access ещё валиден,\n * вызывающий может попробовать запрос с ним; следующий getAccessToken\n * попробует refresh снова.\n */\n async getAccessToken(): Promise<string | null> {\n await this.hydrated;\n if (!this.session) {\n // Race window: другой контекст (popup) залогинился, но storage-watch\n // event ещё не долетел до этого инстанса (chrome.storage.onChanged\n // async). Один лишний storage read для разлогиненного case'а — приемлемая\n // плата за то, что background не отдаёт null когда токен уже есть.\n await this.rehydrateFromStorage();\n if (!this.session) return null;\n }\n if (this.isFresh(this.session)) return this.session.access_token;\n try {\n const refreshed = await this.refresh();\n return refreshed?.access_token ?? null;\n } catch {\n // Сеть упала — отдаём текущий (возможно скоро истечёт, но лучше чем null).\n return this.session?.access_token ?? null;\n }\n }\n\n async signInWithEmail(input: {\n email: string;\n password: string;\n userMeta?: Record<string, string>;\n /** Idempotency-key (UUID) — повторный submit при двойном клике вернёт\n * тот же результат вместо второго запроса в GoTrue. Без передачи\n * inflight-дедупликации нет; SDK не дедуплицирует auth по умолчанию,\n * потому что email/password можно поменять между кликами. */\n idempotencyKey?: string;\n }): Promise<AuthSession> {\n await this.hydrated;\n const visitorId = await this.readVisitorId();\n type Resp = RawTokens & { user: AuthUser };\n const headers: Record<string, string> = {};\n if (input.idempotencyKey) headers['Idempotency-Key'] = input.idempotencyKey;\n const resp = await this.api.request<Resp>(\n `/api/v1/paywall/${this.paywallId}/auth/email/signin`,\n {\n method: 'POST',\n headers: Object.keys(headers).length ? headers : undefined,\n body: JSON.stringify({\n email: input.email,\n password: input.password,\n visitor_id: visitorId,\n user_meta: input.userMeta\n })\n }\n );\n const session = this.toSession(resp, resp.user);\n this.setSession(session);\n return session;\n }\n\n /**\n * Signup. Если в Supabase включён email confirm — сервер возвращает\n * `{status: 'confirmation_required', user}` и НЕ выдаёт токены. В этом\n * случае setSession не зовётся, юзер должен пройти OTP/magic-link\n * (отдельная фича следующего PR).\n */\n async signUp(input: {\n email: string;\n password: string;\n userMeta?: Record<string, string>;\n /** Idempotency-key (UUID). Защита от двойного клика на «Sign Up» —\n * без неё бэк может создать trial-balances и отправить confirmation-email\n * дважды. */\n idempotencyKey?: string;\n }): Promise<SignUpResult> {\n await this.hydrated;\n const visitorId = await this.readVisitorId();\n type Resp =\n | { status: 'confirmation_required'; user: { id: string; email: string } }\n | (RawTokens & { status: 'signed_in'; user: AuthUser });\n const headers: Record<string, string> = {};\n if (input.idempotencyKey) headers['Idempotency-Key'] = input.idempotencyKey;\n const resp = await this.api.request<Resp>(\n `/api/v1/paywall/${this.paywallId}/auth/email/signup`,\n {\n method: 'POST',\n headers: Object.keys(headers).length ? headers : undefined,\n body: JSON.stringify({\n email: input.email,\n password: input.password,\n visitor_id: visitorId,\n user_meta: input.userMeta\n })\n }\n );\n if (resp.status === 'confirmation_required') {\n return { kind: 'confirmation_required', user: resp.user };\n }\n const session = this.toSession(resp, resp.user);\n this.setSession(session);\n return { kind: 'signed_in', session };\n }\n\n /**\n * Повторная отправка confirmation-email после signUp с включённым\n * email-confirm. Использует GoTrue `/resend` type='signup'. Бэк всегда\n * отдаёт ok (anti-enumeration), кроме 429 при rate-limit (~1 раз/мин на\n * email на стороне Supabase). Host обрабатывает 429 показом «подождите\n * минуту»; остальное — как success.\n */\n async resendConfirmation(input: {\n email: string;\n /** Защита от двойного клика. */\n idempotencyKey?: string;\n }): Promise<void> {\n await this.hydrated;\n const headers: Record<string, string> = {};\n if (input.idempotencyKey) headers['Idempotency-Key'] = input.idempotencyKey;\n await this.api.request<{ ok: true }>(\n `/api/v1/paywall/${this.paywallId}/auth/email/resend`,\n {\n method: 'POST',\n headers: Object.keys(headers).length ? headers : undefined,\n body: JSON.stringify({ email: input.email })\n }\n );\n }\n\n /**\n * Email-OTP / signin без password. Шлёт 6-значный код юзеру на email.\n * Anti-enumeration: бэк всегда отдаёт ok, поэтому метод не различает\n * «email не существует» и «отправлено» — следующий шаг (verifyOtp) сам\n * упадёт invalid_otp если юзера нет. Под капотом GoTrue с create_user=true,\n * так что новые юзеры через OTP логинятся за один шаг (отправка → ввод\n * кода → session).\n */\n async sendOtp(input: {\n email: string;\n createUser?: boolean;\n userMeta?: Record<string, unknown>;\n }): Promise<void> {\n await this.hydrated;\n await this.api.request<{ ok: true }>(\n `/api/v1/paywall/${this.paywallId}/auth/otp/send`,\n {\n method: 'POST',\n body: JSON.stringify({\n email: input.email,\n create_user: input.createUser ?? true,\n user_meta: input.userMeta\n })\n }\n );\n }\n\n /**\n * Верификация OTP. type='email' (signin/signup-by-otp) — после успеха\n * setSession и onAuthChange. type='recovery' — после /requestPasswordReset:\n * выдаётся короткоживущий access_token для последующего updatePassword.\n * Мы храним recovery-session так же, как обычную: SDK не различает «можно\n * залогиниться» vs «можно сменить пароль» — это одна и та же session.\n */\n async verifyOtp(input: {\n email: string;\n token: string;\n type?: OtpVerifyType;\n userMeta?: Record<string, string>;\n }): Promise<AuthSession> {\n await this.hydrated;\n const visitorId = await this.readVisitorId();\n type Resp = RawTokens & { user: AuthUser };\n const resp = await this.api.request<Resp>(\n `/api/v1/paywall/${this.paywallId}/auth/otp/verify`,\n {\n method: 'POST',\n body: JSON.stringify({\n email: input.email,\n token: input.token,\n type: input.type ?? 'email',\n visitor_id: visitorId,\n user_meta: input.userMeta\n })\n }\n );\n const session = this.toSession(resp, resp.user);\n this.setSession(session);\n return session;\n }\n\n /**\n * Запрос recovery email. Бэк всегда ok, чтобы не палить enumeration.\n * Юзер вводит код из письма в SDK-ui → verifyOtp({type:'recovery'}) →\n * получает session → updatePassword.\n */\n async requestPasswordReset(input: { email: string }): Promise<void> {\n await this.hydrated;\n await this.api.request<{ ok: true }>(\n `/api/v1/paywall/${this.paywallId}/auth/password/request-reset`,\n {\n method: 'POST',\n body: JSON.stringify({ email: input.email })\n }\n );\n }\n\n /**\n * Меняет пароль текущей session. Работает после verifyOtp({type:'recovery'})\n * (recovery-session) и после обычного логина — оба случая дают валидный\n * access_token. Если session нет — бросаем PaywallError('not_authenticated')\n * до сетевого запроса, чтобы UI не дёргал бэк впустую.\n */\n async updatePassword(input: { password: string }): Promise<void> {\n await this.hydrated;\n const accessToken = await this.getAccessToken();\n if (!accessToken) {\n throw new PaywallError('not_authenticated', 'no active session');\n }\n await this.api.request<{ ok: true; user: { id: string; email: string | null } }>(\n `/api/v1/paywall/${this.paywallId}/auth/password/update`,\n {\n method: 'POST',\n headers: { Authorization: `Bearer ${accessToken}` },\n body: JSON.stringify({ password: input.password })\n }\n );\n }\n\n /**\n * Анонимный signin (Supabase user без email). Лестница попыток:\n *\n * 1. Если уже залогинены анонимно (session.user.is_anonymous === true) —\n * no-op, возвращаем текущую session. Идемпотентно для UI'я, который\n * может звать signInAnonymously() в render-loop'е, не отслеживая state.\n *\n * 2. Resume через сохранённый anon refresh_token (`STORAGE_KEYS.anonRefreshToken`).\n * Если токен есть — пробуем `/auth/refresh` им. Success → setSession,\n * возвращаем юзера ТОГО ЖЕ id что был при предыдущем anon signin'е\n * (обещание из user-фидбека: «если разлогинился из анонимного —\n * логинить в этот же акк»).\n *\n * 3. Иначе → POST /auth/anonymous/signin → setSession + сохраняем\n * refresh_token в anonRefreshToken.\n *\n * `captchaToken` сейчас не требуется — captcha protection в Supabase\n * отключена, защита от per-IP abuse держится на rate-limit'е Supabase'а\n * (30/час per real-IP, см. IP forwarding setup в supabaseAuthRest.ts) +\n * CF Bot Fight Mode на edge. Поле оставлено optional для forward-compat:\n * когда сервер начнёт возвращать challenge_required в риск-сценариях,\n * SDK сможет передать proof-of-something обратно без breaking change.\n *\n * `forceCaptcha: true` пропускает шаги 1-2 и сразу делает /signin (создаёт\n * нового anon-юзера). Используется в switch-account flow. Имя поля исторически\n * остаётся `forceCaptcha`, хотя капчи там больше нет — менять имя ломает\n * host-сигнатуру; смысл «принудительно новая anon-сессия» сохранён.\n *\n * Параллельные вызовы дедуплицируются через `inflightAnonSignin` — два\n * click'а на «Войти как гость» не создадут двух anon-юзеров (два /signup =\n * два user_id, второй trial-баланс улетает в нирвану).\n */\n async signInAnonymously(input: {\n captchaToken?: string;\n userMeta?: Record<string, string>;\n forceCaptcha?: boolean;\n } = {}): Promise<AuthSession> {\n if (this.inflightAnonSignin) return this.inflightAnonSignin;\n\n this.inflightAnonSignin = (async () => {\n await this.hydrated;\n\n // 1. Уже анон — не дёргаем сеть.\n if (\n !input.forceCaptcha &&\n this.session?.user.is_anonymous === true\n ) {\n return this.session;\n }\n\n // 2. Resume через сохранённый refresh_token.\n if (!input.forceCaptcha) {\n const resumed = await this.resumeAnonymous();\n if (resumed) return resumed;\n }\n\n // 3. Fresh signin. captcha_token шлём только если host явно передал\n // (forward-compat для будущего challenge-response механизма).\n const visitorId = await this.readVisitorId();\n type Resp = RawTokens & { user: AuthUser };\n const resp = await this.api.request<Resp>(\n `/api/v1/paywall/${this.paywallId}/auth/anonymous/signin`,\n {\n method: 'POST',\n body: JSON.stringify({\n ...(input.captchaToken ? { captcha_token: input.captchaToken } : {}),\n visitor_id: visitorId,\n user_meta: input.userMeta\n })\n }\n );\n\n // Бэк не выставляет is_anonymous=true в user-объекте? Подстраховка для\n // SDK-side флага: всегда true для этого роута.\n const user: AuthUser = {\n ...resp.user,\n email: resp.user.email ?? null,\n is_anonymous: true\n };\n const session = this.toSession(resp, user);\n this.setSession(session);\n // Persist refresh для будущих resume — в writeAnonRefreshToken,\n // так же `setSession` уже сохранил полную session в authSession-storage.\n await this.writeAnonRefreshToken(session.refresh_token);\n return session;\n })();\n\n try {\n return await this.inflightAnonSignin;\n } finally {\n this.inflightAnonSignin = null;\n }\n }\n\n /**\n * Внутренний resume — пробует /auth/refresh с сохранённым anon refresh_token.\n * Возвращает session при успехе, null если токена нет или он отозван (401).\n * Сетевые ошибки бросает наружу — caller сам решает, ретраить или просить\n * пользователя пройти капчу.\n */\n private async resumeAnonymous(): Promise<AuthSession | null> {\n const rt = await this.readAnonRefreshToken();\n if (!rt) return null;\n try {\n const resp = await this.api.request<RawTokens>(\n `/api/v1/paywall/${this.paywallId}/auth/refresh`,\n { method: 'POST', body: JSON.stringify({ refresh_token: rt }) }\n );\n // /auth/refresh не возвращает user — реконструируем минимально из текущей\n // session (если был anon в storage) или ставим заглушку. Для полного\n // профиля host может позвать BillingClient.getUser().\n const fallbackUser: AuthUser =\n this.session?.user.is_anonymous === true\n ? this.session.user\n : { id: '', email: null, is_anonymous: true };\n const session = this.toSession(resp, fallbackUser);\n this.setSession(session);\n // Rotation: GoTrue выдаёт новый refresh_token, обновляем persisted.\n await this.writeAnonRefreshToken(session.refresh_token);\n return session;\n } catch (e) {\n if (e instanceof PaywallError && e.status === 401) {\n // Токен отозван — чистим, fallthrough в caller'е сделает fresh signin\n // на /auth/anonymous/signin (создаст нового anon-юзера).\n await this.clearAnonRefreshToken();\n return null;\n }\n // Сеть/5xx — не трогаем токен, пусть юзер ретрайит.\n throw e;\n }\n }\n\n /**\n * Анон → email/password upgrade. Сохраняет тот же auth.user.id, балансы\n * и trial-quotas остаются. Поведение зависит от Supabase email-confirm\n * настройки проекта:\n *\n * - Confirmation OFF → backend сразу обновляет email + password в auth.users.\n * Возвращаем `kind: 'updated'`, локально патчим session.user.email +\n * is_anonymous=false (текущий access_token остаётся валидным, перевыдавать\n * не нужно — GoTrue не вращает токены на updateUser).\n *\n * - Confirmation ON → backend отдаёт `confirmation_required`. Текущая\n * session ОСТАЁТСЯ анонимной до клика юзером по confirmation-ссылке.\n * Password применяется сразу (можно дальше логиниться по нему даже до\n * confirm'а). После клика — следующий /auth/refresh подтянет обновлённый\n * is_anonymous=false из JWT (refresh не возвращает user, так что\n * UI может явно подёргать `auth.refresh()` через минуту-другую, либо\n * дождаться lazy-refresh при истечении access).\n *\n * Без активной session бросает `not_authenticated`. Дедупликации нет —\n * двойной submit формы UI должен предотвратить idempotencyKey'ом.\n */\n async upgradeAnonymousToEmail(input: {\n email: string;\n password: string;\n userMeta?: Record<string, string>;\n /** Idempotency-key для защиты от двойного клика. GoTrue PUT /user не\n * идемпотентен сам по себе — повторный submit при двойном клике может\n * вызвать race с email-confirmation (две confirmation-ссылки на тот же\n * адрес). UI должен передать UUID. */\n idempotencyKey?: string;\n }): Promise<UpgradeAnonymousResult> {\n await this.hydrated;\n const accessToken = await this.getAccessToken();\n if (!accessToken) {\n throw new PaywallError('not_authenticated', 'no active session');\n }\n\n type Resp =\n | { status: 'updated'; user: AuthUser }\n | { status: 'confirmation_required'; email: string };\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${accessToken}`\n };\n if (input.idempotencyKey) headers['Idempotency-Key'] = input.idempotencyKey;\n\n const resp = await this.api.request<Resp>(\n `/api/v1/paywall/${this.paywallId}/auth/anonymous/upgrade`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n email: input.email,\n password: input.password,\n user_meta: input.userMeta\n })\n }\n );\n\n if (resp.status === 'confirmation_required') {\n // Не трогаем local session — она ещё анонимная по факту.\n return { kind: 'confirmation_required', email: resp.email };\n }\n\n // Confirmation off: патчим локальную user-часть session. Токены те же,\n // но user.email и is_anonymous теперь правильные — UI должен сразу\n // показывать «Signed in as <email>» вместо «Guest».\n const current = this.session;\n if (!current) {\n // Race: session ушёл между getAccessToken и сюда (signOut в другой\n // вкладке). Бэк уже обновил auth.users — но локально ничего не\n // меняем, host увидит чистое logged-out состояние.\n throw new PaywallError(\n 'not_authenticated',\n 'session disappeared during upgrade'\n );\n }\n const updatedUser: AuthUser = {\n ...current.user,\n id: resp.user.id,\n email: resp.user.email,\n is_anonymous: resp.user.is_anonymous ?? false\n };\n const updatedSession: AuthSession = { ...current, user: updatedUser };\n this.setSession(updatedSession);\n\n // Юзер больше не анонимный — anon refresh_token «принадлежит» теперь\n // обычному акку. Не имеет смысла держать его как «вернуть в анона»:\n // на signOut он всё равно теперь полноценный logout. Чистим, чтобы\n // следующий signInAnonymously попросил новую капчу (не залогинит в\n // upgraded аккаунт случайно).\n await this.clearAnonRefreshToken();\n\n return { kind: 'updated', session: updatedSession };\n }\n\n /**\n * OAuth signin через popup с PKCE. Жизненный цикл:\n * 1. Генерим verifier+challenge+state локально (verifier не уходит на бэк\n * до /exchange — это защита от перехвата code'а).\n * 2. POST /oauth/init с challenge → бэк отдаёт authorize_url.\n * 3. Открываем popup, ждём postMessage с типом 'pw-oauth' и нашим state.\n * 4. POST /oauth/exchange с {auth_code, code_verifier} → session.\n *\n * Таймаут — 5 минут от открытия popup'а. Если юзер закрыл popup до конца\n * флоу (window.closed → true) — бросаем PaywallError('oauth_cancelled').\n * Параллельные вызовы НЕ дедупятся — каждый открывает свой popup; вызывать\n * параллельно не имеет смысла, но защищаться от этого код не должен.\n *\n * onPopupOpened вызывается сразу после успешного window.open (до ожидания\n * code'а). UI использует это, чтобы сбросить loading-state кнопки: дальше\n * ответственность за флоу у popup'а, основная страница не должна висеть.\n * Если popup'ом не вернулся code (юзер закрыл вкладку, closed-detection\n * не сработал из-за COOP-severance) — promise дойдёт до oauth_timeout\n * через 5 минут, но кнопка к этому моменту уже свободна.\n */\n async signInWithOAuth(input: {\n provider: OAuthProvider;\n scopes?: string;\n userMeta?: Record<string, string>;\n onPopupOpened?: () => void;\n }): Promise<AuthSession> {\n if (typeof window === 'undefined') {\n throw new PaywallError('oauth_unavailable', 'window is required for OAuth');\n }\n\n // Single-process путь: start → openPopup → waitForOAuthCode → complete.\n // Состояние flow живёт у нас на heap'е до complete'а; для split-режима\n // (offscreen-architecture в @monetize/sdk-extension) start и complete\n // вызываются отдельными запросами — verifier остаётся внутри AuthClient'а.\n const { authorize_url, state } = await this.startOAuthFlow({\n provider: input.provider,\n scopes: input.scopes,\n userMeta: input.userMeta\n });\n\n const popup = this.openPopup(authorize_url, `pw-oauth-${state}`);\n if (!popup) {\n // Cleanup pending flow — без popup'а complete никогда не позовут.\n this.oauthFlows.delete(state);\n throw new PaywallError(\n 'popup_blocked',\n 'browser blocked auth popup — call from a user gesture'\n );\n }\n input.onPopupOpened?.();\n\n const code = await waitForOAuthCode(popup, state);\n\n if (this.destroyed) {\n this.oauthFlows.delete(state);\n throw new PaywallError('aborted', 'AuthClient destroyed mid-flow');\n }\n\n return this.completeOAuthFlow({ state, code });\n }\n\n /**\n * Шаг 1 OAuth split-API: инициирует flow на бэке, генерит PKCE verifier\n * + state, сохраняет их у себя, возвращает `{authorize_url, state}` для\n * открытия popup'а. Верификатор НЕ выходит наружу — его держит AuthClient\n * до `completeOAuthFlow`.\n *\n * Используется в offscreen-архитектуре (@monetize/sdk-extension): start\n * вызывается через RPC из content-script'а, content открывает popup\n * нативно (gesture preserved), затем зовёт completeOAuthFlow с code'ом.\n * AuthClient (в offscreen'е) делает /exchange с сохранённым verifier'ом.\n *\n * Pending flows GC'атся через 10мин — больше чем юзеру нужно прокликать\n * Google. Без cleanup'а Map бы рос на каждый закрытый popup.\n */\n async startOAuthFlow(input: {\n provider: OAuthProvider;\n scopes?: string;\n userMeta?: Record<string, string>;\n }): Promise<{ authorize_url: string; state: string }> {\n await this.hydrated;\n this.gcOAuthFlows();\n\n const verifier = generateCodeVerifier();\n const challenge = await deriveCodeChallenge(verifier);\n const state = generateState();\n\n // Anon-upgrade hand-off: если у нас уже есть session (обычно — анонимная\n // после signInAnonymously()), шлём её access_token на /oauth/init. Бэк\n // пойдёт через GoTrue `linkIdentity` вместо `signInWithOAuth` — после\n // OAuth callback'а user_id останется тот же, что был у анона, и\n // привязанные к нему trial-balances/purchases никуда не денутся.\n // Зеркалит legacy StartAuthPage.tsx (is_anonymous → linkIdentity).\n //\n // Если host хочет именно «свич аккаунт» (новый user_id) — он должен\n // сначала signOut({forgetAnonymous: true}), тогда session=null, Bearer\n // не уйдёт, и /oauth/init вернёт обычный signin-flow.\n const headers: Record<string, string> = {};\n const accessToken = await this.getAccessToken().catch((): string | null => null);\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n const { authorize_url } = await this.api.request<{ authorize_url: string }>(\n `/api/v1/paywall/${this.paywallId}/auth/oauth/init`,\n {\n method: 'POST',\n headers: Object.keys(headers).length ? headers : undefined,\n body: JSON.stringify({\n provider: input.provider,\n code_challenge: challenge,\n code_challenge_method: 's256',\n scopes: input.scopes\n })\n }\n );\n\n this.oauthFlows.set(state, {\n verifier,\n userMeta: input.userMeta,\n startedAt: Date.now()\n });\n\n return { authorize_url, state };\n }\n\n /**\n * Шаг 2 OAuth split-API: обменивает code (полученный из popup) на session,\n * используя verifier, сохранённый при startOAuthFlow. После успеха — set\n * session и эмит onAuthChange.\n *\n * Если flow не найден (state не из startOAuthFlow или GC'нулся за TTL'ом) —\n * бросает `oauth_invalid_state`. Caller должен начать заново через\n * startOAuthFlow.\n */\n async completeOAuthFlow(input: { state: string; code: string }): Promise<AuthSession> {\n await this.hydrated;\n const flow = this.oauthFlows.get(input.state);\n if (!flow) {\n throw new PaywallError(\n 'oauth_invalid_state',\n 'OAuth flow not found — start with startOAuthFlow first or check TTL'\n );\n }\n this.oauthFlows.delete(input.state);\n\n const visitorId = await this.readVisitorId();\n type Resp = RawTokens & { user: AuthUser };\n const resp = await this.api.request<Resp>(\n `/api/v1/paywall/${this.paywallId}/auth/oauth/exchange`,\n {\n method: 'POST',\n body: JSON.stringify({\n auth_code: input.code,\n code_verifier: flow.verifier,\n visitor_id: visitorId,\n user_meta: flow.userMeta\n })\n }\n );\n if (this.destroyed) {\n throw new PaywallError('aborted', 'AuthClient destroyed mid-flow');\n }\n const session = this.toSession(resp, resp.user);\n this.setSession(session);\n return session;\n }\n\n private gcOAuthFlows(): void {\n const cutoff = Date.now() - OAUTH_FLOW_TTL_MS;\n for (const [k, v] of this.oauthFlows) {\n if (v.startedAt < cutoff) this.oauthFlows.delete(k);\n }\n }\n\n /**\n * Refresh access/refresh пары через текущий refresh_token. Дедуплицирует\n * параллельные вызовы (один in-flight promise на весь клиент).\n *\n * - 401 → refresh_token отозван/невалиден → чистим session, эмитим logout.\n * - Сеть/5xx → пробрасываем ошибку, session оставляем — юзер не должен\n * разлогиниваться из-за временной сетевой проблемы.\n * - Нет session → возвращаем null без сетевого запроса.\n */\n async refresh(): Promise<AuthSession | null> {\n await this.hydrated;\n if (!this.session) return null;\n if (this.inflightRefresh) return this.inflightRefresh;\n\n const refreshToken = this.session.refresh_token;\n const currentUser = this.session.user;\n\n this.inflightRefresh = (async () => {\n try {\n const resp = await this.api.request<RawTokens>(\n `/api/v1/paywall/${this.paywallId}/auth/refresh`,\n {\n method: 'POST',\n body: JSON.stringify({ refresh_token: refreshToken })\n }\n );\n // Сервер user в /refresh не возвращает — переносим из текущей session.\n const session = this.toSession(resp, currentUser);\n this.setSession(session);\n // Anon-rotation: refresh_token вращается на каждый refresh, держим\n // persisted-копию синхронной, иначе при signOut() и попытке resume\n // будет старый, уже отозванный токен → 401 → потеря анон-аккаунта.\n if (currentUser.is_anonymous === true) {\n await this.writeAnonRefreshToken(session.refresh_token);\n }\n return session;\n } catch (e) {\n if (e instanceof PaywallError && e.status === 401) {\n // Если refresh упал на анон-юзере — чистим anonRefreshToken тоже,\n // он невалиден. Иначе следующий resumeAnonymous() пойдёт по тому\n // же мёртвому токену и снова получит 401.\n if (currentUser.is_anonymous === true) {\n await this.clearAnonRefreshToken();\n }\n this.setSession(null);\n return null;\n }\n throw e;\n } finally {\n this.inflightRefresh = null;\n }\n })();\n\n return this.inflightRefresh;\n }\n\n /**\n * Глобальный logout — инвалидирует ВСЕ refresh-токены юзера на всех\n * устройствах/контекстах через GoTrue `/logout?scope=global`. Используется\n * для compromise-account флоу («подозрительная активность, разлогинить\n * везде»).\n *\n * Local-side: чистим текущую session, остальные контексты (другие вкладки\n * / extension popup и background) подхватят logout через storage-watch\n * автоматически. Active access-токены в других контекстах останутся валидны\n * до их естественного истечения (1 час max), но refresh уже не сработает —\n * после первого `getAccessToken()` каждый контекст разлогинится сам.\n *\n * Безопасность: бэк не принимает целевой user_id — резолвит юзера из\n * Bearer, нельзя разлогинить чужой аккаунт.\n */\n async revokeAllSessions(): Promise<void> {\n await this.hydrated;\n const accessToken = this.session?.access_token;\n if (!accessToken) {\n throw new PaywallError('not_authenticated', 'no active session');\n }\n // Сначала сетевой запрос, потом local clear — обратный порядок относительно\n // signOut(). Если бэк упадёт, оставляем юзера залогиненным локально (он\n // может попробовать ещё раз); UX-преимущество мгновенного logout'а здесь\n // меньше, чем риск думать что устройство разлогинено когда оно не\n // разлогинено реально.\n await this.api.request<{ ok: true }>(\n `/api/v1/paywall/${this.paywallId}/auth/revoke-all`,\n {\n method: 'POST',\n headers: { Authorization: `Bearer ${accessToken}` }\n }\n );\n this.setSession(null);\n }\n\n /**\n * Signout: чистит локальную session СРАЗУ (UX — мгновенный logout без\n * ожидания сети), потом best-effort POST /auth/signout с текущим access.\n * Ошибка сети/5xx тут уже не критична — на бэке токен и так истечёт.\n *\n * Anon-aware: по умолчанию anonRefreshToken сохраняется. Это позволяет\n * после signOut() позвать signInAnonymously() и попасть в ТОТ ЖЕ\n * анон-аккаунт без капчи (см. resumeAnonymous). Поведение предсказуемое\n * для UX'а «гость → залогинился → разлогинился → снова гость с теми же\n * балансами».\n *\n * `forgetAnonymous: true` — полное забытие, вместе с anonRefreshToken.\n * Нужно для сценариев типа «свич аккаунта на устройстве» или жалоб на\n * приватность («очисти все мои следы»).\n */\n async signOut(opts: { forgetAnonymous?: boolean } = {}): Promise<void> {\n await this.hydrated;\n const accessToken = this.session?.access_token;\n const wasAnonymous = this.session?.user.is_anonymous === true;\n this.setSession(null);\n if (opts.forgetAnonymous) {\n await this.clearAnonRefreshToken();\n }\n if (!accessToken) return;\n // Тонкий момент: GoTrue `/logout` (scope=local default) инвалидирует\n // текущий refresh_token. Для анона текущий refresh_token = anonRefreshToken\n // в нашем storage'е; если позвать /logout — anonRefreshToken станет\n // невалиден, и следующий signInAnonymously() не сможет resume этого\n // юзера. Поэтому при signOut'е анона БЕЗ forgetAnonymous пропускаем\n // /logout — токен остаётся живым для будущего возврата. Local-side\n // юзер уже разлогинен (setSession(null)), что и нужно UX'у.\n if (wasAnonymous && !opts.forgetAnonymous) return;\n try {\n await this.api.request(\n `/api/v1/paywall/${this.paywallId}/auth/signout`,\n {\n method: 'POST',\n headers: { Authorization: `Bearer ${accessToken}` }\n }\n );\n } catch {\n /* swallow — local state уже чистый */\n }\n }\n\n /**\n * Подписка на изменения session: signin/signup/refresh/signOut/expired-401.\n * Колбек вызывается с текущим snapshot через microtask (если session есть)\n * + на каждое реальное изменение. Возвращает unsubscribe.\n */\n onAuthChange(cb: AuthChangeListener): () => void {\n this.listeners.add(cb);\n if (this.session) {\n const snapshot = this.session;\n queueMicrotask(() => {\n if (this.listeners.has(cb)) cb(snapshot);\n });\n }\n return () => {\n this.listeners.delete(cb);\n };\n }\n\n private isFresh(s: AuthSession): boolean {\n return s.expires_at - Date.now() > REFRESH_LEEWAY_MS;\n }\n\n private toSession(raw: RawTokens, user: AuthUser): AuthSession {\n // GoTrue отдаёт expires_at в секундах (unix), expires_in — в секундах.\n // SDK хранит абсолютный ms, чтобы isFresh() был тривиальным сравнением.\n const expiresAt =\n raw.expires_at != null\n ? raw.expires_at * 1000\n : Date.now() + raw.expires_in * 1000;\n return {\n access_token: raw.access_token,\n refresh_token: raw.refresh_token,\n expires_at: expiresAt,\n user\n };\n }\n\n private setSession(\n s: AuthSession | null,\n opts: { skipPersist?: boolean } = {}\n ): void {\n if (this.destroyed) return;\n const before = this.session;\n this.session = s;\n // skipPersist: применяем session, пришедшую из storage-watch'а\n // (другой контекст уже записал ровно это в storage). Без флага мы бы\n // делали лишний writeback и в Chrome Extension получили бы петлю\n // onChanged → applyExternalSession → setSession → persist → onChanged.\n if (!opts.skipPersist) void this.persist();\n if (!sameSession(before, s)) this.emit();\n }\n\n private emit(): void {\n for (const cb of this.listeners) {\n try {\n cb(this.session);\n } catch (e) {\n console.warn('[paywall] onAuthChange listener threw', e);\n }\n }\n }\n\n private storageKey(): string {\n return STORAGE_KEYS.authSession(this.paywallId);\n }\n\n private async hydrate(): Promise<void> {\n try {\n const raw = await this.storage.getItem(this.storageKey());\n if (!raw) return;\n const parsed = JSON.parse(raw) as AuthSession | null;\n if (\n !parsed ||\n typeof parsed.access_token !== 'string' ||\n typeof parsed.refresh_token !== 'string' ||\n typeof parsed.expires_at !== 'number' ||\n !parsed.user\n ) {\n return;\n }\n // Просроченный access — оставляем session, lazy refresh подберёт.\n // Если refresh_token тоже мёртв (>30 дней неактивности), refresh\n // упадёт 401 и AuthClient разлогинит сам.\n this.session = parsed;\n this.emit();\n } catch {\n /* corrupted entry — игнорируем, юзер просто увидит logged-out */\n }\n }\n\n // Используется как race-fallback в getAccessToken: между construction'ом\n // (когда storage был пуст) и onChanged-доставкой могло произойти signin\n // в другом контексте. Не дублирует watch — тот про push, этот про pull.\n private async rehydrateFromStorage(): Promise<void> {\n try {\n const raw = await this.storage.getItem(this.storageKey());\n if (!raw) return;\n const parsed = JSON.parse(raw) as AuthSession | null;\n if (\n !parsed ||\n typeof parsed.access_token !== 'string' ||\n typeof parsed.refresh_token !== 'string' ||\n typeof parsed.expires_at !== 'number' ||\n !parsed.user\n ) {\n return;\n }\n this.setSession(parsed, { skipPersist: true });\n } catch {\n /* ignore */\n }\n }\n\n /**\n * Освобождает ресурсы AuthClient'а: отписывает storage-watch, чистит\n * listener'ы, выставляет destroyed-флаг. После destroy все async-операции\n * (inflight refresh, OAuth popup, applyExternalSession) early-return'ят\n * через `isDestroyed()` guard'ы — никаких write-back'ов в storage,\n * никаких эмитов на пустые listener'ы.\n *\n * destroy() идемпотентен: повторный вызов — no-op.\n */\n destroy(): void {\n this.destroyed = true;\n if (this.storageUnwatch) {\n this.storageUnwatch();\n this.storageUnwatch = null;\n }\n this.listeners.clear();\n // inflightRefresh не отменяется (Promise нельзя cancel'нуть), но его\n // success-handler проверяет destroyed и пропускает setSession. То же\n // для waitForOAuthCode — добавляется guard в signInWithOAuth.\n this.inflightRefresh = null;\n }\n\n /** Sync-проверка: был ли вызван destroy(). Полезно для UI / тестов. */\n isDestroyed(): boolean {\n return this.destroyed;\n }\n\n private async persist(): Promise<void> {\n try {\n if (this.session) {\n await this.storage.setItem(\n this.storageKey(),\n JSON.stringify(this.session)\n );\n } else {\n await this.storage.removeItem(this.storageKey());\n }\n } catch {\n /* quota / disabled — не критично, in-memory состояние верное */\n }\n }\n\n private async readAnonRefreshToken(): Promise<string | null> {\n try {\n const v = await this.storage.getItem(STORAGE_KEYS.anonRefreshToken(this.paywallId));\n return typeof v === 'string' && v.length > 0 ? v : null;\n } catch {\n return null;\n }\n }\n\n private async writeAnonRefreshToken(token: string): Promise<void> {\n try {\n await this.storage.setItem(\n STORAGE_KEYS.anonRefreshToken(this.paywallId),\n token\n );\n } catch {\n /* quota / disabled — anon resume сломается, но текущая session жива */\n }\n }\n\n private async clearAnonRefreshToken(): Promise<void> {\n try {\n await this.storage.removeItem(\n STORAGE_KEYS.anonRefreshToken(this.paywallId)\n );\n } catch {\n /* ignore */\n }\n }\n\n /**\n * Читает stable visitor_id из storage если он там уже есть. НЕ генерит:\n * AuthClient может быть инстанцирован раньше BillingClient, а синтетический\n * visitor_id без касания пейвола не имеет смысла (нет гостевых покупок,\n * которые надо бы линковать). undefined → бэк сам пропустит ветку\n * \"merge guest purchases\".\n */\n private async readVisitorId(): Promise<string | undefined> {\n try {\n const v = await this.storage.getItem(STORAGE_KEYS.visitorId);\n return typeof v === 'string' && v.length >= 16 ? v : undefined;\n } catch {\n return undefined;\n }\n }\n}\n\n// Таймаут OAuth-флоу. 5 минут с запасом покрывают: 2FA в Google, ручной\n// switch-account в Apple, медленную сеть. Дольше — почти гарантированно\n// зависший popup, лучше показать ошибку.\nconst OAUTH_TIMEOUT_MS = 5 * 60_000;\n// Период проверки window.closed. Браузер не эмитит событие закрытия popup'а\n// для cross-origin окон, поэтому опрашиваем поллингом. 500ms — компромисс\n// между отзывчивостью и cpu.\nconst OAUTH_POLL_MS = 500;\n\ninterface OAuthMessage {\n type?: string;\n status?: string;\n code?: string;\n error?: string;\n description?: string;\n messageId?: string;\n}\n\n/** Ожидает OAuth-callback в popup'е и резолвится с code'ом. Используется\n * в `signInWithOAuth` и при split-API flow (где popup открывается извне,\n * например в content-script'е extension'а с offscreen-AuthClient'ом). */\nexport function waitForOAuthCode(popup: Window, expectedState: string): Promise<string> {\n return new Promise((resolve, reject) => {\n let settled = false;\n\n const cleanup = () => {\n settled = true;\n window.removeEventListener('message', onMessage);\n clearInterval(closedTimer);\n clearTimeout(timeoutTimer);\n };\n\n const onMessage = (e: MessageEvent) => {\n if (settled) return;\n const data = e.data as OAuthMessage | null;\n if (!data || data.type !== 'pw-oauth') return;\n // Origin не валидируем: callback page отсылает с targetOrigin='*' из-за\n // COOP-ограничений в popup'е. state — единственный нонc, привязанный\n // к открытому popup'у в этой странице, так что defence именно через\n // него: чужой постмессадж не знает наш state.\n if (data.messageId !== expectedState) return;\n\n if (data.status === 'success' && data.code) {\n cleanup();\n try { popup.close(); } catch { /* ignore */ }\n resolve(data.code);\n } else if (data.status === 'error') {\n cleanup();\n try { popup.close(); } catch { /* ignore */ }\n reject(\n new PaywallError(\n 'oauth_failed',\n data.description || data.error || 'OAuth provider returned error'\n )\n );\n }\n };\n\n // window.closed — true когда юзер закрыл popup сам или браузер закрыл его\n // из-за провайдерской ошибки (некоторые провайдеры так делают). Закрытие\n // без message = отмена.\n const closedTimer = setInterval(() => {\n if (settled) return;\n let closed: boolean;\n try {\n closed = popup.closed;\n } catch {\n // Cross-origin доступ запрещён — лучше игнорировать, чем падать.\n return;\n }\n if (closed) {\n cleanup();\n reject(new PaywallError('oauth_cancelled', 'auth popup was closed'));\n }\n }, OAUTH_POLL_MS);\n\n const timeoutTimer = setTimeout(() => {\n if (settled) return;\n cleanup();\n try { popup.close(); } catch { /* ignore */ }\n reject(new PaywallError('oauth_timeout', 'OAuth flow timed out'));\n }, OAUTH_TIMEOUT_MS);\n\n window.addEventListener('message', onMessage);\n });\n}\n\nfunction sameSession(a: AuthSession | null, b: AuthSession | null): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n return (\n a.access_token === b.access_token &&\n a.refresh_token === b.refresh_token &&\n a.expires_at === b.expires_at &&\n a.user.id === b.user.id &&\n a.user.email === b.user.email\n );\n}\n","import { SDK_VERSION } from './api';\nimport type { AuthClient } from './auth';\nimport {\n type Balance,\n PaywallError,\n QuotaExceededError\n} from './types';\n\n// ApiGatewayClient — клиент SDK 3.0 для метированного AI-прокси\n// `/api/v1/api-gateway/<provider_id>[/<path>]?paywall_id=<id>`.\n//\n// Ответственность узкая:\n// - проксировать запрос с правильными хедерами (Bearer, X-Paywall-Id);\n// - на 402 распарсить детали и бросить QuotaExceededError;\n// - на success триггернуть колбек (BillingClient декрементит локальный балланс);\n// - вернуть СЫРОЙ Response, чтобы caller сам решал — JSON, SSE, multipart.\n//\n// SSE-парсера и JSON-обёрток сознательно нет — они тащат байты в bundle.\n// Caller использует `res.body.getReader()` или `for await (const c of res.body)`\n// для стрима, `res.json()`/`res.text()` для остального. Это совпадает с тем,\n// как работает fetch — никакого кастомного API учить не надо.\n\nconst DEFAULT_API_ORIGIN = 'https://appbox.space';\n\nexport interface ApiGatewayClientOptions {\n paywallId: string;\n apiOrigin?: string;\n /** AuthClient — Bearer добавляется автоматически. На 401 от gateway клиент\n * не делает refresh: AuthClient уже сделал lazy-refresh в getAccessToken. */\n auth?: AuthClient;\n /** Headless-сценарий или legacy-флоу: явный userId вместо Bearer.\n * Передаётся как `X-User-ID`. Если задан и `auth` — Bearer выигрывает. */\n userId?: string;\n capabilities?: string[];\n fetch?: typeof fetch;\n /** Хук для оптимистичного декремента балансов в BillingClient.\n * ApiGatewayClient его дёргает на 200 (success), передавая queryType из\n * ответа (если бэк его прислал в `X-Query-Type`) или undefined.\n * Парсить body для извлечения queryType ApiGatewayClient НЕ умеет — это\n * было бы лишним чтением body, а главное — body может быть стримом. */\n onChargeSuccess?: (queryType: string | undefined) => void;\n /** Хук для рефетча балансов после 402. BillingClient ходит к /balances\n * и обновляет state, чтобы UI показал актуальный счётчик. */\n onQuotaExceeded?: (err: QuotaExceededError) => void;\n}\n\nexport interface ApiGatewayCallParams {\n /** UUID api-провайдера из платформы (`paywall_internal_api_providers.id`). */\n providerId: string;\n /** Путь после провайдера: `v1/chat/completions`, `messages`, и т.д.\n * Конкатенируется через `/`. Пустая строка/undefined = root провайдера. */\n path?: string;\n method?: 'GET' | 'POST';\n /** JSON-сериализуемый объект → application/json. FormData → multipart с\n * авто-boundary. ReadableStream/Blob/string — пробрасываются как есть.\n * Если undefined и method='POST' — отправляется пустое тело. */\n body?: unknown;\n /** Дополнительные хедеры. Перетирают наши, кроме Authorization (его всегда\n * ставим из auth) и X-Paywall-Id. */\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}\n\nexport class ApiGatewayClient {\n readonly paywallId: string;\n readonly apiOrigin: string;\n private auth: AuthClient | undefined;\n private userId: string | undefined;\n private capabilities: string[] | undefined;\n private customFetch: typeof fetch | undefined;\n private onChargeSuccess: ((queryType: string | undefined) => void) | undefined;\n private onQuotaExceeded: ((err: QuotaExceededError) => void) | undefined;\n\n constructor(opts: ApiGatewayClientOptions) {\n if (!opts.paywallId) {\n throw new PaywallError('invalid_config', 'paywallId is required');\n }\n this.paywallId = opts.paywallId;\n this.apiOrigin = opts.apiOrigin ?? DEFAULT_API_ORIGIN;\n this.auth = opts.auth;\n this.userId = opts.userId;\n this.capabilities = opts.capabilities;\n this.customFetch = opts.fetch;\n this.onChargeSuccess = opts.onChargeSuccess;\n this.onQuotaExceeded = opts.onQuotaExceeded;\n // Безопасность: в браузере userId должен приходить из Bearer (бэк\n // резолвит через GoTrue), а не передаваться явно — иначе host может\n // подсунуть чужой ID. `auth` без `userId` — норма; `userId` без `auth`\n // в браузере — потенциальная уязвимость, предупреждаем.\n if (\n opts.userId &&\n !opts.auth &&\n typeof window !== 'undefined' &&\n typeof (window as { document?: unknown }).document !== 'undefined'\n ) {\n console.warn(\n '[paywall] WARNING: ApiGatewayClient.userId set without auth in browser. ' +\n 'Client can spoof userId. Use AuthClient + Bearer for trusted user.id.'\n );\n }\n }\n\n async call(params: ApiGatewayCallParams): Promise<Response> {\n const path = params.path ? params.path.replace(/^\\/+/, '') : '';\n const url = new URL(\n `/api/v1/api-gateway/${encodeURIComponent(params.providerId)}${path ? `/${path}` : ''}`,\n this.apiOrigin\n );\n // paywall_id шлём и в query (legacy v2 контракт), и в X-Paywall-Id хедере\n // (SDK 3.0 контракт). Бэк-route после патча принимает оба.\n url.searchParams.set('paywall_id', this.paywallId);\n\n const headers = new Headers(params.headers);\n headers.set('X-SDK-Version', SDK_VERSION);\n headers.set('X-Paywall-Id', this.paywallId);\n if (this.capabilities?.length) {\n headers.set('X-SDK-Capabilities', this.capabilities.join(','));\n }\n\n const token = await this.auth?.getAccessToken();\n if (token) {\n headers.set('Authorization', `Bearer ${token}`);\n } else if (this.userId) {\n headers.set('X-User-ID', this.userId);\n }\n\n // Content-Type: тот же подход, что в ApiClient. FormData — браузер сам.\n // unknown body, не строка/FormData/Blob — JSON.stringify.\n const isFormData = typeof FormData !== 'undefined' && params.body instanceof FormData;\n const isBlob = typeof Blob !== 'undefined' && params.body instanceof Blob;\n const isStream =\n typeof ReadableStream !== 'undefined' && params.body instanceof ReadableStream;\n const isString = typeof params.body === 'string';\n\n let body: BodyInit | undefined;\n if (params.body === undefined || params.body === null) {\n body = undefined;\n } else if (isFormData || isBlob || isStream || isString) {\n body = params.body as BodyInit;\n } else {\n body = JSON.stringify(params.body);\n if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/json');\n }\n\n // Локальная переменная вместо `this.fetchImpl(...)` — нативный fetch\n // требует this=globalThis, а вызов через поле объекта связывает this с\n // ApiGatewayClient и Chrome бросает 'Illegal invocation'.\n const fetchImpl = this.customFetch ?? fetch;\n let response: Response;\n try {\n response = await fetchImpl(url.toString(), {\n method: params.method ?? 'POST',\n headers,\n body,\n signal: params.signal,\n credentials: 'omit'\n });\n } catch (cause) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n throw new PaywallError('network_error', `Network request failed: ${detail}`, { cause });\n }\n\n if (response.status === 402) {\n const err = await parseQuotaError(response);\n this.onQuotaExceeded?.(err);\n throw err;\n }\n\n if (!response.ok) {\n // Не дренируем body — caller может попытаться `res.text()` сам, либо\n // мы клонируем для парсинга. Парсим клон, чтобы вернуть оригинал нетронутым\n // (важно для стримовых ответов; для JSON ошибок — clone() дешёвый).\n const code = await tryReadErrorCode(response.clone());\n throw new PaywallError(\n code ?? `http_${response.status}`,\n response.statusText || 'Gateway request failed',\n { status: response.status }\n );\n }\n\n // Charge произошёл на бэке (см. /api-gateway/route.ts: `if (queryType !== 'free')`).\n // Бэк не возвращает обновлённый баланс в хедерах — оптимистично декрементим\n // на клиенте. queryType приходит из X-Query-Type, если бэк его проставил;\n // иначе хук получит undefined, и BillingClient уйдёт в re-fetch /balances.\n const queryType = response.headers.get('X-Query-Type') ?? undefined;\n this.onChargeSuccess?.(queryType);\n\n return response;\n }\n}\n\ninterface QuotaErrorBody {\n error?: string;\n details?: {\n balances?: Array<{ balances?: Balance[] } | Balance[] | unknown>;\n queryType?: string;\n currentBalance?: Balance | null;\n };\n}\n\nasync function parseQuotaError(response: Response): Promise<QuotaExceededError> {\n let body: QuotaErrorBody = {};\n try {\n body = (await response.json()) as QuotaErrorBody;\n } catch {\n /* malformed — отдадим пустые поля */\n }\n\n // Бэк отдаёт `details.balances` как массив строк paywall_balances:\n // [{ balances: Balance[] }] (см. supabase select). Извлекаем плоский массив.\n const rawBalances = body.details?.balances;\n let balances: Balance[] = [];\n if (Array.isArray(rawBalances)) {\n const first = rawBalances[0] as { balances?: Balance[] } | Balance[] | undefined;\n if (Array.isArray(first)) {\n balances = first as Balance[];\n } else if (first && Array.isArray((first as { balances?: Balance[] }).balances)) {\n balances = (first as { balances: Balance[] }).balances;\n }\n }\n\n return new QuotaExceededError({\n balances,\n queryType: body.details?.queryType ?? '',\n currentBalance: body.details?.currentBalance ?? null\n });\n}\n\nasync function tryReadErrorCode(response: Response): Promise<string | null> {\n const ct = response.headers.get('content-type') ?? '';\n if (!ct.includes('application/json')) return null;\n try {\n const data = (await response.json()) as { error?: string; code?: string };\n return data.code || data.error || null;\n } catch {\n return null;\n }\n}\n","import { ApiClient } from './api';\nimport {\n ApiGatewayClient,\n type ApiGatewayClientOptions\n} from './ApiGatewayClient';\nimport type { AuthClient, AuthUser } from './auth';\nimport {\n createStorage,\n ensureVisitorId,\n generateVisitorId as generateUuid,\n type StorageAdapter,\n STORAGE_KEYS\n} from './storage';\nimport {\n type Acquiring,\n type Balance,\n type CheckoutResult,\n type Identity,\n type Layout,\n type LocaleOverrides,\n type PaywallBootstrap,\n type PaywallPrice,\n type PaywallPurchaseDetailed,\n type PaywallSettings,\n type PaywallUser,\n type UserLanguageInfo,\n PaywallError\n} from './types';\n\n// Свежесть in-memory кеша user. 5с — компромисс: достаточно, чтобы naïve-юзер,\n// дёрнувший getUser в setInterval(1000), не нагружал сервер; недостаточно,\n// чтобы пропустить успешную оплату дольше пары секунд после revalidateTag.\nconst USER_CACHE_TTL_MS = 5_000;\n// Persistent cache (storage) живёт 30 минут. Дольше — рискованно отдавать\n// устаревший snapshot без сети.\nconst USER_PERSIST_TTL_MS = 30 * 60_000;\n// Persistent bootstrap живёт 1 час. На каждом mount BillingClient hydrate'ит\n// его из storage и параллельно шлёт revalidate с `?if_version=<v>`. Если\n// сервер ответил `unchanged: true` — мы лишь обновляем user, structure\n// остаётся та же (cheap path). При истечении TTL — блокирующий полный\n// запрос; не отдаём stale, который потенциально не отражает изменения\n// настроек админом (revalidateTag на бэке инвалидирует unstable_cache, но\n// не знает про клиентский storage). 1 час — компромисс: попаdaния в кэш\n// доминируют над холодными запусками, при этом изменения в админке\n// доходят до клиента в пределах часа без явного refresh.\nconst BOOTSTRAP_PERSIST_TTL_MS = 60 * 60_000;\n// Порог свежести cached bootstrap'а: если последняя запись была раньше этого —\n// при следующем `bootstrap()` уйдёт фоновый revalidate с `?if_version`.\n// Раньше — return cached без сети (миллисекунды между двумя `bootstrap()`\n// не имеют смысла дёргать). 5 минут — большинство переоткрытий popup'а\n// попадают в холодный период, при этом мы не штурмуем сервер при бурстах.\nconst BOOTSTRAP_STALE_THRESHOLD_MS = 5 * 60_000;\nconst EMPTY_USER: PaywallUser = {\n has_active_subscription: false,\n purchases: [],\n trial: null\n};\n\nfunction identityKey(identity: Identity | undefined): string {\n if (!identity) return 'guest';\n return identity.email || identity.userId || identity.anonymousId || 'guest';\n}\n\nfunction sameUser(a: PaywallUser | null, b: PaywallUser | null): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\ntype UserListener = (user: PaywallUser) => void;\ntype BalancesListener = (balances: Balance[]) => void;\n\n// Балансы AI-провайдеров. 5с TTL — как у user-cache: balance меняется только\n// после успешного gateway-вызова (мы декрементим оптимистично) или вне SDK\n// (платёж пополнил квоту); в обоих случаях короткий TTL достаточно.\nconst BALANCES_CACHE_TTL_MS = 5_000;\n// Persistent balances живут 5 минут. Достаточно, чтобы переоткрытие popup'а\n// в пределах рабочей сессии (типичный паттерн extension'а) шло из кэша; не\n// настолько долго, чтобы баланс сильно разъехался с серверной правдой при\n// нескольких покупках подряд. Свежий decrement через `decrementBalanceLocal`\n// сразу пишется в storage и доходит до других вкладок через `storage.watch`.\nconst BALANCES_PERSIST_TTL_MS = 5 * 60_000;\n// Порог свежести cached balances: при возрасте младше — `getBalances()`\n// возвращает кэш без сетевого запроса. Старше — фоновый refetch\n// (stale-while-revalidate). force=true обходит порог. 30 секунд — компромисс:\n// частые UI-renders (счётчик баланса в widget'е) не штурмуют сервер, при\n// этом изменения, сделанные на бэке без участия SDK, доходят достаточно\n// быстро.\nconst BALANCES_STALE_THRESHOLD_MS = 30_000;\n\nfunction sameBalances(a: Balance[] | null, b: Balance[] | null): boolean {\n if (a === b) return true;\n if (!a || !b || a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i].type !== b[i].type || a[i].count !== b[i].count) return false;\n }\n return true;\n}\n\nexport interface BillingClientOptions {\n paywallId: string;\n apiOrigin?: string;\n identity?: Identity;\n storage?: StorageAdapter;\n capabilities?: string[];\n fetch?: typeof fetch;\n /**\n * Server SDK API key. Используется для `/start-checkout` в headless/hybrid-сценариях,\n * где вызов идёт из trusted-окружения (backend клиента). В client-native пути\n * ключ НЕ передавать — приватный токен утечёт в браузер.\n */\n apiKey?: string;\n /**\n * AuthClient для подключения Bearer-авторизации и автосинка identity. Если\n * передан — все запросы получают `Authorization: Bearer <access_token>`,\n * а identity пересчитывается из auth.user на каждом login/logout/refresh\n * (перетирает явно заданный `opts.identity` после первого auth-event'а).\n *\n * Без auth BillingClient работает как раньше: identity приходит снаружи\n * через `setIdentity`, Bearer не отправляется.\n */\n auth?: AuthClient;\n /**\n * Preview/editor-mode. Когда true:\n * - `bootstrap()` НЕ ходит в сеть — отдаёт только `cachedBootstrap`, заданный\n * через `setBootstrap()`. Без seed'а throw'ает (caller обязан засидить до open).\n * - Storage.watch / persist отключены (preview редактора локален для текущей вкладки).\n * - `setBootstrap(partial)` доступен как публичный setter — host'у разрешено\n * мутировать кеш для live-обновления модалки в редакторе админки.\n * Дефолт false — обычный production-режим.\n */\n preview?: boolean;\n}\n\nconst DEFAULT_API_ORIGIN = 'https://appbox.space';\n\nexport class BillingClient {\n readonly paywallId: string;\n readonly apiOrigin: string;\n readonly capabilities: string[] | undefined;\n /** AuthClient, если был передан в options. Иначе undefined. */\n readonly auth: AuthClient | undefined;\n private api: ApiClient;\n private storage: StorageAdapter;\n private identity: Identity | undefined;\n private apiKey: string | undefined;\n private fetchImpl: typeof fetch | undefined;\n private cachedBootstrap: PaywallBootstrap | null = null;\n // Время последней успешной записи cachedBootstrap (mono Date.now). Используем\n // для TTL: после BOOTSTRAP_PERSIST_TTL_MS считаем stale и идём в сеть\n // блокирующе (нельзя отдавать устаревший layout — админ мог его поменять).\n private cachedBootstrapAt = 0;\n // In-flight dedupe для bootstrap. Параллельные `bootstrap()` (например, mount\n // двух виджетов одновременно) получают один и тот же promise — один сетевой\n // запрос. Stale-while-revalidate ветка тоже пишет сюда фоновый promise,\n // чтобы commit'ы не пересекались.\n private inflightBootstrap: Promise<PaywallBootstrap> | null = null;\n private bootstrapListeners = new Set<(b: PaywallBootstrap) => void>();\n // Отписка от storage.watch — другая вкладка / popup / service-worker\n // могла обновить bootstrap; через watch мы получаем onChanged без\n // сетевого запроса. null = адаптер не поддерживает watch (memory).\n private bootstrapStorageUnwatch: (() => void) | null = null;\n private authUnsubscribe: (() => void) | null = null;\n\n // user-cache: in-memory с TTL + in-flight dedupe + persistent fallback.\n private cachedUser: PaywallUser | null = null;\n private cachedUserAt = 0;\n private inflightUser: Promise<PaywallUser> | null = null;\n private userListeners = new Set<UserListener>();\n\n // Stable visitor_id для аналитики. Резолвится один раз при инициализации,\n // переиспользуется на все track-вызовы. Не привязан к identity.\n private visitorIdPromise: Promise<string> | null = null;\n private visitorId: string | null = null;\n\n // In-flight createCheckout dedupe — Stage 1 защиты от дубликатов покупок.\n // Параллельные клики по CTA (двойной клик, две вкладки на одной странице)\n // получают тот же promise и тот же server-side checkout-URL вместо двух\n // запросов к /start-checkout. Ключ — либо переданный idempotencyKey, либо\n // `auto:${priceId}` (один inflight на цену для авто-сгенеренных ключей).\n private inflightCheckouts = new Map<string, Promise<CheckoutResult>>();\n\n // balances-cache: симметрично user-cache. ApiGatewayClient оптимистично\n // декрементит через decrementBalanceLocal(); явный getBalances({force:true})\n // ходит к /balances и обновляет state. Listener'ы получают snapshot после\n // каждого реального изменения (Object.is не сравниваем — массивы разные).\n private cachedBalances: Balance[] | null = null;\n private cachedBalancesAt = 0;\n // Отписка от storage.watch для balances. Ключ identity-bound, при\n // setIdentity отписываемся и переподписываемся под новым identityKey.\n private balancesStorageUnwatch: (() => void) | null = null;\n private inflightBalances: Promise<Balance[]> | null = null;\n private balanceListeners = new Set<BalancesListener>();\n\n // Preview/editor-mode: см. BillingClientOptions.preview. Фиксируется в\n // конструкторе; runtime-переключения не предусмотрено — preview/production\n // это разные жизненные циклы клиента.\n private readonly previewMode: boolean;\n // Монотонный счётчик для генерации синтетического version в setBootstrap.\n // Реальный server-version имеет вид \"<paywall_id>:<hash>\"; здесь мы кладём\n // \"preview:<n>\" чтобы applyBootstrap гарантированно увидел смену version\n // и дёрнул listener'ы (PaywallRoot rerender'ит на каждый setBootstrap).\n private previewVersionCounter = 0;\n\n constructor(opts: BillingClientOptions) {\n if (!opts.paywallId) {\n throw new PaywallError('invalid_config', 'paywallId is required');\n }\n\n this.paywallId = opts.paywallId;\n this.apiOrigin = opts.apiOrigin ?? DEFAULT_API_ORIGIN;\n this.capabilities = opts.capabilities;\n this.auth = opts.auth;\n this.previewMode = opts.preview === true;\n // Если auth передан — initial identity берём из cached user (если он\n // успел гидрироваться в конструкторе AuthClient — обычно нет, поэтому\n // ниже подписываемся на onAuthChange и обновим, как только session\n // зарезолвится). Явно заданный opts.identity побеждает только до\n // первого auth-event'а — после login/logout это поле перетрётся.\n const authUser = opts.auth?.getCachedUser();\n this.identity = opts.identity ?? (authUser ? authUserToIdentity(authUser) : undefined);\n this.apiKey = opts.apiKey;\n this.fetchImpl = opts.fetch;\n // Безопасность: приватный server-SDK ключ НИКОГДА не должен попасть в\n // браузер. Detect-эвристика — наличие `window.document` (не идеальная,\n // но отсекает обычные web/extension случаи; в Node/Deno/Bun fallback\n // на `typeof window === 'undefined'`). Не throw'аем — host может иметь\n // нестандартный сценарий (e2e-тесты с инжекцией ключа), но громко\n // предупреждаем в console.error чтобы это попало в Sentry / логи.\n if (\n opts.apiKey &&\n typeof window !== 'undefined' &&\n typeof (window as { document?: unknown }).document !== 'undefined'\n ) {\n console.error(\n '[paywall] SECURITY: BillingClient.apiKey detected in browser context. ' +\n 'This is a server-SDK key and exposes your account. Remove apiKey ' +\n 'or move BillingClient to a trusted backend.'\n );\n }\n this.storage = createStorage(opts.storage);\n this.api = new ApiClient({\n apiOrigin: this.apiOrigin,\n paywallId: opts.paywallId,\n capabilities: opts.capabilities,\n fetch: opts.fetch,\n // Bearer прокидывается каждый запрос. AuthClient.getAccessToken\n // делает lazy refresh, дедупит, на 401 возвращает null — тогда\n // Authorization-хедер просто не выставится.\n getAuthToken: opts.auth ? () => opts.auth!.getAccessToken() : undefined\n });\n\n if (opts.auth) {\n this.authUnsubscribe = opts.auth.onAuthChange((session) => {\n const next = session ? authUserToIdentity(session.user) : undefined;\n // Не дёргаем setIdentity если ничего не поменялось (избежим лишних\n // инвалидаций bootstrap/user-cache).\n if (sameIdentity(this.identity, next)) return;\n this.setIdentity(next);\n });\n }\n\n // Seed из persistent storage — чтобы первый getUser() мог отдать last-known\n // мгновенно (offline fallback). Не блокируем конструктор.\n void this.hydrateUserFromStorage();\n\n // То же для bootstrap'а: hydrate + подписка на cross-context изменения.\n // Если popup уже сходил за свежим bootstrap'ом, content-script подхватит\n // через storage.watch без своего сетевого запроса.\n void this.hydrateBootstrapFromStorage();\n this.subscribeBootstrapStorage();\n\n // Balances: identity-bound persist. На init ключ = identity на момент\n // конструктора; setIdentity отписывается и переподписывается под новым.\n void this.hydrateBalancesFromStorage();\n this.subscribeBalancesStorage();\n\n // Резолвим visitor_id заранее, чтобы EventTracker мог брать sync-ссылку\n // (this.visitorId) почти сразу после первого микротаска.\n this.visitorIdPromise = ensureVisitorId(this.storage).then((id) => {\n this.visitorId = id;\n return id;\n });\n }\n\n /**\n * Stable visitor_id (UUID v4). Первый вызов awaitит первичный резолв из\n * storage; последующие — мгновенно из in-memory кеша. Используется\n * EventTracker'ом для атрибуции аналитики.\n */\n async getVisitorId(): Promise<string> {\n if (this.visitorId) return this.visitorId;\n if (!this.visitorIdPromise) {\n this.visitorIdPromise = ensureVisitorId(this.storage).then((id) => {\n this.visitorId = id;\n return id;\n });\n }\n return this.visitorIdPromise;\n }\n\n /** Sync-доступ к visitor_id. null если ещё не зарезолвили (первые ms жизни). */\n getCachedVisitorId(): string | null {\n return this.visitorId;\n }\n\n setIdentity(identity: Identity | undefined): void {\n this.identity = identity;\n // bootstrap НЕ сбрасываем: structure (layout/prices/offers/locales) от\n // identity не зависит, persisted shape переиспользуем. user обновится\n // отдельно через getUser({force:true}) ниже + следующий revalidate\n // bootstrap'а подтянет свежий user одним round-trip'ом, если нужно.\n // user привязан к identity — переключение чистит, иначе один юзер увидит\n // подписку другого после re-login.\n this.cachedUser = null;\n this.cachedUserAt = 0;\n this.inflightUser = null;\n // Балансы привязаны к Bearer-юзеру (см. /balances route — он использует\n // Auth-юзера, не identity.email). При re-login обнуляем, listener'ы\n // получат пустой массив до следующего getBalances.\n this.cachedBalances = null;\n this.cachedBalancesAt = 0;\n this.inflightBalances = null;\n // Storage-ключ балансов identity-bound — отписываемся от старого ключа\n // и переподписываемся под новым identityKey'ем. Hydrate подхватит\n // persisted balances нового юзера (если открывал расширение раньше).\n if (this.balancesStorageUnwatch) {\n this.balancesStorageUnwatch();\n this.balancesStorageUnwatch = null;\n }\n void this.hydrateBalancesFromStorage();\n this.subscribeBalancesStorage();\n void this.hydrateUserFromStorage();\n // Auto-refetch user'а в фоне для нового identity. Без этого UI'ам с\n // подпиской на onUserChange (account-widget'ы, pop'ы статуса) пришлось\n // бы вручную дёргать getUser после каждого signin'а — а они обычно\n // не знают что signin произошёл. С refetch'ем onUserChange broadcast'ит\n // свежий has_active_subscription автоматически. Promise проглатывает\n // ошибки — getUser сам обновит cachedUser в EMPTY_USER при сетевом\n // фейле, listener'ы получат rollback-snapshot.\n if (identity) {\n void this.getUser({ force: true }).catch(() => {\n /* network failure — listener'ы получат EMPTY_USER через applyUser */\n });\n }\n }\n\n /**\n * Отписаться от auth-event'ов и сбросить listener'ы. Вызывать когда\n * BillingClient больше не нужен (тесты, hot-reload, переинициализация).\n * Без destroy() listener на AuthClient переживёт BillingClient и будет\n * дёргать setIdentity на освобождённом инстансе. Слушатели user/balance\n * чистятся, чтобы упавший host (например, размонтированный React-tree)\n * не держал замыкания на эти колбеки.\n */\n destroy(): void {\n if (this.authUnsubscribe) {\n this.authUnsubscribe();\n this.authUnsubscribe = null;\n }\n if (this.bootstrapStorageUnwatch) {\n this.bootstrapStorageUnwatch();\n this.bootstrapStorageUnwatch = null;\n }\n if (this.balancesStorageUnwatch) {\n this.balancesStorageUnwatch();\n this.balancesStorageUnwatch = null;\n }\n this.userListeners.clear();\n this.balanceListeners.clear();\n this.bootstrapListeners.clear();\n }\n\n getIdentity(): Identity | undefined {\n return this.identity;\n }\n\n getStorage(): StorageAdapter {\n return this.storage;\n }\n\n async bootstrap(\n forceOrOpts: boolean | { force?: boolean; signal?: AbortSignal } = false\n ): Promise<PaywallBootstrap> {\n // Старая сигнатура `bootstrap(force: boolean)` сохраняется для совместимости\n // с уже написанным host-кодом; новая — `bootstrap({force?, signal?})`.\n const opts =\n typeof forceOrOpts === 'boolean' ? { force: forceOrOpts } : forceOrOpts;\n\n // Preview-mode: сеть отключена. Caller обязан был засидить cachedBootstrap\n // через setBootstrap() до первого open(). Без seed'а кидаем явную ошибку,\n // чтобы редактор админки сразу увидел причину пустой модалки.\n if (this.previewMode) {\n if (this.cachedBootstrap) return this.cachedBootstrap;\n throw new PaywallError(\n 'invalid_config',\n 'BillingClient in preview mode but cachedBootstrap is not seeded. Call setBootstrap(bootstrap) before open().'\n );\n }\n\n // Stale-while-revalidate: если кэш свежий по TTL — отдаём мгновенно и\n // в фоне идём за свежим (с `?if_version=<v>`, чтобы 99% случаев бэк\n // ответил коротким `unchanged: true`). Force обходит весь кэш и блокирует.\n const now = Date.now();\n const cacheFresh =\n this.cachedBootstrap &&\n this.cachedBootstrapAt > 0 &&\n now - this.cachedBootstrapAt < BOOTSTRAP_PERSIST_TTL_MS;\n\n if (!opts.force && cacheFresh) {\n const shouldRevalidate =\n now - this.cachedBootstrapAt > BOOTSTRAP_STALE_THRESHOLD_MS;\n if (shouldRevalidate) {\n // Фоновый revalidate — не блокируем caller, ошибки swallow'им (cache\n // всё ещё считается достоверным до истечения TTL).\n void this.revalidateBootstrap(opts.signal).catch(() => {\n /* network/abort — listener'ы получат свежее на следующий запрос */\n });\n }\n return this.cachedBootstrap!;\n }\n\n // Параллельные mount'ы (виджет + popup) получают один и тот же promise.\n // Без dedupe — два сетевых запроса с одинаковым результатом.\n if (this.inflightBootstrap) return this.inflightBootstrap;\n\n this.inflightBootstrap = this.fetchBootstrap({\n ifVersion: opts.force ? undefined : this.cachedBootstrap?.version,\n signal: opts.signal\n }).finally(() => {\n this.inflightBootstrap = null;\n });\n\n return this.inflightBootstrap;\n }\n\n /**\n * Подписка на изменения bootstrap'а: applyBootstrap (сетевой revalidate,\n * cross-context storage.watch). Срабатывает ТОЛЬКО при реальном изменении\n * `version` (unchanged-ответ от сервера не дёргает listener'ов). Возвращает\n * unsubscribe.\n */\n onBootstrapChange(cb: (b: PaywallBootstrap) => void): () => void {\n this.bootstrapListeners.add(cb);\n return () => {\n this.bootstrapListeners.delete(cb);\n };\n }\n\n /**\n * Заменить cachedBootstrap частичными или полными данными и эмитнуть всем\n * подписчикам. Используется host'ом в preview-mode (редактор админки) для\n * live-обновления открытой модалки без сетевого revalidate'а.\n *\n * Поведение:\n * - Без `cachedBootstrap` ожидаются как минимум `settings` + `prices` —\n * иначе PaywallRoot не сможет отрендерить тарифы и упадёт.\n * - С существующим кешем партиал мёрджится поверх: `settings` глубокий мёрдж\n * на 1 уровень (поля настроек), массивы `prices`/`offers` перезаписываются.\n * - Каждый вызов бампит `version` (\"preview:<n>\"), чтобы applyBootstrap'овая\n * проверка `versionChanged` всегда срабатывала и listener'ы дёргались.\n * - Persist в storage НЕ делаем — preview не должен утекать в другие вкладки.\n *\n * В non-preview режиме метод доступен, но это редкий путь (например, для\n * тестов host'а) — production-код должен полагаться на bootstrap() + revalidate.\n */\n setBootstrap(partial: Partial<PaywallBootstrap>): void {\n const base: PaywallBootstrap = this.cachedBootstrap ?? {\n settings: { id: this.paywallId, name: '' } as PaywallSettings,\n prices: [],\n offers: []\n };\n\n const merged: PaywallBootstrap = {\n ...base,\n ...partial,\n settings:\n partial.settings !== undefined\n ? { ...base.settings, ...partial.settings }\n : base.settings,\n prices: partial.prices !== undefined ? partial.prices : base.prices,\n offers: partial.offers !== undefined ? partial.offers : base.offers,\n version: `preview:${++this.previewVersionCounter}`\n };\n\n if (!merged.layout) {\n merged.layout = buildDefaultLayout(merged.settings, merged.prices);\n }\n applyLocaleOverrides(merged);\n\n this.cachedBootstrap = merged;\n this.cachedBootstrapAt = Date.now();\n\n for (const cb of this.bootstrapListeners) {\n try {\n cb(merged);\n } catch (e) {\n console.warn('[paywall] onBootstrapChange listener threw', e);\n }\n }\n }\n\n // Network primitive — единая точка для force-запроса, revalidate'а и\n // первого холодного bootstrap'а. `ifVersion` шлёт server-side short-circuit:\n // если совпала — бэк отвечает `{unchanged: true, version, user}` и мы лишь\n // обновляем cached user, structure (layout/prices/offers/locales) не трогаем.\n private async fetchBootstrap(opts: {\n ifVersion?: string;\n signal?: AbortSignal;\n }): Promise<PaywallBootstrap> {\n const headers: Record<string, string> = {};\n if (this.identity?.email) headers['X-User-Email'] = this.identity.email;\n\n const path = opts.ifVersion\n ? `/api/v1/paywall/${this.paywallId}/bootstrap?if_version=${encodeURIComponent(opts.ifVersion)}`\n : `/api/v1/paywall/${this.paywallId}/bootstrap`;\n\n const resp = await this.api.request<\n PaywallBootstrap | { unchanged: true; version: string; user?: PaywallUser }\n >(path, {\n ...(Object.keys(headers).length ? { headers } : {}),\n signal: opts.signal\n });\n\n if ('unchanged' in resp && resp.unchanged) {\n // Server-side подтвердил, что structure не изменилась. Cached остаётся,\n // обновляем только user. Если cached почему-то null (race на старте) —\n // fallback: повторяем запрос без if_version, чтобы получить full.\n if (!this.cachedBootstrap) {\n return this.fetchBootstrap({ signal: opts.signal });\n }\n // Освежим TTL — за unchanged-ответом тоже идёт сеть, кэш всё ещё валиден.\n this.cachedBootstrapAt = Date.now();\n if (resp.user) this.applyUser(resp.user);\n return this.cachedBootstrap;\n }\n\n const bootstrap = resp as PaywallBootstrap;\n if (!bootstrap.layout) {\n bootstrap.layout = buildDefaultLayout(bootstrap.settings, bootstrap.prices);\n }\n applyLocaleOverrides(bootstrap);\n\n this.applyBootstrap(bootstrap, { persist: true });\n if (bootstrap.user) this.applyUser(bootstrap.user);\n\n return bootstrap;\n }\n\n // Фоновый revalidate из stale-while-revalidate ветки. Дедуплицируется через\n // `inflightBootstrap`, чтобы параллельные revalidate'ы не пересекались.\n private revalidateBootstrap(signal?: AbortSignal): Promise<PaywallBootstrap> {\n if (this.inflightBootstrap) return this.inflightBootstrap;\n this.inflightBootstrap = this.fetchBootstrap({\n ifVersion: this.cachedBootstrap?.version,\n signal\n }).finally(() => {\n this.inflightBootstrap = null;\n });\n return this.inflightBootstrap;\n }\n\n // Применяет fresh bootstrap к state: emit listeners ТОЛЬКО при изменении\n // version (т.е. structure реально другая). Это нужно, чтобы повторный\n // applyBootstrap из storage.watch не перерисовал UI зря, если другая\n // вкладка нашла тот же version. persist=false для пути «получили из\n // storage» — там кто-то другой уже записал.\n private applyBootstrap(\n bootstrap: PaywallBootstrap,\n { persist }: { persist: boolean }\n ): void {\n const versionChanged =\n !this.cachedBootstrap || this.cachedBootstrap.version !== bootstrap.version;\n\n this.cachedBootstrap = bootstrap;\n this.cachedBootstrapAt = Date.now();\n\n if (persist) void this.persistBootstrap(bootstrap);\n\n if (versionChanged) {\n for (const cb of this.bootstrapListeners) {\n try {\n cb(bootstrap);\n } catch (e) {\n console.warn('[paywall] onBootstrapChange listener threw', e);\n }\n }\n }\n }\n\n private async hydrateBootstrapFromStorage(): Promise<void> {\n if (this.cachedBootstrap) return;\n try {\n const raw = await this.storage.getItem(STORAGE_KEYS.bootstrap(this.paywallId));\n if (!raw) return;\n const parsed = JSON.parse(raw) as {\n at: number;\n bootstrap: PaywallBootstrap;\n } | null;\n if (!parsed?.bootstrap) return;\n if (Date.now() - parsed.at > BOOTSTRAP_PERSIST_TTL_MS) return;\n // Race-защита: если за время `await` кто-то успел положить свежий\n // bootstrap (одновременный фоновый fetch) — не перетираем.\n if (this.cachedBootstrap) return;\n // Локали могут быть не применены в persisted-shape'е — гарантируем\n // консистентность накатив их заново. applyLocaleOverrides идемпотентен.\n applyLocaleOverrides(parsed.bootstrap);\n this.cachedBootstrap = parsed.bootstrap;\n this.cachedBootstrapAt = parsed.at;\n // emit listener'ам — host'ы могут подписаться синхронно в конструкторе\n // и ждать первый snapshot. user из persisted — может быть очень старый,\n // не применяем (свежий придёт через сетевой запрос / hydrateUser).\n for (const cb of this.bootstrapListeners) {\n try {\n cb(parsed.bootstrap);\n } catch (e) {\n console.warn('[paywall] onBootstrapChange listener threw', e);\n }\n }\n } catch {\n /* corrupted entry — игнорируем */\n }\n }\n\n private async persistBootstrap(bootstrap: PaywallBootstrap): Promise<void> {\n // Не персистим bootstrap без version — старый бэк не отдаёт его, и без\n // version нет смысла в ревалидации (всегда придётся тянуть full payload).\n if (!bootstrap.version) return;\n try {\n // user'а в persisted не пишем — он живёт под своим ключом userState\n // с собственным TTL/identity-маппингом.\n const { user: _user, ...rest } = bootstrap;\n await this.storage.setItem(\n STORAGE_KEYS.bootstrap(this.paywallId),\n JSON.stringify({ at: Date.now(), bootstrap: rest })\n );\n } catch {\n /* quota / disabled */\n }\n }\n\n // Cross-context sync: другая вкладка / popup / sw записали свежий bootstrap\n // → мы подхватываем без сетевого запроса. Адаптеры без watch (memory) —\n // no-op, всё работает как раньше через сеть.\n private subscribeBootstrapStorage(): void {\n if (typeof this.storage.watch !== 'function') return;\n this.bootstrapStorageUnwatch = this.storage.watch(\n STORAGE_KEYS.bootstrap(this.paywallId),\n (raw) => {\n if (!raw) return;\n try {\n const parsed = JSON.parse(raw) as {\n at: number;\n bootstrap: PaywallBootstrap;\n } | null;\n if (!parsed?.bootstrap) return;\n // Если та же version — нет смысла перезаписывать (избежим лишних\n // listener'ов из applyBootstrap).\n if (\n this.cachedBootstrap?.version &&\n this.cachedBootstrap.version === parsed.bootstrap.version\n ) {\n this.cachedBootstrapAt = parsed.at;\n return;\n }\n applyLocaleOverrides(parsed.bootstrap);\n this.applyBootstrap(parsed.bootstrap, { persist: false });\n } catch {\n /* corrupted entry — ignore */\n }\n }\n );\n }\n\n /** Возвращает последний загруженный bootstrap без сетевого запроса.\n * null = bootstrap ещё не загружали. Удобно для post-checkout-логики\n * (PaywallUI читает success_redirect_url, не делая второго round-trip'а). */\n getCachedBootstrap(): PaywallBootstrap | null {\n return this.cachedBootstrap;\n }\n\n /**\n * Шорткат поверх `bootstrap()`: ждёт загрузку структуры пейвола и возвращает\n * цены. Полезно когда host рисует цены вне модалки (карточки на лендинге,\n * \"Pricing\" page и т.п.) и не хочет руками распаковывать bootstrap.\n *\n * Locale-оверрайды (`label`/`description` под `navigator.language`) уже\n * применены — массив готов к рендеру. Кэш/TTL/stale-while-revalidate — те\n * же, что у `bootstrap()`: повторный вызов не штурмует сервер.\n */\n async getPrices(\n opts: { force?: boolean; signal?: AbortSignal } = {}\n ): Promise<PaywallPrice[]> {\n const b = await this.bootstrap(opts);\n return b.prices;\n }\n\n /** Sync-снимок цен из последнего bootstrap'а. null = ещё не загружали. */\n getCachedPrices(): PaywallPrice[] | null {\n return this.cachedBootstrap?.prices ?? null;\n }\n\n /**\n * Снимок того, какой язык SDK сейчас считает «языком юзера». Полезно для\n * синхронизации i18n хоста с тем, что фактически показывает пейвол — чтобы\n * окружающий UI не противоречил модалке (например, host рисует кнопку\n * \"Subscribe\" на английском, а пейвол показывает «Подписаться» на русском).\n *\n * Возвращает структуру, а не один тэг, чтобы интегратор мог:\n * - быстро взять `tag` для своих переводов;\n * - отличить «пейвол реально на этом языке» (`applied !== null`) от\n * «SDK угадал, но локали для этого языка нет — рендерится база»;\n * - решить, чему доверять при противоречии browserLanguage vs countryLanguage\n * (тур, expat, VPN — у каждого свой ответ).\n *\n * Sync-вызов: данные уже в bootstrap'е, отдельных запросов не делает.\n * Если `bootstrap()` ещё не вызывался — `applied` и `countryLanguage`\n * будут `null`, но `browserLanguage` и `tag` всё равно отдадутся, если\n * есть `navigator.language`.\n */\n getUserLanguage(): UserLanguageInfo {\n const browserLanguage =\n typeof navigator !== 'undefined' && navigator.language ? navigator.language : null;\n const countryLanguage = this.cachedBootstrap?.settings.locale_default ?? null;\n const applied = this.cachedBootstrap ? pickLocaleKey(this.cachedBootstrap) : null;\n const tag = applied ?? browserLanguage ?? countryLanguage;\n return { tag, applied, browserLanguage, countryLanguage };\n }\n\n /**\n * Получить актуальное состояние подписки/покупок.\n *\n * - In-memory cache TTL 5с — naïve setInterval(1000) не нагружает сервер.\n * - In-flight dedupe — параллельные вызовы получают один promise.\n * - `force: true` обходит кеш (для post-checkout проверки).\n * - Без identity возвращает empty-state (сервер тоже так делает).\n */\n async getUser(\n { force = false, signal }: { force?: boolean; signal?: AbortSignal } = {}\n ): Promise<PaywallUser> {\n if (!force && this.cachedUser && Date.now() - this.cachedUserAt < USER_CACHE_TTL_MS) {\n return this.cachedUser;\n }\n if (this.inflightUser) return this.inflightUser;\n\n this.inflightUser = (async () => {\n try {\n if (!this.identity?.email) {\n this.applyUser(EMPTY_USER);\n return EMPTY_USER;\n }\n const fresh = await this.api.request<PaywallUser>(\n `/api/v1/paywall/${this.paywallId}/user-state`,\n { headers: { 'X-User-Email': this.identity.email }, signal }\n );\n this.applyUser(fresh);\n return fresh;\n } finally {\n this.inflightUser = null;\n }\n })();\n\n return this.inflightUser;\n }\n\n /**\n * Подписка на изменения user-state. Колбек вызывается:\n * - сразу с last-known user (если есть в кеше) — по умолчанию через\n * microtask, опционально SYNC (см. опции);\n * - на каждое реальное изменение (getUser/bootstrap принёс другой shape).\n *\n * `opts.immediate`:\n * - `'microtask'` (default) — initial snapshot отдаётся в queueMicrotask,\n * чтобы host успел доресетнуть state в том же тике. Безопасный выбор\n * для большинства интеграций.\n * - `'sync'` — initial snapshot отдаётся прямо в текущем frame'е, до\n * возврата из onUserChange. Удобно для React/Vue useEffect-cleanup'а\n * (избегаем лишнего ре-рендера) и SSR (мгновенная синхронизация).\n * - `'none'` — не отдавать initial snapshot, только реальные изменения.\n *\n * Возвращает функцию отписки.\n */\n onUserChange(\n cb: UserListener,\n opts: { immediate?: 'microtask' | 'sync' | 'none' } = {}\n ): () => void {\n this.userListeners.add(cb);\n const mode = opts.immediate ?? 'microtask';\n if (this.cachedUser && mode !== 'none') {\n const snapshot = this.cachedUser;\n if (mode === 'sync') {\n try {\n cb(snapshot);\n } catch (e) {\n console.warn('[paywall] onUserChange initial sync threw', e);\n }\n } else {\n queueMicrotask(() => {\n if (this.userListeners.has(cb)) cb(snapshot);\n });\n }\n }\n return () => {\n this.userListeners.delete(cb);\n };\n }\n\n /** Текущий cached user без сетевого запроса. null = ещё не загружали. */\n getCachedUser(): PaywallUser | null {\n return this.cachedUser;\n }\n\n private applyUser(user: PaywallUser): void {\n const changed = !sameUser(this.cachedUser, user);\n this.cachedUser = user;\n this.cachedUserAt = Date.now();\n if (changed) {\n void this.persistUser(user);\n for (const cb of this.userListeners) {\n try {\n cb(user);\n } catch (e) {\n console.warn('[paywall] onUserChange listener threw', e);\n }\n }\n }\n }\n\n private storageKey(): string {\n return STORAGE_KEYS.userState(this.paywallId, identityKey(this.identity));\n }\n\n private async hydrateUserFromStorage(): Promise<void> {\n if (this.cachedUser) return;\n try {\n const raw = await this.storage.getItem(this.storageKey());\n if (!raw) return;\n const parsed = JSON.parse(raw) as { at: number; user: PaywallUser } | null;\n if (!parsed?.user) return;\n if (Date.now() - parsed.at > USER_PERSIST_TTL_MS) return;\n // Только если за это время никто не успел положить свежий — иначе\n // перетрём более актуальные данные.\n if (this.cachedUser) return;\n this.applyUser(parsed.user);\n } catch {\n /* corrupted entry — игнорируем, в сети возьмём свежий */\n }\n }\n\n private async persistUser(user: PaywallUser): Promise<void> {\n try {\n await this.storage.setItem(\n this.storageKey(),\n JSON.stringify({ at: Date.now(), user })\n );\n } catch {\n /* quota / disabled — не критично */\n }\n }\n\n /**\n * Балансы AI-провайдеров (`paywall_balances` × `tokenization_queries`).\n *\n * - In-memory cache TTL 5с — параллельные UI-renders не дёргают сеть;\n * - In-flight dedupe — параллельные `getBalances` получают один promise;\n * - `force: true` обходит кеш (типичный кейс — после QuotaExceededError);\n * - Без auth (Bearer не выдан) возвращает пустой массив без сетевого\n * запроса: бэк всё равно ответит 401, нет смысла тратить round-trip.\n *\n * Если у пейвола `tokenization=false` — бэк отдаёт `[]`, как для гостя.\n * SDK не различает «нет квоты» и «нет квот вообще» — caller сам решает\n * по `currentBalance` в QuotaExceededError или `balances.length`.\n */\n async getBalances(\n { force = false, signal }: { force?: boolean; signal?: AbortSignal } = {}\n ): Promise<Balance[]> {\n const now = Date.now();\n const age = this.cachedBalances ? now - this.cachedBalancesAt : Infinity;\n\n // Стабильный путь: cache свежий (in-memory 5с или persisted младше\n // BALANCES_STALE_THRESHOLD_MS). Возврат без сетевого запроса.\n if (\n !force &&\n this.cachedBalances &&\n (age < BALANCES_CACHE_TTL_MS || age < BALANCES_STALE_THRESHOLD_MS)\n ) {\n return this.cachedBalances;\n }\n\n // Stale-while-revalidate: cache есть, но возраст между\n // STALE_THRESHOLD и PERSIST_TTL. Возвращаем кэш мгновенно, в фоне\n // обновляем — listener'ы получат свежее через storage.watch +\n // applyBalances. Force пропускает эту ветку — caller ждёт свежее.\n if (\n !force &&\n this.cachedBalances &&\n age < BALANCES_PERSIST_TTL_MS\n ) {\n void this.fetchBalances({ signal }).catch(() => {\n /* swallow — fallback на cached, явный force даст следующую попытку */\n });\n return this.cachedBalances;\n }\n\n // Cache отсутствует или expired (>PERSIST_TTL) — блокирующий запрос.\n if (this.inflightBalances) return this.inflightBalances;\n return this.fetchBalances({ signal });\n }\n\n // Network primitive — единая точка для force/stale-revalidate/cold-start.\n // Дедуплицируется через `inflightBalances`.\n private fetchBalances({ signal }: { signal?: AbortSignal } = {}): Promise<Balance[]> {\n if (this.inflightBalances) return this.inflightBalances;\n this.inflightBalances = (async () => {\n try {\n // /balances требует Bearer. Без auth — пустой массив, listener'ы\n // не дёргаем (это shape «не загружали», а не «изменилось»).\n if (!this.auth) {\n this.applyBalances([]);\n return [];\n }\n const resp = await this.api.request<{\n balances: Balance[];\n tokenization: boolean;\n }>(`/api/v1/paywall/${this.paywallId}/balances`, { signal });\n const fresh = Array.isArray(resp.balances) ? resp.balances : [];\n this.applyBalances(fresh);\n return fresh;\n } finally {\n this.inflightBalances = null;\n }\n })();\n return this.inflightBalances;\n }\n\n /** Sync snapshot. null = ещё не загружали (или explicit clear на re-login). */\n getCachedBalances(): Balance[] | null {\n return this.cachedBalances;\n }\n\n /**\n * Подписка на изменения балансов: getBalances/decrementBalanceLocal/setIdentity.\n * `opts.immediate` работает так же, как в `onUserChange`: 'microtask'\n * (default), 'sync' (для React/Vue useEffect), 'none' (только изменения).\n * Возвращает unsubscribe.\n */\n onBalanceChange(\n cb: BalancesListener,\n opts: { immediate?: 'microtask' | 'sync' | 'none' } = {}\n ): () => void {\n this.balanceListeners.add(cb);\n const mode = opts.immediate ?? 'microtask';\n if (this.cachedBalances && mode !== 'none') {\n const snapshot = this.cachedBalances;\n if (mode === 'sync') {\n try {\n cb(snapshot);\n } catch (e) {\n console.warn('[paywall] onBalanceChange initial sync threw', e);\n }\n } else {\n queueMicrotask(() => {\n if (this.balanceListeners.has(cb)) cb(snapshot);\n });\n }\n }\n return () => {\n this.balanceListeners.delete(cb);\n };\n }\n\n /**\n * Оптимистично уменьшает count для `queryType` на 1 и нотифицирует\n * listener'ов. Используется ApiGatewayClient'ом сразу после успешного\n * gateway-вызова (бэк уже снял кредит, см. `chargeApiQueries`).\n *\n * Если queryType отсутствует в кеше или count<=0 — no-op (не уходим в\n * отрицательные значения, бэк всё равно правильный source-of-truth).\n * Если кеша нет вовсе — тоже no-op: явный getBalances({force:true}) на\n * следующем рендере подтянет актуальный shape.\n *\n * queryType может быть undefined (gateway не прислал X-Query-Type) —\n * в этом случае декремент не делаем, но просим refreshBalances() для\n * выравнивания.\n */\n decrementBalanceLocal(queryType: string | undefined): void {\n if (!queryType) {\n void this.getBalances({ force: true });\n return;\n }\n if (!this.cachedBalances) return;\n const idx = this.cachedBalances.findIndex((b) => b.type === queryType);\n if (idx < 0) return;\n const current = this.cachedBalances[idx];\n if (current.count <= 0) return;\n const next = this.cachedBalances.map((b, i) =>\n i === idx ? { ...b, count: b.count - 1 } : b\n );\n this.applyBalances(next);\n }\n\n /** Принудительный re-fetch — типичный вызов после QuotaExceededError, чтобы\n * UI получил актуальный balance=0 и нарисовал upgrade-prompt. */\n refreshBalances(): Promise<Balance[]> {\n return this.getBalances({ force: true });\n }\n\n /**\n * Фабрика ApiGatewayClient'а с подключённым к этому billing'у balance-стейтом:\n * - Bearer/identity берутся из текущего auth/identity;\n * - на success декрементим cachedBalances оптимистично;\n * - на 402 (QuotaExceededError) триггерим refreshBalances() для актуального snapshot'а.\n *\n * Если переопределить опции через `overrides` — принимаются как есть, но\n * `onChargeSuccess`/`onQuotaExceeded` всё равно вызываются (composable, host\n * может добавить свой колбек поверх).\n */\n createApiGatewayClient(\n overrides: Partial<\n Omit<ApiGatewayClientOptions, 'paywallId' | 'auth' | 'userId'>\n > = {}\n ): ApiGatewayClient {\n const userOnCharge = overrides.onChargeSuccess;\n const userOnQuota = overrides.onQuotaExceeded;\n return new ApiGatewayClient({\n paywallId: this.paywallId,\n apiOrigin: this.apiOrigin,\n auth: this.auth,\n userId: this.auth ? undefined : this.identity?.userId,\n capabilities: this.capabilities,\n fetch: this.fetchImpl,\n ...overrides,\n onChargeSuccess: (queryType) => {\n this.decrementBalanceLocal(queryType);\n userOnCharge?.(queryType);\n },\n onQuotaExceeded: (err) => {\n void this.refreshBalances();\n userOnQuota?.(err);\n }\n });\n }\n\n private applyBalances(balances: Balance[], { persist = true } = {}): void {\n const changed = !sameBalances(this.cachedBalances, balances);\n this.cachedBalances = balances;\n this.cachedBalancesAt = Date.now();\n // Persist даже если !changed — обновляем `at` чтобы другие контексты\n // считали кэш свежим (иначе они через 30с уйдут в сеть зря). persist=false\n // для пути «прилетело через storage.watch» — там кто-то уже записал.\n if (persist) void this.persistBalances(balances);\n if (changed) {\n for (const cb of this.balanceListeners) {\n try {\n cb(balances);\n } catch (e) {\n console.warn('[paywall] onBalanceChange listener threw', e);\n }\n }\n }\n }\n\n private balancesStorageKey(): string {\n return STORAGE_KEYS.balances(this.paywallId, identityKey(this.identity));\n }\n\n private async hydrateBalancesFromStorage(): Promise<void> {\n if (this.cachedBalances) return;\n try {\n const raw = await this.storage.getItem(this.balancesStorageKey());\n if (!raw) return;\n const parsed = JSON.parse(raw) as { at: number; balances: Balance[] } | null;\n if (!parsed?.balances || !Array.isArray(parsed.balances)) return;\n if (Date.now() - parsed.at > BALANCES_PERSIST_TTL_MS) return;\n // Race-защита: если за время `await` свежий уже прилетел из сети —\n // не перетираем.\n if (this.cachedBalances) return;\n this.cachedBalances = parsed.balances;\n this.cachedBalancesAt = parsed.at;\n for (const cb of this.balanceListeners) {\n try {\n cb(parsed.balances);\n } catch (e) {\n console.warn('[paywall] onBalanceChange listener threw', e);\n }\n }\n } catch {\n /* corrupted entry — игнорируем */\n }\n }\n\n private async persistBalances(balances: Balance[]): Promise<void> {\n try {\n await this.storage.setItem(\n this.balancesStorageKey(),\n JSON.stringify({ at: Date.now(), balances })\n );\n } catch {\n /* quota / disabled */\n }\n }\n\n // Cross-context sync: другая вкладка / popup / SW обновили balances\n // (свежий getBalances или оптимистичный decrement) → подхватываем без\n // сетевого запроса.\n private subscribeBalancesStorage(): void {\n if (typeof this.storage.watch !== 'function') return;\n this.balancesStorageUnwatch = this.storage.watch(\n this.balancesStorageKey(),\n (raw) => {\n if (!raw) return;\n try {\n const parsed = JSON.parse(raw) as { at: number; balances: Balance[] } | null;\n if (!parsed?.balances || !Array.isArray(parsed.balances)) return;\n // Если cached моложе или той же эпохи — наш свежее. Иначе applyBalances\n // без повторного persist (writer уже записал).\n if (parsed.at <= this.cachedBalancesAt) return;\n this.applyBalances(parsed.balances, { persist: false });\n } catch {\n /* corrupted entry — ignore */\n }\n }\n );\n }\n\n async createCheckout(params: {\n priceId: string;\n successUrl?: string;\n errorUrl?: string;\n shopUrl?: string;\n trialDays?: number;\n /**\n * Stage 1 защиты от дубликатов покупок. Идемпотентный ключ запроса\n * (UUID). Повторный вызов с тем же ключом вернёт тот же checkout-URL\n * без второго обращения к платёжному провайдеру. Если не передан —\n * SDK генерит UUID v4 сам и дедуплицирует параллельные клики по\n * `auto:${priceId}`.\n */\n idempotencyKey?: string;\n /** Renewal/upgrade flow — игнорирует у бэка проверку has_active_subscription.\n * По умолчанию /start-checkout возвращает 409 если у юзера уже есть\n * active subscription (защита от случайных двойных оплат). С\n * `ignoreActivePurchase: true` бэк создаёт новый checkout, прежняя\n * подписка отменится после успешной оплаты. Передавать только когда\n * юзер явно выбрал \"Renew/Upgrade\" в host-UI. */\n ignoreActivePurchase?: boolean;\n /** Отмена inflight-запроса. Параллельные вызовы дедуплицируются по\n * `inflightKey`, поэтому signal отменяет ВСЕ ожидающие на этот ключ —\n * это OK для типичного UX (юзер закрыл модалку — все checkout'ы отменены). */\n signal?: AbortSignal;\n }): Promise<CheckoutResult> {\n if (!this.identity?.email) {\n throw new PaywallError(\n 'identity_required',\n 'createCheckout requires identity with email'\n );\n }\n\n const inflightKey = params.idempotencyKey ?? `auto:${params.priceId}`;\n const existing = this.inflightCheckouts.get(inflightKey);\n if (existing) return existing;\n\n const idempotencyKey = params.idempotencyKey ?? generateUuid();\n\n // Бэк-контракт camelCase (online/app/api/v1/paywall/[id]/start-checkout/route.ts):\n // { email, priceId, successUrl, errorUrl, shopUrl, trial_days, userMeta, localCurrency }.\n // Response: { checkoutUrl, userId, acquiring } — маппим в SDK-shape { url, sessionId }.\n const headers: Record<string, string> = {\n 'Idempotency-Key': idempotencyKey\n };\n if (this.apiKey) headers['X-Api-Key'] = this.apiKey;\n\n // Settings из bootstrap'а — fallback для shopUrl/successUrl. Caller всё\n // ещё может перебить их явным аргументом (host-приложение со своим UX).\n const settings = this.cachedBootstrap?.settings;\n const successUrl = params.successUrl ?? settings?.success_redirect_url ?? undefined;\n const shopUrl = params.shopUrl ?? settings?.checkout_shop_url ?? undefined;\n\n const promise = this.api\n .request<{\n checkoutUrl: string;\n userId: string;\n // Бэк-контракт: имя acquirer'а, к которому ушёл checkout. SDK сам по\n // acquiring ничего не ветвит (URL открывается одним и тем же\n // window.open), но прокидывает его в CheckoutResult и в событие\n // `checkout_started` — чтобы host и /events-аналитика могли строить\n // конверсию по эквайрингам.\n acquiring: Acquiring;\n }>(`/api/v1/paywall/${this.paywallId}/start-checkout`, {\n method: 'POST',\n headers,\n signal: params.signal,\n body: JSON.stringify({\n email: this.identity.email,\n priceId: Number(params.priceId),\n successUrl,\n errorUrl: params.errorUrl,\n shopUrl,\n productName: settings?.checkout_product_name ?? undefined,\n trial_days: params.trialDays,\n ignoreActivePurchase: params.ignoreActivePurchase ? true : undefined,\n userMeta: this.identity.userId ? { userId: this.identity.userId } : undefined\n })\n })\n .then((resp): CheckoutResult => ({ url: resp.checkoutUrl, acquiring: resp.acquiring }))\n .catch((err): never => {\n // Бэк отдаёт 409 + `{ hasActivePurchase: true }` когда у юзера уже есть\n // активная подписка. Это не ошибка checkout-а — это сигнал «покажи\n // success/restored». Нормализуем в отдельный код, чтобы PaywallRoot\n // мог переключиться в purchase_success view без специфичной для этого\n // эндпоинта проверки status+payload.\n if (\n err instanceof PaywallError &&\n err.status === 409 &&\n err.cause &&\n typeof err.cause === 'object' &&\n (err.cause as { hasActivePurchase?: unknown }).hasActivePurchase === true\n ) {\n throw new PaywallError(\n 'already_purchased',\n 'You already have an active subscription',\n { status: 409, cause: err.cause }\n );\n }\n throw err;\n });\n\n this.inflightCheckouts.set(inflightKey, promise);\n // Очищаем после завершения, чтобы следующий клик после завершения\n // получил новый ключ и новый запрос. Параллельные ретраи во время\n // запроса при этом честно дедуплицируются на тот же promise.\n // .catch(() => {}) — финализатор не должен превращать reject promise'а\n // в unhandled rejection; caller createCheckout всё равно получит\n // исходный reject через `return promise`.\n promise\n .finally(() => {\n if (this.inflightCheckouts.get(inflightKey) === promise) {\n this.inflightCheckouts.delete(inflightKey);\n }\n })\n .catch(() => {});\n\n return promise;\n }\n\n /**\n * URL Stripe/Paddle/Chargebee customer portal — место, где залогиненный\n * юзер может управлять подпиской (отменить, обновить карту, скачать\n * инвойсы). Опен-флоу управляется host'ом:\n *\n * ```ts\n * const { url } = await billing.getCustomerPortalUrl();\n * window.open(url, '_blank');\n * ```\n *\n * Auth: Bearer (через AuthClient) или server-side `apiKey`. Без auth и\n * без apiKey бросает PaywallError('identity_required'). 403 от бэка\n * (нет активной подписки / acquiring не поддерживает portal) пробрасывается\n * как PaywallError('forbidden') с `status: 403` — host рендерит \"no\n * subscription to manage\".\n */\n async getCustomerPortalUrl(\n opts: { signal?: AbortSignal } = {}\n ): Promise<{ url: string }> {\n if (!this.auth && !this.apiKey && !this.identity?.email) {\n throw new PaywallError(\n 'identity_required',\n 'getCustomerPortalUrl requires auth, apiKey, or identity.email'\n );\n }\n const headers: Record<string, string> = {};\n if (this.apiKey) headers['X-Api-Key'] = this.apiKey;\n // Без Bearer — legacy путь: email/userMeta в body. С Bearer — бэк сам\n // достаёт email через GoTrue, body можно слать пустым.\n const body =\n this.auth && this.auth.getCachedSession()\n ? {}\n : {\n email: this.identity?.email,\n userMeta: this.identity?.userId\n ? { userId: this.identity.userId }\n : undefined\n };\n const resp = await this.api.request<{ url: string }>(\n `/api/v1/paywall/${this.paywallId}/get-customer-portal`,\n {\n method: 'POST',\n headers: Object.keys(headers).length ? headers : undefined,\n body: JSON.stringify(body),\n signal: opts.signal\n }\n );\n return { url: resp.url };\n }\n\n /**\n * Список покупок юзера с rich-полями (цена, валюта, interval, discount,\n * cancel-метаданные). Подходит для customer-portal UI: cards с кнопками\n * Cancel/Renew/Manage. Менее cache-friendly чем `getUser` — ходит в\n * `/api/v1/paywall/[id]/user` без unstable_cache, потому что list для UI\n * должен быть свежим после cancel-а.\n *\n * Auth: Bearer обязателен (через AuthClient). Без Bearer — 401 от бэка,\n * пробрасываем как PaywallError('http_401'). Гость → пустой список.\n */\n async listPurchases(\n opts: { signal?: AbortSignal } = {}\n ): Promise<PaywallPurchaseDetailed[]> {\n if (!this.auth) {\n throw new PaywallError(\n 'auth_required',\n 'listPurchases requires AuthClient (Bearer auth)'\n );\n }\n const resp = await this.api.request<{\n purchases: PaywallPurchaseDetailed[];\n }>(`/api/v1/paywall/${this.paywallId}/user`, {\n method: 'GET',\n signal: opts.signal\n });\n return resp.purchases ?? [];\n }\n\n /**\n * Отменить подписку. Бэк проверит что subscription принадлежит auth-юзеру\n * и сделает cancel у acquiring'а (Stripe/Paddle/Chargebee). По умолчанию\n * cancel в конце текущего периода — юзер сохраняет access до renewal date'ы.\n *\n * `reason` обязательна (валидация на бэке). Удобно собрать через select\n * причин в host-UI, как в legacy customer portal'е.\n *\n * Auth: Bearer обязателен.\n */\n async cancelSubscription(params: {\n subscriptionId: string;\n reason: string;\n signal?: AbortSignal;\n }): Promise<{\n subscription: {\n status: string | null;\n canceled_at: string | null;\n cancel_at: string | null;\n cancel_at_period_end: boolean | null;\n };\n }> {\n if (!this.auth) {\n throw new PaywallError(\n 'auth_required',\n 'cancelSubscription requires AuthClient (Bearer auth)'\n );\n }\n return this.api.request<{\n subscription: {\n status: string | null;\n canceled_at: string | null;\n cancel_at: string | null;\n cancel_at_period_end: boolean | null;\n };\n }>(`/api/paywall/cancel-subscription`, {\n method: 'POST',\n body: JSON.stringify({\n subscriptionId: params.subscriptionId,\n paywallId: this.paywallId,\n cancellationReason: params.reason\n }),\n signal: params.signal\n });\n }\n\n /**\n * Создаёт саппорт-тикет. Если есть `files` — multipart/form-data, иначе JSON.\n * Email берётся (1) из явного поля payload.email; (2) из identity если оно есть.\n * Если ни того, ни другого нет — бэк отвергнет тикет (`email_required`).\n *\n * Bearer-токен (если AuthClient подключён) добавляется автоматически — бэк\n * перевешивает customer_email на email из сессии (защита от подделки).\n */\n async createSupportTicket(payload: {\n subject: string;\n content: string;\n email?: string;\n files?: File[];\n }): Promise<{ ticket: { id: number; status: string } }> {\n const customerEmail = payload.email ?? this.identity?.email ?? null;\n const path = `/api/v1/paywall/${this.paywallId}/support/ticket`;\n const hasFiles = !!payload.files && payload.files.length > 0;\n if (hasFiles) {\n const form = new FormData();\n form.set('subject', payload.subject);\n form.set('content', payload.content);\n if (customerEmail) form.set('customer_email', customerEmail);\n for (const f of payload.files!) form.append('files', f);\n return this.api.request<{ ticket: { id: number; status: string } }>(path, {\n method: 'POST',\n body: form\n });\n }\n return this.api.request<{ ticket: { id: number; status: string } }>(path, {\n method: 'POST',\n body: JSON.stringify({\n subject: payload.subject,\n content: payload.content,\n customer_email: customerEmail\n })\n });\n }\n}\n\nfunction authUserToIdentity(user: AuthUser): Identity {\n return { email: user.email, userId: user.id };\n}\n\nfunction sameIdentity(a: Identity | undefined, b: Identity | undefined): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n return (\n a.email === b.email &&\n a.userId === b.userId &&\n a.anonymousId === b.anonymousId\n );\n}\n\nfunction buildDefaultLayout(settings: PaywallSettings, prices: PaywallPrice[]): Layout {\n return {\n type: 'modal',\n blocks: [\n { type: 'heading', text: settings.name || 'Upgrade', level: 1 },\n { type: 'price_grid', priceIds: prices.map((p) => p.id) },\n { type: 'cta_button', label: 'Continue', action: 'checkout' }\n ]\n };\n}\n\n/** Подбирает оверрайды по `navigator.language` (с fallback на base-tag и\n * на `settings.locale_default`). Возвращает первый существующий ключ из\n * карты — без normalize'а кейсов: ключи в bootstrap всё равно приходят\n * с бэка в едином формате. */\nfunction pickLocaleKey(bootstrap: PaywallBootstrap): string | null {\n const map = bootstrap.locales;\n if (!map) return null;\n const candidates: string[] = [];\n if (typeof navigator !== 'undefined') {\n if (navigator.language) candidates.push(navigator.language);\n const base = navigator.language?.split('-')[0];\n if (base && base !== navigator.language) candidates.push(base);\n }\n const fallback = bootstrap.settings.locale_default;\n if (fallback) candidates.push(fallback);\n for (const key of candidates) {\n if (key && Object.prototype.hasOwnProperty.call(map, key)) return key;\n }\n return null;\n}\n\nfunction applyLocaleOverrides(bootstrap: PaywallBootstrap): void {\n const key = pickLocaleKey(bootstrap);\n if (!key) return;\n const overrides: LocaleOverrides | undefined = bootstrap.locales?.[key];\n if (!overrides) return;\n if (overrides.layout) {\n bootstrap.layout = overrides.layout;\n }\n if (overrides.prices) {\n bootstrap.prices = bootstrap.prices.map((p) => {\n const o = overrides.prices?.[p.id];\n if (!o) return p;\n // Точечно перетираем только переданные поля, остальное оставляем как есть.\n // null в overrides — явный сброс (например, скрыть description в этой локали).\n const next: PaywallPrice = { ...p };\n if ('label' in o) next.label = o.label ?? null;\n if ('description' in o) next.description = o.description ?? null;\n return next;\n });\n }\n}\n","import { SDK_VERSION } from './api';\n\n// Аналитический трекер SDK 3.0. Принимает события (системные через\n// bindEventTracker и кастомные через PaywallUI.track()), копит в буфере и\n// батчем шлёт на /api/v1/paywall/{id}/events.\n//\n// Принципы:\n// - Fire-and-forget. Любая ошибка POST не должна влиять на UX.\n// - Бэк-нагрузка минимальна: батч ~10-20 событий за окно ~1.5с.\n// - sendBeacon на pagehide/visibilitychange — гарантирует доставку\n// \"последней мили\" при закрытии вкладки.\n// - Без headers в beacon-режиме (нельзя по спеке) — visitor_id/user_id/sdk\n// metadata дублируются в body как fallback. Сервер их умеет читать.\n\nexport interface TrackedEvent {\n type: string;\n ts: number;\n props?: Record<string, unknown>;\n}\n\nexport interface EventTrackerOptions {\n endpoint: string;\n paywallId: string;\n capabilities?: string[];\n getVisitorId: () => Promise<string>;\n getCachedVisitorId?: () => string | null;\n getUserId?: () => string | null | undefined;\n enabled?: boolean;\n flushIntervalMs?: number;\n maxBufferSize?: number;\n /** Тестовый override fetch'а. */\n fetch?: typeof fetch;\n /** Тестовый override sendBeacon'а — позволяет проверить unload-flow в jsdom. */\n sendBeacon?: (url: string, data: BodyInit) => boolean;\n}\n\nconst DEFAULT_FLUSH_INTERVAL_MS = 1500;\nconst DEFAULT_MAX_BUFFER_SIZE = 20;\n// Hard cap, чтобы фоновая запись не разрослась бесконечно при глухой сети.\nconst HARD_BUFFER_LIMIT = 200;\n\nexport class EventTracker {\n private opts: EventTrackerOptions;\n private buffer: TrackedEvent[] = [];\n private flushTimer: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n private unloadHandler: (() => void) | null = null;\n private visibilityHandler: (() => void) | null = null;\n\n constructor(opts: EventTrackerOptions) {\n this.opts = opts;\n if (this.isEnabled()) this.attachUnloadHandlers();\n }\n\n private isEnabled(): boolean {\n return this.opts.enabled !== false;\n }\n\n track(type: string, props?: Record<string, unknown>): void {\n if (this.destroyed || !this.isEnabled()) return;\n if (typeof type !== 'string' || type.length === 0) return;\n\n this.buffer.push({ type, ts: Date.now(), props });\n\n const max = this.opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n if (this.buffer.length >= max) {\n void this.flush();\n return;\n }\n if (this.buffer.length > HARD_BUFFER_LIMIT) {\n // Защита от утечки при недоступности сервера: дропаем самые старые.\n this.buffer = this.buffer.slice(-HARD_BUFFER_LIMIT);\n }\n this.scheduleFlush();\n }\n\n private scheduleFlush(): void {\n if (this.flushTimer || this.destroyed) return;\n const interval = this.opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n this.flushTimer = setTimeout(() => {\n this.flushTimer = null;\n void this.flush();\n }, interval);\n }\n\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = null;\n }\n\n const events = this.buffer;\n this.buffer = [];\n\n try {\n const visitorId = await this.opts.getVisitorId();\n const userId = this.opts.getUserId?.() ?? null;\n const body = JSON.stringify({ events });\n const fetchImpl = this.opts.fetch ?? (typeof fetch !== 'undefined' ? fetch : undefined);\n if (!fetchImpl) return;\n\n await fetchImpl(this.opts.endpoint, {\n method: 'POST',\n credentials: 'omit',\n keepalive: true, // если страница закроется в этот момент — браузер всё равно дотянет\n headers: this.buildHeaders(visitorId, userId),\n body\n });\n } catch {\n /* тихо: аналитика не должна мешать UX. Потеря события приемлема. */\n }\n }\n\n /**\n * Отправка через navigator.sendBeacon — для unload/pagehide. Гарантированно\n * долетает (POST с keepalive тоже почти, но beacon сделан именно под это).\n * Headers ставить нельзя (спецификация), поэтому SDK metadata едет в body\n * как fallback-поля, которые сервер читает в дополнение к headers.\n */\n flushBeacon(): void {\n if (this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n const visitorId = this.opts.getCachedVisitorId?.() ?? null;\n const userId = this.opts.getUserId?.() ?? null;\n\n // Если visitor_id ещё не зарезолвили (редкий race на ранней секунде жизни) —\n // вернём события в буфер и вызовем обычный flush с keepalive-fetch'ом.\n if (!visitorId) {\n this.buffer.unshift(...events);\n void this.flush();\n return;\n }\n\n const body = JSON.stringify({\n events,\n // body-level дубликаты для beacon-flow, читаются сервером как fallback.\n visitor_id: visitorId,\n user_id: userId,\n sdk_version: SDK_VERSION,\n paywall_id: this.opts.paywallId,\n capabilities: this.opts.capabilities?.join(',') ?? ''\n });\n\n const beacon =\n this.opts.sendBeacon ??\n (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function'\n ? navigator.sendBeacon.bind(navigator)\n : null);\n\n if (!beacon) {\n // Возвращаем events в буфер — обычный flush через keepalive подберёт.\n this.buffer.unshift(...events);\n void this.flush();\n return;\n }\n\n try {\n // text/plain — sendBeacon обычно ставит этот тип, сервер парсит вручную.\n const ok = beacon(this.opts.endpoint, body);\n if (!ok) {\n this.buffer.unshift(...events);\n void this.flush();\n }\n } catch {\n this.buffer.unshift(...events);\n void this.flush();\n }\n }\n\n private buildHeaders(visitorId: string, userId: string | null): Record<string, string> {\n const h: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-SDK-Version': SDK_VERSION,\n 'X-Paywall-Id': this.opts.paywallId,\n 'X-Visitor-Id': visitorId\n };\n if (this.opts.capabilities?.length) {\n h['X-SDK-Capabilities'] = this.opts.capabilities.join(',');\n }\n if (userId) h['X-User-Id'] = userId;\n return h;\n }\n\n private attachUnloadHandlers(): void {\n if (typeof window === 'undefined') return;\n\n this.unloadHandler = () => this.flushBeacon();\n this.visibilityHandler = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n this.flushBeacon();\n }\n };\n\n // pagehide — основной путь (стабильнее чем unload, работает в bfcache).\n window.addEventListener('pagehide', this.unloadHandler);\n // visibilitychange/hidden — дополнительный, на iOS Safari часто единственный.\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n }\n\n private detachUnloadHandlers(): void {\n if (typeof window === 'undefined') return;\n if (this.unloadHandler) window.removeEventListener('pagehide', this.unloadHandler);\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n }\n this.unloadHandler = null;\n this.visibilityHandler = null;\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = null;\n }\n void this.flush();\n this.detachUnloadHandlers();\n }\n}\n","import type { StorageAdapter } from '../storage';\nimport type { OpensTrialStatus, TimeTrialStatus, TrialConfig, TrialStatus } from '../types';\nimport type { TrialStore } from './TrialStore';\n\nconst HOUR_MS = 60 * 60 * 1000;\n\n// Ключи 1-в-1 как в legacy v2 (online/components/PayWallIframeOpener.tsx) —\n// миграция с v2 на SDK 3.0 не сбрасывает прогресс триала у действующих юзеров.\nfunction timeKey(paywallId: string): string {\n return `paywall-${paywallId}-trial-time-first-open`;\n}\nfunction opensKey(paywallId: string): string {\n return `paywall-${paywallId}-skip-times`;\n}\n\nexport class LocalTrialStore implements TrialStore {\n constructor(\n private readonly storage: StorageAdapter,\n private readonly paywallId: string,\n private readonly config: TrialConfig\n ) {}\n\n async check(): Promise<TrialStatus> {\n if (this.config.mode === 'time') return this.checkTime();\n return this.checkOpens();\n }\n\n async recordBlock(): Promise<TrialStatus> {\n if (this.config.mode === 'time') return this.recordTime();\n return this.recordOpens();\n }\n\n async reset(): Promise<void> {\n await this.storage.removeItem(this.config.mode === 'time' ? timeKey(this.paywallId) : opensKey(this.paywallId));\n }\n\n private async checkTime(): Promise<TimeTrialStatus> {\n const totalMs = this.config.payload * HOUR_MS;\n const raw = await this.storage.getItem(timeKey(this.paywallId));\n const startedAt = raw ? Number(raw) : null;\n if (!startedAt || !Number.isFinite(startedAt)) {\n // Триал ещё не стартовал — первый open() считается активным триалом\n // (паывол не покажется, мы запишем firstOpen в recordBlock()).\n return {\n mode: 'time',\n blocked: true,\n startedAt: null,\n expiresAt: null,\n remainingMs: totalMs,\n totalMs\n };\n }\n const expiresAt = startedAt + totalMs;\n const remainingMs = Math.max(0, expiresAt - Date.now());\n return {\n mode: 'time',\n blocked: remainingMs > 0,\n startedAt,\n expiresAt,\n remainingMs,\n totalMs\n };\n }\n\n private async checkOpens(): Promise<OpensTrialStatus> {\n const total = this.config.payload;\n const raw = await this.storage.getItem(opensKey(this.paywallId));\n const used = raw ? Number(raw) : 0;\n const safeUsed = Number.isFinite(used) ? used : 0;\n // v2-семантика: `paywall-${id}-skip-times` хранит число уже выполненных\n // блокировок. Триал активен, пока `used < total`. payload=3, used=0..2 —\n // ещё блокируем; used=3 — следующий open() покажет паывол.\n const blocked = safeUsed < total;\n const remaining = Math.max(0, total - safeUsed);\n return {\n mode: 'opens',\n blocked,\n remainingActions: remaining,\n totalActions: total\n };\n }\n\n private async recordTime(): Promise<TimeTrialStatus> {\n const totalMs = this.config.payload * HOUR_MS;\n const key = timeKey(this.paywallId);\n const raw = await this.storage.getItem(key);\n let startedAt = raw ? Number(raw) : null;\n if (!startedAt || !Number.isFinite(startedAt)) {\n startedAt = Date.now();\n await this.storage.setItem(key, String(startedAt));\n }\n const expiresAt = startedAt + totalMs;\n const remainingMs = Math.max(0, expiresAt - Date.now());\n return {\n mode: 'time',\n blocked: remainingMs > 0,\n startedAt,\n expiresAt,\n remainingMs,\n totalMs\n };\n }\n\n private async recordOpens(): Promise<OpensTrialStatus> {\n const total = this.config.payload;\n const key = opensKey(this.paywallId);\n const raw = await this.storage.getItem(key);\n const used = raw ? Number(raw) : 0;\n const safeUsed = Number.isFinite(used) ? used : 0;\n // Не инкрементим выше total — counter становится «sticky at total» после\n // истечения, чтобы повторные `recordBlock()` (если вдруг вызовется уже на\n // expired-триале) не разъезжались с `check()`.\n const next = Math.min(total, safeUsed + 1);\n await this.storage.setItem(key, String(next));\n const remaining = Math.max(0, total - next);\n return {\n mode: 'opens',\n blocked: next < total,\n remainingActions: remaining,\n totalActions: total\n };\n }\n}\n","import type { StorageAdapter } from '../storage';\nimport type { TrialConfig, TrialStatus } from '../types';\nimport { LocalTrialStore } from './LocalTrialStore';\nimport type { TrialStore } from './TrialStore';\n\nlet warned = false;\n\n/**\n * Стаб серверного хранилища триала. Делегирует в {@link LocalTrialStore} —\n * SDK работает корректно, но фактически state живёт у клиента, а не на бэке.\n *\n * Когда появится серверный endpoint (`/api/v1/paywall/{id}/trial-state`),\n * заменим internals: GET для `check()`, POST для `recordBlock()`. Публичный\n * контракт `TrialStore` не меняется — `paywall.open()` flow в PaywallUI\n * трогать не придётся.\n *\n * Пока админка кладёт `settings.trial.storage = 'server'`, мы выводим один\n * console.warn и продолжаем как с `'client'`. Это позволяет владельцу пейвола\n * включить тоггл в админке заранее и проверить, что SDK не падает.\n */\nexport class ServerTrialStore implements TrialStore {\n private readonly fallback: LocalTrialStore;\n\n constructor(storage: StorageAdapter, paywallId: string, config: TrialConfig) {\n if (!warned) {\n warned = true;\n console.warn(\n '[paywall] trial.storage=\"server\" is not implemented yet — falling back to client storage. ' +\n 'State lives in localStorage; users can reset trial by clearing site data.'\n );\n }\n this.fallback = new LocalTrialStore(storage, paywallId, config);\n }\n\n check(): Promise<TrialStatus> {\n return this.fallback.check();\n }\n\n recordBlock(): Promise<TrialStatus> {\n return this.fallback.recordBlock();\n }\n\n reset(): Promise<void> {\n return this.fallback.reset();\n }\n}\n","import type { StorageAdapter } from '../storage';\nimport type { TrialConfig } from '../types';\nimport { LocalTrialStore } from './LocalTrialStore';\nimport { ServerTrialStore } from './ServerTrialStore';\nimport type { TrialStore } from './TrialStore';\n\nexport type { TrialStore } from './TrialStore';\nexport { LocalTrialStore } from './LocalTrialStore';\nexport { ServerTrialStore } from './ServerTrialStore';\n\n/** Резолвит реализацию TrialStore по `settings.trial.storage` из bootstrap.\n * null/undefined config — каллер должен это проверять сам и не вызывать\n * фабрику (триал отключён → store вообще не нужен). */\nexport function createTrialStore(\n storage: StorageAdapter,\n paywallId: string,\n config: TrialConfig\n): TrialStore {\n if (config.storage === 'server') {\n return new ServerTrialStore(storage, paywallId, config);\n }\n return new LocalTrialStore(storage, paywallId, config);\n}\n","// Wire-protocol для коммуникации content-script ↔ service worker ↔ offscreen.\n//\n// Дизайн-цели:\n// 1. Типизированный request/response — каждый метод BillingClient/AuthClient\n// имеет свой `kind` с JSON-сериализуемыми params и result.\n// 2. Push-события (broadcast) — onUserChange/onAuthChange/track-результаты\n// летят от offscreen ко всем подключённым content-script'ам без request'а.\n// 3. Reconnection-friendly — каждое сообщение самодостаточно (нет shared state\n// в port'е, который ломается при смерти SW). Только request_id для match'а.\n// 4. Errors как данные — PaywallError сериализуется в плоский JSON, content\n// reconstruct'ит на своей стороне (instanceof работает).\n//\n// Транспорт под капотом — chrome.runtime.connect (long-lived port). Маршрут:\n// content_script → SW (forwarder) → offscreen. SW не хранит state, только\n// мапит port'ы content↔offscreen и поднимает offscreen если тот мёртв.\n\n/** Версия протокола — bump'аем при breaking changes wire-формата. SW сравнивает\n * версии при handshake'е и отказывается маршрутить, если версии разъехались\n * (extension и SDK обновлены не одновременно). */\nexport const PROTOCOL_VERSION = 1 as const;\n\n// === Request/Response ===\n\n/** Сообщение отмены: клиент послал cancel(id), сервер ищет соответствующий\n * AbortController в активных запросах и abort'ит его. Не имеет ответа —\n * fire-and-forget. */\nexport interface CancelEnvelope {\n type: 'cancel';\n id: string;\n}\n\nexport type RequestKind =\n // BillingClient\n | 'billing.bootstrap'\n | 'billing.getCachedBootstrap'\n | 'billing.getUser'\n | 'billing.getCachedUser'\n | 'billing.getBalances'\n | 'billing.getCachedBalances'\n | 'billing.createCheckout'\n | 'billing.listPurchases'\n | 'billing.cancelSubscription'\n | 'billing.getIdentity'\n | 'billing.setIdentity'\n | 'billing.getVisitorId'\n // AuthClient\n | 'auth.signInWithEmail'\n | 'auth.signUp'\n | 'auth.signOut'\n | 'auth.getCachedSession'\n | 'auth.refresh'\n | 'auth.requestPasswordReset'\n | 'auth.updatePassword'\n | 'auth.sendOtp'\n | 'auth.verifyOtp'\n | 'auth.resendConfirmation'\n | 'auth.revokeAllSessions'\n | 'auth.oauthStart'\n | 'auth.oauthExchange'\n | 'auth.getAccessToken'\n | 'auth.signInAnonymously'\n // EventTracker\n | 'tracker.track'\n // Storage proxy — для consumer'ов, которые через `billing.getStorage()`\n // хотят single-source-of-truth storage. Offscreen'овский localStorage\n // шарится между всеми content-script'ами.\n | 'storage.get'\n | 'storage.set'\n | 'storage.remove'\n // Trial-store — read-modify-write атомарно в offscreen (через navigator.locks).\n // Альтернативный путь к storage-proxy: вместо двух независимых операций\n // get+set, recordBlock делает их атомарно за один RPC.\n | 'trial.check'\n | 'trial.recordBlock'\n | 'trial.reset'\n // Internal\n | 'handshake'\n | 'subscribe'\n | 'unsubscribe';\n\nexport interface RequestEnvelope<P = unknown> {\n type: 'request';\n id: string;\n kind: RequestKind;\n params: P;\n}\n\nexport interface ResponseOk<R = unknown> {\n type: 'response';\n id: string;\n ok: true;\n result: R;\n}\n\nexport interface ResponseErr {\n type: 'response';\n id: string;\n ok: false;\n error: SerializedError;\n}\n\nexport type ResponseEnvelope<R = unknown> = ResponseOk<R> | ResponseErr;\n\n// === Events (broadcast) ===\n\nexport type EventKind =\n | 'userChange' // billing.onUserChange tick\n | 'authChange' // auth.onAuthChange tick\n | 'balancesChange'; // balance refresh broadcast\n\nexport interface EventEnvelope<P = unknown> {\n type: 'event';\n kind: EventKind;\n payload: P;\n}\n\n// === Errors as data ===\n\n/** Плоский снепшот PaywallError, который переживает structured cloning.\n * Reconstruct происходит в RemoteBillingClient'е через `new PaywallError(...)` —\n * чтобы у host'а `error instanceof PaywallError` работал как обычно. */\nexport interface SerializedError {\n name: string;\n code: string;\n message: string;\n status?: number;\n /** Stack из offscreen'а — для отладки в DevTools content-script'а. */\n stack?: string;\n}\n\n// === Discriminated union ===\n\nexport type Envelope =\n | RequestEnvelope\n | ResponseEnvelope\n | EventEnvelope\n | CancelEnvelope;\n\n// === Type helpers ===\n\nexport type RequestParams<K extends RequestKind> =\n K extends keyof RequestParamsMap ? RequestParamsMap[K] : unknown;\n\nexport type RequestResult<K extends RequestKind> =\n K extends keyof RequestResultMap ? RequestResultMap[K] : unknown;\n\nexport type EventPayload<K extends EventKind> =\n K extends keyof EventPayloadMap ? EventPayloadMap[K] : unknown;\n\n// Карты params/result/payload для каждого RequestKind/EventKind заполняются\n// в messages.ts — там они привязаны к конкретным типам из @sdk/core/types\n// (PaywallBootstrap и т.д.). Здесь только generic-каркас, без import-cycle\n// на UI-пакет.\nexport interface RequestParamsMap {}\nexport interface RequestResultMap {}\nexport interface EventPayloadMap {}\n","import { PaywallError } from '@sdk/core/types';\nimport type { SerializedError } from './protocol';\n\n// Сериализация PaywallError в плоский JSON для chrome.runtime messaging\n// (Error через structured cloning теряет class identity — instanceof ломается).\n// Reconstruct на content-стороне восстанавливает PaywallError, host'ы пишут\n// `if (e instanceof PaywallError)` как обычно.\n\nexport function serializeError(error: unknown): SerializedError {\n if (error instanceof PaywallError) {\n return {\n name: 'PaywallError',\n code: error.code,\n message: error.message,\n status: error.status,\n stack: error.stack\n };\n }\n if (error instanceof Error) {\n return {\n name: error.name || 'Error',\n code: 'unknown',\n message: error.message,\n stack: error.stack\n };\n }\n return {\n name: 'Error',\n code: 'unknown',\n message: typeof error === 'string' ? error : 'Unknown error'\n };\n}\n\nexport function reconstructError(s: SerializedError): Error {\n if (s.name === 'PaywallError') {\n const err = new PaywallError(s.code, s.message, { status: s.status });\n if (s.stack) err.stack = s.stack;\n return err;\n }\n const err = new Error(s.message);\n err.name = s.name;\n if (s.stack) err.stack = s.stack;\n return err;\n}\n","// Адаптер chrome.runtime.Port → MessageChannel. Единственная точка кода,\n// где живёт chrome.* — содержит и client, и server side runtime.connect /\n// onConnect API. Тестируется только в extension-runtime (e2e), unit-тесты\n// shared/transport-*.ts работают с in-memory MessageChannel реализацией.\n\nimport type { MessageChannel } from './channel';\nimport type { Envelope } from './protocol';\n\n/** Обернуть существующий port в MessageChannel. Используется на server-side\n * (offscreen / SW) — там port уже создан onConnect-listener'ом. */\nexport function portToChannel(port: chrome.runtime.Port): MessageChannel {\n let disconnected = false;\n const messageCbs = new Set<(envelope: Envelope) => void>();\n const disconnectCbs = new Set<() => void>();\n\n const onMessageListener = (msg: unknown): void => {\n for (const cb of messageCbs) cb(msg as Envelope);\n };\n const onDisconnectListener = (): void => {\n if (disconnected) return;\n disconnected = true;\n for (const cb of disconnectCbs) cb();\n port.onMessage.removeListener(onMessageListener);\n port.onDisconnect.removeListener(onDisconnectListener);\n };\n\n port.onMessage.addListener(onMessageListener);\n port.onDisconnect.addListener(onDisconnectListener);\n\n return {\n send(envelope) {\n if (disconnected) return;\n try {\n port.postMessage(envelope);\n } catch (e) {\n // postMessage кидает если port уже закрыт. Эмулируем disconnect, чтобы\n // TransportClient/Server не висели на in-flight request'ах.\n onDisconnectListener();\n throw e;\n }\n },\n onMessage(cb) {\n messageCbs.add(cb);\n return () => messageCbs.delete(cb);\n },\n onDisconnect(cb) {\n if (disconnected) {\n // Идемпотентно — late subscribers сразу получают сигнал.\n queueMicrotask(cb);\n return () => {};\n }\n disconnectCbs.add(cb);\n return () => disconnectCbs.delete(cb);\n },\n close() {\n if (disconnected) return;\n port.disconnect();\n onDisconnectListener();\n }\n };\n}\n\n/** Client-side фабрика канала: открыть port на extension'овский runtime по имени.\n * Принимающая сторона — service worker (chrome.runtime.onConnect там). */\nexport function createRuntimeChannel(portName: string): MessageChannel {\n const port = chrome.runtime.connect({ name: portName });\n return portToChannel(port);\n}\n"],"names":["PaywallError","code","message","opts","QuotaExceededError","input","SDK_VERSION","ApiClient","path","init","url","fetchImpl","headers","token","isFormBody","response","cause","payload","hasChromeStorage","chromeLocal","key","resolve","items","v","value","cb","onChanged","handler","changes","area","change","webLocal","e","memoryMap","memoryLocal","createStorage","override","STORAGE_KEYS","paywallId","identityKey","generateVisitorId","c","bytes","i","hex","b","ensureVisitorId","storage","existing","id","randomBytes","len","base64url","bin","generateCodeVerifier","deriveCodeChallenge","verifier","enc","hash","generateState","DEFAULT_API_ORIGIN","REFRESH_LEEWAY_MS","OAUTH_FLOW_TTL_MS","AuthClient","name","raw","parsed","visitorId","resp","session","accessToken","resumed","user","rt","fallbackUser","current","updatedUser","updatedSession","authorize_url","state","popup","waitForOAuthCode","challenge","flow","cutoff","k","refreshToken","currentUser","wasAnonymous","snapshot","s","expiresAt","before","sameSession","OAUTH_TIMEOUT_MS","OAUTH_POLL_MS","expectedState","reject","settled","cleanup","onMessage","closedTimer","timeoutTimer","data","closed","a","ApiGatewayClient","params","isFormData","isBlob","isStream","isString","body","detail","err","parseQuotaError","tryReadErrorCode","queryType","rawBalances","balances","first","USER_CACHE_TTL_MS","USER_PERSIST_TTL_MS","BOOTSTRAP_PERSIST_TTL_MS","BOOTSTRAP_STALE_THRESHOLD_MS","EMPTY_USER","identity","sameUser","BALANCES_CACHE_TTL_MS","BALANCES_PERSIST_TTL_MS","BALANCES_STALE_THRESHOLD_MS","sameBalances","BillingClient","authUser","authUserToIdentity","next","sameIdentity","forceOrOpts","now","cacheFresh","partial","base","merged","buildDefaultLayout","applyLocaleOverrides","bootstrap","signal","persist","versionChanged","_user","rest","browserLanguage","countryLanguage","applied","pickLocaleKey","force","fresh","mode","changed","age","idx","overrides","userOnCharge","userOnQuota","inflightKey","generateUuid","settings","successUrl","shopUrl","promise","customerEmail","form","f","prices","p","map","candidates","fallback","o","DEFAULT_FLUSH_INTERVAL_MS","DEFAULT_MAX_BUFFER_SIZE","HARD_BUFFER_LIMIT","EventTracker","type","props","max","interval","events","userId","beacon","h","HOUR_MS","timeKey","opensKey","LocalTrialStore","config","totalMs","startedAt","remainingMs","total","used","safeUsed","blocked","remaining","warned","ServerTrialStore","createTrialStore","PROTOCOL_VERSION","serializeError","error","reconstructError","portToChannel","port","disconnected","messageCbs","disconnectCbs","onMessageListener","msg","onDisconnectListener","envelope","createRuntimeChannel","portName"],"mappings":"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"}
|