{"version":3,"sources":["../../src/detect/error-presenter.ts","../../src/detect/api.ts","../../src/detect/index.ts"],"names":["CARD_ID","DEDUPE_WINDOW_MS","lastSignature","lastShownAt","esc","s","d","isCodedError","error","EN_STRINGS","resolve","input","pack","titleFor","code","bodyFor","body","sanitizeNativeMessage","RETRIABLE_CODES","classifyThrown","presentError","errorOrMessage","options","resolveStrings","resolved","now","existing","operatorName","dismissLabel","retryLabel","showRetry","customStyle","k","v","card","heading","dismiss","API_BASE","buildEventData","reportEvent","apiKey","event","_data","validateApiKey","res","dispatchInstallState","state","BEACIO_EVENTS","maybeShowBanner","bannerStateOverride","showInstallBanner","bannerConfig","bannerOpts","isOriginDenied","bt","isPrivateBrowsingBestEffort","storage","probeKey","initBeacio","getExtensionInstallState","isIOSHomeScreenWebApp","isIOSSafari","homeScreenWebApp","installState","buildSetupDeepLink","config","returnUrl","SETUP_URL","resolveOnboardingState","returnLink","buildReturnLink","setupUrl","pageUrl","buildOpenInSafariUrl","buildOnboardingUrl","APP_STORE_URL"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @beacio/detect#presentError — SB-SDK-05\n *\n * A drop-in, framework-free branded ERROR presenter. The polished branded surface\n * already exists for the INSTALL prompt (banner.ts); this is its sibling for the\n * FAILURE path. S&B (and any vanilla-JS site) uses raw `navigator.bluetooth`\n * across hundreds of call sites and will not rewrite them — so the worst surface,\n * a blocking, stack-leaking `window.alert()`, is converted into a non-blocking,\n * dismissible, recovery-oriented card with a ~1-line edit:\n *\n *   catch (error) { beacioDetect.presentError(error); }\n *\n * Design constraints (mirroring banner.ts):\n *  - Errors are consumed STRUCTURALLY — anything carrying a `.code` / `.message`\n *    / `.suggestion` / `.isRetriable` is understood — so a host page can hand us\n *    a BeacioError-shaped object without owning the class. The CLASSIFICATION of\n *    a raw DOMException, the code union and the retriable set come from\n *    `../error-taxonomy`, the leaf module the SDK's own `BeacioError.from` uses:\n *    one classifier, so the card and the caller's `switch` can never disagree.\n *    (Until 2026-08-12 they were hand-copied twins, justified by an\n *    \"@beacio/core is an OPTIONAL peer of @beacio/detect\" rule that no longer\n *    exists — there is no `packages/detect`, `./detect` is a subpath export of\n *    core, and these modules already import `../events` / `../urls`. The twins\n *    had diverged on eleven inputs by the time they were collapsed.)\n *  - The card NEVER leaks a stack trace, internal codes, WebKit jargon, or a\n *    competitor name. The friendly body comes from the per-code copy table, NOT\n *    the raw error string.\n *  - Identical errors fired in a short window are coalesced to ONE card (defends\n *    against the backgrounded alert-storm).\n *  - All user-visible strings are overridable via a copy/locale object\n *    (PresentErrorOptions.strings) — the i18n seam SB-SDK-07 converges on; the\n *    `lang` field selects a built-in pack (German shipped), and `strings`\n *    deep-merges over it. English defaults apply when neither is supplied (no\n *    regression). The per-code copy + dismiss/retry come from the SAME shared\n *    i18n module the install banner uses (./i18n), so a localized card and a\n *    localized banner never drift.\n */\n\n// The ONE error taxonomy: the code union, the retriable set (a retriable card\n// shows a retry affordance), the classifier for raw DOMExceptions, and the\n// native-message sanitiser. A leaf module by design — it pulls in neither\n// BeacioError nor the SUGGESTIONS table nor withRetry, so the zero-config\n// browser-auto drop-in stays inside its gzip budget.\nimport { classifyThrown, RETRIABLE_CODES, sanitizeNativeMessage, type BeacioErrorCode } from '../error-taxonomy';\n// SB-SDK-07: the shared localized-string seam (same module the install banner\n// consumes).\nimport { type DeepPartial, EN_STRINGS, type ErrorStrings, type LocaleStrings, resolveStrings } from './i18n';\n\n// Part of `@beacio/core/detect`'s published type surface (detect/index.ts).\nexport type { BeacioErrorCode };\n\n/**\n * Per-code friendly headline + body. Plain-English, recovery-oriented, no internal\n * codes, no jargon, no competitor names. This is the body shown to the user — the\n * raw error string (which may carry a stack or a competitor name) is NEVER shown.\n *\n * SB-SDK-07: there is no local copy table. The presenter's English\n * source-of-truth and the shared i18n pack are ONE table (EN_STRINGS.error), so\n * they cannot drift; localized rendering reads the RESOLVED pack (which may be\n * German). `isCodedError` therefore anchors code membership on\n * `EN_STRINGS.error.titles` directly. Completeness needs no test: the packs are\n * typed `Record<BeacioErrorCode, string>` against the ONE union, so a new code\n * fails i18n.ts's compile before any test runs.\n */\n\n/**\n * Options for {@link presentError}. Parity with BannerOptions where it overlaps\n * (operatorName, style), plus the retry affordance + the copy/locale seam.\n */\nexport interface PresentErrorOptions {\n  /** Operator/app name shown in the card (e.g. \"STORZ & BICKEL\"). */\n  operatorName?: string;\n  /**\n   * SB-SDK-07: BCP-47 UI language (e.g. 'de'). Selects the built-in pack for the\n   * per-code title/body + dismiss/retry labels; omitted ⇒ derived from\n   * navigator.language, else English. A per-call `strings` (and the explicit\n   * dismissText/retryText) still override the selected pack. Always wins over\n   * navigator.language.\n   */\n  lang?: string;\n  /** Retry button label override (takes precedence over strings.retry). */\n  retryText?: string;\n  /** Dismiss button label override (takes precedence over strings.dismiss). */\n  dismissText?: string;\n  /**\n   * Invoked when the user taps the retry affordance (retriable errors only), so a\n   * caller can re-run its connect()/operation. The card is dismissed first.\n   */\n  onRetry?: () => void;\n  /** Extra inline styles merged onto the card container. */\n  style?: Record<string, string>;\n  /**\n   * Copy/locale overrides for every user-visible string (SB-SDK-07 seam),\n   * deep-merged over the selected language pack by the SAME `resolveStrings`\n   * the install banner uses — so the seam also covers the per-code `titles` and\n   * the `generic` fallback, not just `messages`/`dismiss`/`retry`. Every field is\n   * optional; an omitted field keeps the pack's value, so a caller that passes\n   * nothing is byte-identical to today.\n   */\n  strings?: DeepPartial<ErrorStrings>;\n}\n\nconst CARD_ID = 'beacio-error';\n/** Coalesce window for identical errors (ms) — defends the alert-storm. */\nconst DEDUPE_WINDOW_MS = 1500;\n\n/** Last rendered signature + timestamp, for identical-error debounce. */\nlet lastSignature: string | null = null;\nlet lastShownAt = 0;\n\n/** HTML-escape (same idiom as banner.ts esc()). */\nfunction esc(s: string): string {\n  const d = document.createElement('div');\n  d.textContent = s;\n  return d.innerHTML;\n}\n\n/** Does an unknown value look like a BeacioError (structural, no class import)? */\nfunction isCodedError<T>(error: T): error is T & { code: BeacioErrorCode; message?: string; suggestion?: string; isRetriable?: boolean } {\n  return (\n    typeof error === 'object' &&\n    error !== null &&\n    'code' in error &&\n    typeof (error as { code: string }).code === 'string' &&\n    (error as { code: string }).code in EN_STRINGS.error.titles\n  );\n}\n\ninterface Resolved {\n  /** Stable code when known (drives retriable + dedupe signature), else null. */\n  code: BeacioErrorCode | null;\n  title: string;\n  body: string;\n  isRetriable: boolean;\n  /** Dedupe signature: identical inputs coalesce to one card. */\n  signature: string;\n}\n\n/**\n * Normalise any input — a BeacioError-shaped object, a raw DOMException/Error, or\n * a bare string — into branded, stack-free, competitor-free card content. The raw\n * error string is shown ONLY for a bare-string input (the S&B generateErrorMsg\n * path, where the caller passes its own already-friendly message); structured\n * errors always use the per-code copy table so no native jargon/stack leaks.\n */\nfunction resolve(input: unknown, pack: LocaleStrings['error']): Resolved {\n  // SB-SDK-07: per-code title/body come from the RESOLVED pack — which the caller's\n  // `strings` has already been deep-merged into, so a per-call override still wins.\n  const titleFor = (code: BeacioErrorCode): string => pack.titles[code];\n  const bodyFor = (code: BeacioErrorCode): string => pack.messages[code];\n\n  // Bare string: the caller's own message IS the body (generateErrorMsg path) —\n  // but sanitise it first (AC1/AC6) so a string carrying a stack, a native URL, or\n  // a competitor name never renders verbatim. When nothing meaningful survives,\n  // fall back to the branded generic body so the card is never blank.\n  if (typeof input === 'string') {\n    const clean = sanitizeNativeMessage(input);\n    const body = clean || pack.generic.body;\n    return { code: null, title: pack.generic.title, body, isRetriable: false, signature: `str:${body}` };\n  }\n\n  // BeacioError-shaped (structural): trust .code for copy + retriable.\n  if (isCodedError(input)) {\n    const code = input.code;\n    return {\n      code,\n      title: titleFor(code),\n      body: bodyFor(code),\n      isRetriable: typeof input.isRetriable === 'boolean' ? input.isRetriable : RETRIABLE_CODES.has(code),\n      signature: `code:${code}`,\n    };\n  }\n\n  // Raw DOMException / Error: classify through the SHARED taxonomy seam —\n  // `classifyThrown` takes the thrown value directly (the DOM-name dance, the\n  // message extraction and the GH #354 carried code all live in the taxonomy\n  // now), reaching the SAME decision the SDK's BeacioError.from reaches — then\n  // use branded copy, NEVER the raw .message (it may carry a stack or a\n  // competitor name). 'GATT_OPERATION_FAILED' is the presenter's required\n  // fallback for an error the taxonomy cannot place.\n  if (typeof input === 'object' && input !== null) {\n    const { code } = classifyThrown(input, 'GATT_OPERATION_FAILED');\n    return { code, title: titleFor(code), body: bodyFor(code), isRetriable: RETRIABLE_CODES.has(code), signature: `dom:${code}` };\n  }\n\n  return { code: null, title: pack.generic.title, body: pack.generic.body, isRetriable: false, signature: 'generic' };\n}\n\n/**\n * Present a branded, non-blocking, dismissible error card. Replaces a blocking\n * `window.alert(error.toString() + error.stack)` with a recovery-oriented surface.\n *\n * @param errorOrMessage A BeacioError, a raw DOMException/Error, or a string.\n * @param options Operator name, copy/locale overrides, and an onRetry handler.\n * @returns The card element, or null when the error is coalesced (a card for an\n *          identical error is already on screen) so callers can no-op safely.\n */\nexport function presentError(errorOrMessage: unknown, options: PresentErrorOptions = {}): HTMLElement | null {\n  // SSR / non-DOM guard (mirrors banner.ts dispatch guards).\n  if (typeof document === 'undefined') return null;\n\n  // SB-SDK-07: resolve the localized pack ONCE (explicit lang > navigator.language\n  // > English) with the caller's `strings` deep-merged on top; per-code copy +\n  // dismiss/retry derive from it, with the explicit dismissText/retryText last.\n  // deepMerge skips undefined override keys, so an absent `strings` is a no-op.\n  const pack = resolveStrings({ lang: options.lang, strings: { error: options.strings } }).error;\n  const resolved = resolve(errorOrMessage, pack);\n\n  // AC2: coalesce identical errors fired within a short window into ONE card —\n  // suppress the duplicate (and never fall back to alert). An identical card still\n  // on screen also suppresses, so a burst never stacks.\n  const now = Date.now();\n  const existing = document.getElementById(CARD_ID);\n  if (existing && lastSignature === resolved.signature && now - lastShownAt < DEDUPE_WINDOW_MS) {\n    return null;\n  }\n  // A new error replaces any prior card (single card surface at a time).\n  if (existing) existing.remove();\n  lastSignature = resolved.signature;\n  lastShownAt = now;\n\n  const operatorName = options.operatorName;\n  // Precedence: explicit dismissText/retryText > the resolved pack (which already\n  // has the per-call `strings` merged over the language pack).\n  const dismissLabel = options.dismissText ?? pack.dismiss;\n  const retryLabel = options.retryText ?? pack.retry;\n  const showRetry = resolved.isRetriable;\n\n  const customStyle = Object.entries(options.style ?? {})\n    .map(([k, v]) => `${k}:${v}`)\n    .join(';');\n\n  const card = document.createElement('div');\n  card.id = CARD_ID;\n  card.dataset.beacioErrorCode = resolved.code ?? 'unknown';\n  // AC1: merge caller-supplied style overrides onto the card container (parity with\n  // BannerOptions, whose bar banner applies options.style). Inline style wins over\n  // the stylesheet default so an operator can theme the card without forking.\n  if (customStyle) card.style.cssText = customStyle;\n\n  // Title carries the operator name when supplied (parity with the banner), so the\n  // card is branded to the host app rather than anonymous.\n  const heading = operatorName ? `${operatorName} — ${resolved.title}` : resolved.title;\n\n  card.innerHTML = `\n<style>\n#${CARD_ID}{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:2147483646;\n  max-width:420px;width:calc(100% - 32px);background:#fff;color:#1c1c1e;border-radius:14px;\n  padding:16px 18px;display:flex;flex-direction:column;gap:10px;\n  font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif;\n  box-shadow:0 6px 20px rgba(0,0,0,.2);animation:bce-u .3s ease-out}\n@keyframes bce-u{from{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%,0)}}\n#${CARD_ID} *{box-sizing:border-box;margin:0;padding:0}\n.bce-row{display:flex;align-items:flex-start;gap:12px}\n.bce-ic{width:28px;height:28px;border-radius:8px;background:#ff3b30;flex-shrink:0;display:flex;\n  align-items:center;justify-content:center}\n.bce-ic svg{width:18px;height:18px;fill:#fff}\n.bce-tx{flex:1;min-width:0}\n.bce-tt{font-size:15px;font-weight:600;line-height:1.3}\n.bce-bd{font-size:14px;line-height:1.4;color:#3a3a3c;margin-top:3px}\n.bce-x{background:none;border:none;color:#8e8e93;font-size:20px;cursor:pointer;line-height:1;\n  padding:0 2px;align-self:flex-start}\n/* SB-SDK-07: visually-hidden text label on the icon-only dismiss control. The\n   glyph stays the only visible mark; the label surfaces in the accessibility\n   tree + DOM text so the LOCALIZED dismiss copy is present (German when lang\n   selects it), not just an aria-label attribute. */\n.bce-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;\n  clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.bce-act{display:flex;gap:8px;justify-content:flex-end}\n.bce-retry{padding:9px 16px;background:#007aff;color:#fff;border:none;border-radius:10px;\n  font-size:15px;font-weight:600;cursor:pointer}\n.bce-retry:active{opacity:.85}\n@media(prefers-color-scheme:dark){\n  #${CARD_ID}{background:#1c1c1e;color:#fff}\n  .bce-bd{color:#aeaeb2}\n}\n</style>\n<div class=\"bce-row\">\n  <div class=\"bce-ic\"><svg viewBox=\"0 0 24 24\"><path d=\"M12 2 1 21h22L12 2zm0 5 7.5 13h-15L12 7zm-1 4v4h2v-4h-2zm0 6v2h2v-2h-2z\"/></svg></div>\n  <div class=\"bce-tx\">\n    <p class=\"bce-tt\">${esc(heading)}</p>\n    <p class=\"bce-bd\">${esc(resolved.body)}</p>\n  </div>\n  <button class=\"bce-x\" aria-label=\"${esc(dismissLabel)}\">&times;<span class=\"bce-sr\">${esc(dismissLabel)}</span></button>\n</div>\n${showRetry ? `<div class=\"bce-act\"><button class=\"bce-retry\" type=\"button\">${esc(retryLabel)}</button></div>` : ''}`;\n\n  function dismiss(): void {\n    card.remove();\n    // Allow an immediate, DIFFERENT error to show; only identical ones within the\n    // window are coalesced, and dismissing clears the on-screen-card suppression.\n    lastShownAt = 0;\n  }\n\n  // Wire listeners synchronously (jsdom click in the test fires in the same tick).\n  card.querySelector<HTMLElement>('.bce-x')?.addEventListener('click', dismiss);\n  // The dismiss aria-label control doubles as the required dismissible button.\n  if (showRetry) {\n    card.querySelector<HTMLElement>('.bce-retry')?.addEventListener('click', () => {\n      dismiss();\n      options.onRetry?.();\n    });\n  }\n\n  document.body.appendChild(card);\n  return card;\n}\n","/**\n * Analytics event reporter and API key validator.\n * Fire-and-forget — analytics must never throw or block.\n */\n\n// SB-SDK-16: api.beacio.com is the DEPLOYED ingest Worker — the api Worker at\n// cloudflare/workers/api (the `ioswebble-api` Worker) serves it via `custom_domain` and owns the keyed\n// /v1/events + /v1/config handlers. It is NOT a phantom host, and it is NOT\n// beacon.beacio.com (a separate Worker with no such routes — the WF2 triage\n// evidence got this wrong; repointing here would silently 404/401). The host↔\n// Worker coupling is locked by tests/api-host-reconciliation.test.ts so it can't\n// drift. No behaviour rides on this while unkeyed: reportEvent early-returns when\n// !apiKey (below), so this only matters on the keyed-tenant launch path.\nconst API_BASE = 'https://api.beacio.com';\n\n/**\n * SB-SDK-15: the EXHAUSTIVE allow-list of fields that may ever leave the browser.\n * The S&B drop-in pitch (\"closes the GDPR sub-processor gap\", §6.3) depends on the\n * embedded polyfill staying network-silent AND, when an operator opts in with a\n * key, emitting only this minimal non-device set. We build the egress object from\n * these fixed keys instead of spreading caller-supplied `data`, so a future caller\n * passing reportEvent(key, evt, { deviceName, serialNumber, gattValue }) CANNOT\n * smuggle GATT values, device names, or serial numbers onto the wire — the guard\n * is structural, not by-convention (tests/telemetry-privacy.test.ts is the gate).\n */\nfunction buildEventData(): Record<string, string> {\n  return { origin: location.hostname, ua: navigator.userAgent };\n}\n\nexport function reportEvent(apiKey: string, event: string, _data?: { [key: string]: string | number | boolean | null }): void {\n  if (!apiKey) return;\n  try {\n    // SB-SDK-16: the ingesting Worker (cloudflare/workers/api) authenticates the keyed\n    // /v1/events path ONLY via `Authorization: Bearer` (extractBearerKey); a\n    // `?key=` query param is read solely by the /v1/detect pixel and would 401\n    // here, silently dropping every event. Send the key in the header.\n    fetch(`${API_BASE}/v1/events`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${apiKey}`,\n      },\n      body: JSON.stringify({\n        events: [{\n          event,\n          data: buildEventData(),\n          timestamp: Date.now(),\n        }],\n      }),\n      keepalive: true,\n    }).catch(() => {});\n  } catch { /* analytics must never throw */ }\n}\n\nexport async function validateApiKey(\n  apiKey: string,\n): Promise<{ operatorId: string; appName: string | null; plan: string } | null> {\n  try {\n    // SB-SDK-16: /v1/config is Bearer-auth-only on the Worker; a `?key=` param\n    // is ignored there and the handler returns 401 → null. Send the key in the\n    // Authorization header so a real key can resolve operator config.\n    const res = await fetch(`${API_BASE}/v1/config`, {\n      headers: { 'Authorization': `Bearer ${apiKey}` },\n    });\n    if (!res.ok) return null;\n    return await res.json();\n  } catch {\n    return null;\n  }\n}\n","/**\n * @beacio/detect\n *\n * Detects iOS Safari, checks if the Beacio extension is installed,\n * and shows an install banner if not. No-op on all other platforms.\n *\n * Your existing Web Bluetooth code works unchanged — this package only\n * handles the \"extension not installed\" case on iOS Safari.\n */\n\nexport {\n  getExtensionInstallState,\n  isExtensionInstalled,\n  // GH #448: `isIOSDevice` is deliberately NOT re-exported. It is the internal\n  // building block isIOSSafari and isIOSHomeScreenWebApp share inside detect.ts and\n  // has no caller outside that module — and every name listed here becomes a\n  // permanently supported symbol of the published `@beacio/core/detect` subpath\n  // (and of the S&B vendored beacio-detect.js drop-in).\n  isIOSHomeScreenWebApp,\n  isIOSSafari,\n} from './detect';\nexport type { ExtensionInstallState } from './detect';\nexport { showInstallBanner, removeInstallBanner, SETUP_STEPS } from './banner';\nexport type { BannerOptions, BannerState, SetupStep } from './banner';\n// SB-SDK-12: the framework-agnostic, ZERO-DOM headless onboarding API for\n// vanilla-JS partners (Storz & Bickel's app is vanilla JS + jQuery and cannot use\n// the React wizard; showInstallBanner injects beacio chrome). A partner draws its\n// OWN \"Enable Bluetooth in Safari\" card and drives it with these primitives —\n// install-state detection (SHARED with detect.ts + the react-sdk\n// ExtensionDetector), the EXTENSION_READY-aware observer, the return-link/clipboard\n// context helper, the dismissal frequency-cap, and the id-form App Store URL. None\n// of these inject DOM.\n// SB-PRD-08: the soft/hard dismissal split is part of the headless API too. A\n// vanilla-JS partner drawing its OWN onboarding card needs the soft \"Not now\"\n// primitive (dismissShort, a short 1-day suppression) — not just the long\n// `dismiss` — plus the documented window lengths, so it can mirror the banner's\n// soft/hard behaviour without hand-writing the internal localStorage key.\n// GH #448: buildOpenInSafariUrl joins the headless surface for the same reason —\n// a partner rendering its OWN Home Screen web-app diagnosis needs the x-safari-\n// escape hatch without pulling in beacio chrome.\nexport {\n  APP_STORE_URL,\n  buildOpenInSafariUrl,\n  DEFAULT_DISMISS_DAYS,\n  dismiss,\n  dismissShort,\n  getInstallState,\n  getReturnContext,\n  isDismissed,\n  isExtensionActive,\n  observeInstallState,\n  saveReturnContext,\n  SHORT_DISMISS_DAYS,\n} from './install-state';\n// SB-SDK-05: the branded, framework-free error presenter — the FAILURE-path\n// sibling of showInstallBanner. Re-exported from the package root (and from\n// core/browser-auto) so a classic <script> site can call\n// beacioDetect.presentError(error) with no module setup.\nexport { presentError } from './error-presenter';\nexport type { PresentErrorOptions, BeacioErrorCode } from './error-presenter';\n// SB-SDK-07: the shared localized-string seam (built-in en/de packs + the pure\n// resolver) that both showInstallBanner and presentError consume. Exported so a\n// consumer can inspect/extend the packs or pre-resolve copy. The BeacioErrorCode\n// type is already exported above (the identical local union), so it is NOT\n// re-exported here to avoid an ambiguous re-export.\nexport { DE_STRINGS, EN_STRINGS, resolveStrings } from './i18n';\nexport type {\n  DeepPartial,\n  ErrorCopy,\n  ErrorStrings,\n  LocaleStrings,\n  ResolveStringsOptions,\n  SetupStepCopy,\n  StateCopy,\n} from './i18n';\nexport { reportEvent, validateApiKey } from './api';\nimport { reportEvent } from './api';\nimport type { ExtensionInstallState } from './detect';\n// detect now lives INSIDE @beacio/core, so the canonical event-name map is an\n// intra-package import from core's events module — the single source of truth\n// (events.test.ts remains the seam-crossing control that the wire literals match).\nimport { BEACIO_EVENTS } from '../events';\n// SBOPT-P2.4 (tier-3 headless): the id-form App Store URL + the pure return-link\n// builder come from the shared zero-DOM install-state module (both are also part\n// of the SB-SDK-12 headless surface); SETUP_URL is core's canonical /setup page.\n// buildOnboardingUrl (banner.ts) is imported LAZILY inside resolveOnboardingState —\n// like initBeacio's own `await import('./banner')` — so a pure-headless consumer\n// that only imports resolveOnboardingState never pulls the banner DOM code.\nimport { APP_STORE_URL, buildOpenInSafariUrl, buildReturnLink } from './install-state';\nimport { SETUP_URL } from '../urls';\nexport interface BeacioOptions {\n  /** Optional API key for campaign tracking */\n  key?: string;\n  /** Operator/app name shown in the prompt (e.g. \"FitTracker\") */\n  operatorName?: string;\n  /**\n   * SB-SDK-07: BCP-47 UI language (e.g. 'de') for the install banner. Threaded\n   * to showInstallBanner so the zero-config initBeacio path is localizable;\n   * omitted ⇒ the banner derives the language from navigator.language, else\n   * English. A `banner.lang` (below) overrides this for the banner specifically.\n   */\n  lang?: string;\n  /** Install banner configuration, or false to disable */\n  banner?:\n    | {\n        /** 'sheet' (default) for iOS bottom sheet, 'banner' for lightweight bar */\n        mode?: 'sheet' | 'banner';\n        position?: 'top' | 'bottom';\n        text?: string;\n        buttonText?: string;\n        style?: Record<string, string>;\n        startOnboardingUrl?: string;\n        appStoreUrl?: string;\n        /** Days to suppress after the explicit \"Don't show again\" opt-out (default: 14) */\n        dismissDays?: number;\n        /**\n         * SB-PRD-08 (AC3): ignore the dismissal cooldown and show anyway. Set this\n         * on a user-initiated recovery call (e.g. re-invoking initBeacio from a\n         * Connect / \"Can't connect?\" gesture) so a previously-dismissed user can\n         * re-open setup without clearing localStorage.\n         */\n        forceShow?: boolean;\n        /** SB-SDK-07: BCP-47 language override for the banner (wins over the top-level `lang`). */\n        lang?: string;\n        /** SB-SDK-11 (tier-2 co-brand): partner accent colour for the prompt chrome. */\n        accentColor?: string;\n        /** SB-SDK-11: partner logo URL (http(s) only; validated). Replaces the beacio glyph. */\n        brandLogoUrl?: string;\n        /** SB-SDK-11: the connected device's display name (e.g. \"VOLCANO HYBRID\"). */\n        deviceName?: string;\n        /** SB-SDK-11: one-shot lead body copy override (HTML-escaped). */\n        body?: string;\n        /** SB-SDK-11: privacy reassurance body override (HTML-escaped). */\n        privacyBody?: string;\n      }\n    | false;\n  /** Called when the extension is detected and ready */\n  onReady?: () => void;\n  /** Called when the extension is installed but Safari still needs activation/allow access */\n  onInstalledInactive?: () => void;\n  /** Called when the extension is NOT installed */\n  onNotInstalled?: () => void;\n}\n\nfunction dispatchInstallState(state: ExtensionInstallState): void {\n  if (typeof window === 'undefined') {\n    return;\n  }\n\n  window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.STATE_CHANGE, {\n    detail: { state }\n  }));\n}\n\n/**\n * Show the install banner unless explicitly disabled.\n * No-op when `options.banner === false`.\n */\nasync function maybeShowBanner(\n  options: BeacioOptions,\n  state: ExtensionInstallState,\n  // SB-SDK-03 AC2: lets the caller pass the per-site 'denied' refinement, which the\n  // ExtensionInstallState markers cannot express (it has no 'denied' member).\n  bannerStateOverride?: import('./banner').BannerState\n): Promise<void> {\n  if (options.banner === false) return;\n  const { showInstallBanner } = await import('./banner');\n  const bannerConfig = typeof options.banner === 'object' ? options.banner : {};\n  const bannerOpts: import('./banner').BannerOptions = {\n    // SB-SDK-11: the co-brand theme (accentColor / brandLogoUrl / deviceName /\n    // body / privacyBody) rides through on this spread of bannerConfig into\n    // BannerOptions, so the zero-config initBeacio path is fully themeable.\n    ...bannerConfig,\n    apiKey: options.key ?? '',\n    operatorName: options.operatorName,\n    // SB-SDK-07: thread the UI language so the zero-config initBeacio path is\n    // localizable; banner.lang wins over the top-level lang, and an omitted lang\n    // lets the banner derive from navigator.language (else English).\n    lang: bannerConfig.lang ?? options.lang,\n    // SB-PRD-03: feed the funnel position so the sheet shows the SPECIFIC remaining\n    // step (or, on 'active', the once-only success toast) instead of restarting setup.\n    state: bannerStateOverride ?? state,\n  };\n  showInstallBanner(bannerOpts);\n}\n\n/**\n * SB-SDK-03 AC2: the extension is installed + enabled, but is THIS origin granted\n * access? Safari's per-origin \"Allow Every Website\" is irreducibly manual\n * (project_ios26_safari_extension_settings_readonly). The W3C-conformant probe is\n * `navigator.bluetooth.getAvailability()`: a defined `navigator.bluetooth` that\n * reports unavailable means the polyfill is present but blocked HERE — the\n * per-site 'denied' state. Any throw / undefined surface is treated as NOT denied\n * so a genuinely-active origin never gets downgraded to the guidance sheet.\n */\nasync function isOriginDenied(): Promise<boolean> {\n  if (typeof navigator === 'undefined') return false;\n  const bt = (navigator as Navigator & { bluetooth?: { getAvailability?: () => Promise<boolean> } })\n    .bluetooth;\n  if (!bt || typeof bt.getAvailability !== 'function') return false;\n  try {\n    return (await bt.getAvailability()) === false;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * SB-SDK-17: BEST-EFFORT Private Browsing detection. iOS Safari disables web\n * extensions in Private Browsing (no per-extension opt-in), so beacio is inert and\n * the content script sets no markers — getExtensionInstallState() resolves\n * 'not-installed' and the user wrongly gets the \"install the app\" sheet even though\n * the app may already be installed. The recovery is to reopen the page in a normal\n * tab, so this routes to the dedicated 'private-browsing' hint instead.\n *\n * iOS exposes NO reliable Private-Browsing API, so this is a HEURISTIC, not a\n * guarantee: historically iOS Safari Private mode gives localStorage a zero quota,\n * so a setItem write-probe throws (QuotaExceededError). We write-and-immediately-\n * remove a throwaway key; a throw is read as \"extensions are likely unavailable\".\n * It is deliberately conservative — ANY success (or an unexpected error shape) is\n * treated as NOT private browsing, so a normal tab is never downgraded to the hint.\n * Callers MUST gate this behind isIOSSafari() (the probe is meaningless elsewhere\n * and the false-positive risk is higher on other engines).\n */\nfunction isPrivateBrowsingBestEffort(): boolean {\n  if (typeof window === 'undefined') return false;\n  try {\n    const storage = window.localStorage;\n    if (!storage) return false;\n    const probeKey = '__beacio_pb_probe__';\n    storage.setItem(probeKey, '1');\n    storage.removeItem(probeKey);\n    return false;\n  } catch {\n    // A write-probe throw (e.g. QuotaExceededError) is the classic iOS Private\n    // Browsing signature. Best-effort only — see the JSDoc above.\n    return true;\n  }\n}\n\n/**\n * Initialize Beacio detection.\n *\n * On iOS Safari: checks if the extension is installed, dispatches events,\n * and optionally shows an install banner.\n *\n * On an iOS Home Screen web app (GH #448): renders the \"open this page in Safari\"\n * diagnosis instead — the page is NOT iOS Safari by the UA test, yet it runs on an\n * iOS device one tap away from a browser where beacio does work.\n *\n * On all other platforms: no-op (returns immediately).\n */\nexport async function initBeacio(options: BeacioOptions): Promise<void> {\n  const { getExtensionInstallState, isIOSHomeScreenWebApp, isIOSSafari } = await import('./detect');\n\n  // GH #448: an iOS Home Screen web app (\"Open as Web App\" ON) strips the `Safari/`\n  // UA token, so isIOSSafari() is FALSE there and this early-return would silently\n  // no-op on a page that only needs to be reopened in Safari — the \"install again,\n  // still broken\" loop. The widening is gated on the iOS-DEVICE test alone, so a\n  // desktop PWA (same `standalone` boolean, no iOS extension to enable) still no-ops.\n  const homeScreenWebApp = typeof window !== 'undefined' && isIOSHomeScreenWebApp(navigator, window);\n  if (!isIOSSafari() && !homeScreenWebApp) return;\n\n  const installState = await getExtensionInstallState();\n  dispatchInstallState(installState);\n\n  if (installState === 'active') {\n    // SB-SDK-03 AC2: the extension is enabled, but this ORIGIN may still be\n    // blocked (\"Allow Every Website\" not granted here) — the one funnel position\n    // the install-state markers cannot distinguish. The W3C signal is\n    // navigator.bluetooth being DEFINED while getAvailability() resolves false\n    // (project_ios26_safari_extension_settings_readonly: per-origin grant is\n    // irreducibly manual). Derive the 'denied' refinement so the banner shows the\n    // aA → Manage Extensions → Allow Every Website guidance, not the success path.\n    if (await isOriginDenied()) {\n      reportEvent(options.key ?? '', 'extension_installed_inactive');\n      if (typeof window !== 'undefined') {\n        window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.INSTALLED_INACTIVE));\n      }\n      options.onInstalledInactive?.();\n      await maybeShowBanner(options, installState, 'denied');\n      return;\n    }\n\n    reportEvent(options.key ?? '', 'extension_active');\n    if (typeof window !== 'undefined') {\n      window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.READY));\n    }\n    options.onReady?.();\n    // SB-PRD-03 AC5: on the active transition, surface the once-only \"beacio is\n    // ready — tap Connect\" toast (showReadyToast self-suppresses for returning\n    // users), replacing the old silent empty state.\n    await maybeShowBanner(options, installState);\n    return;\n  }\n\n  // GH #448: the Home Screen web-app dead end outranks both best-effort heuristics\n  // below. It is a DETERMINISTIC platform API (not a probe), and neither competing\n  // recovery even exists inside a web app: there are no tabs to reopen in, and the\n  // aA → Manage Extensions gesture is Safari-only. \"Open it in Safari\" subsumes\n  // both. Signals mirror the Private-Browsing branch EXACTLY — same 'detect'\n  // event, same NOT_INSTALLED dispatch, same onNotInstalled callback — so this\n  // adds no new public event and no new BeacioOptions callback. `window` is\n  // provably defined here: homeScreenWebApp is itself `typeof window !==\n  // 'undefined' && …`, so no second guard is needed.\n  if (homeScreenWebApp) {\n    reportEvent(options.key ?? '', 'detect');\n    window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.NOT_INSTALLED));\n    options.onNotInstalled?.();\n    await maybeShowBanner(options, installState, 'home-screen-web-app');\n    return;\n  }\n\n  // SB-SDK-17: two iOS-Safari dead ends both look like a marker-less\n  // 'not-installed'/'installed-inactive' to getExtensionInstallState(), so without\n  // this block initBeacio would fall through to the misleading \"install the app\"\n  // sheet even though the app may already be installed. We reach here only for the\n  // non-active states and only on iOS Safari (the guard above already returned for\n  // every other platform, and the GH #448 branch just above returned for a Home\n  // Screen web app — AC3: neither heuristic runs off-iOS, and the Private-Browsing\n  // write-probe is never even invoked there). Both heuristics are\n  // BEST-EFFORT (see isPrivateBrowsingBestEffort / isOriginDenied JSDoc); when\n  // neither fires we fall through to today's generic guidance unchanged.\n\n  // 1) Private Browsing wins: extensions are globally inert (markers suppressed),\n  //    so per-origin signals are unreliable and the real recovery is a normal tab.\n  if (isPrivateBrowsingBestEffort()) {\n    reportEvent(options.key ?? '', 'detect');\n    if (typeof window !== 'undefined') {\n      window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.NOT_INSTALLED));\n    }\n    options.onNotInstalled?.();\n    await maybeShowBanner(options, installState, 'private-browsing');\n    return;\n  }\n\n  // 2) Marker-suppressed per-origin \"Deny\": no markers, yet navigator.bluetooth is\n  //    defined and reports unavailable HERE → a prior \"Deny\" left the extension\n  //    inert on this origin. Surface the SB-SDK-03 'denied' guidance (aA → Manage\n  //    Extensions → Allow Every Website), the same copy block the active branch uses.\n  if (await isOriginDenied()) {\n    reportEvent(options.key ?? '', 'extension_installed_inactive');\n    if (typeof window !== 'undefined') {\n      window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.INSTALLED_INACTIVE));\n    }\n    options.onInstalledInactive?.();\n    await maybeShowBanner(options, installState, 'denied');\n    return;\n  }\n\n  if (installState === 'installed-inactive') {\n    reportEvent(options.key ?? '', 'extension_installed_inactive');\n    if (typeof window !== 'undefined') {\n      window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.INSTALLED_INACTIVE));\n    }\n    options.onInstalledInactive?.();\n\n    await maybeShowBanner(options, installState);\n    return;\n  }\n\n  // Extension NOT installed\n  reportEvent(options.key ?? '', 'detect');\n  if (typeof window !== 'undefined') {\n    window.dispatchEvent(new CustomEvent(BEACIO_EVENTS.NOT_INSTALLED));\n  }\n  options.onNotInstalled?.();\n\n  // Show install banner unless explicitly disabled\n  await maybeShowBanner(options, installState);\n  if (options.banner !== false) {\n    reportEvent(options.key ?? '', 'install_prompted');\n  }\n}\n\n// ─── SBOPT-P2.4: the tier-3 HEADLESS onboarding funnel state ─────────────────\n//\n// initBeacio (above) welds the funnel classification to a banner render: every\n// routing branch reports an event, dispatches a DOM CustomEvent, and calls\n// maybeShowBanner. A tier-3 partner (Storz & Bickel) draws its OWN install prompt\n// and needs that classification with NONE of those side effects. resolveOnboarding-\n// State is the pure, zero-DOM projection of initBeacio's routing OUTCOMES — one\n// tagged-union variant per `return` — so the partner renders its own card from the\n// state instead of beacio chrome.\n\n/**\n * Where a first-run owner is in the irreducibly-manual iOS-26 setup funnel, as\n * DATA a partner renders itself. The union is CLOSED to exactly initBeacio's seven\n * routing outcomes (a discriminated union + exhaustive switch, not scattered\n * undefined checks):\n *  - 'unsupported'         → not iOS Safari; Web Bluetooth via beacio is unavailable.\n *  - 'home-screen-web-app' → running standalone from the Home Screen, where Safari\n *                            extensions never load; the fix is opening it in Safari.\n *  - 'not-installed'       → app not installed; `installUrl` is the id-form App Store link.\n *  - 'installed-inactive'  → installed but the Safari extension toggle is off; `setupUrl` guides.\n *  - 'denied'              → enabled, but per-origin access not granted on THIS site; `setupUrl` guides.\n *  - 'private-browsing'    → Private Browsing disables extensions; the fix is a normal tab.\n *  - 'ready'               → the polyfill is live and this origin is granted; nothing to prompt.\n *\n * Required fields, sentinels over optionals (owner's API rule): each variant\n * carries only the render-ready URLs its OWN prompt needs, all required — no `?`.\n * `returnLink` is the tappable \"return to your page\" affordance\n * (`https://link.beacio.com/return?url=…`) computed purely, with no side effect.\n * 'home-screen-web-app' carries no `returnLink`: the /setup round-trip does not\n * apply there (the recovery is Safari itself), so it carries the current `pageUrl`\n * — the copy/paste fallback — plus the `x-safari-` deep link built from it.\n *\n * GH #448 twin ruling: the DOM twin AGREES — the banner's footer splits on the same\n * question this union does. The /setup round-trip affordances (`#bc-return`, the\n * \"link also copied\" hint and `saveReturnContext()`'s clipboard write) render only\n * for the states whose recovery comes BACK to this page; the 'home-screen-web-app'\n * sheet suppresses all three, because its recovery LEAVES for Safari and it prints\n * the page address for the user to copy — a second, different \"paste this into\n * Safari\" URL would contradict it, and the return link would clobber the clipboard.\n * So \"no `returnLink` in the data\" and \"no `#bc-return` in the DOM\" are one ruling,\n * not a divergence. The other footer chrome (still-stuck / reload / dismiss) is\n * state-independent and unchanged.\n */\nexport type OnboardingState =\n  | { kind: 'unsupported' }\n  | { kind: 'home-screen-web-app'; pageUrl: string; openInSafariUrl: string }\n  | { kind: 'not-installed'; installUrl: string; returnLink: string }\n  | { kind: 'installed-inactive'; setupUrl: string; returnLink: string }\n  | { kind: 'denied'; setupUrl: string; returnLink: string }\n  | { kind: 'private-browsing'; returnLink: string }\n  | { kind: 'ready' };\n\n/**\n * The REQUIRED config for {@link resolveOnboardingState} — no optional args.\n * `apiKey` threads the App Store campaign token (ct/mt) onto the install deep link\n * exactly as the banner's install button does; `operatorName` threads the operator\n * identity onto the guided /setup deep link so it can render \"Return to <operator>\".\n * Pass empty-string sentinels when a field is not in play.\n */\nexport interface OnboardingConfig {\n  operatorName: string;\n  apiKey: string;\n}\n\n/**\n * The guided /setup deep link carrying the operator identity + return origin, so\n * /setup renders the branded return CTA instead of generic copy. Built with\n * encodeURIComponent (not URLSearchParams) so the operator name round-trips in the\n * `%20` percent form the /setup page and the partner's rendered link both expect.\n */\nfunction buildSetupDeepLink(config: OnboardingConfig): string {\n  const returnUrl = typeof window !== 'undefined' ? window.location.href : '';\n  return `${SETUP_URL}?operatorName=${encodeURIComponent(config.operatorName)}&return=${encodeURIComponent(returnUrl)}`;\n}\n\n/**\n * Resolve the current tier-3 onboarding funnel position WITHOUT rendering any\n * beacio chrome. This is the headless projection of initBeacio's routing: the same\n * isIOSSafari early-return (widened by the GH #448 Home Screen web-app guard), the\n * same active → (denied?) → ready split, and the same \"standalone, then Private\n * Browsing, then a marker-suppressed denied\" precedence for the non-active states —\n * but it returns the position as data for a partner to render, dispatching NO\n * events and injecting NO DOM.\n */\nexport async function resolveOnboardingState(config: OnboardingConfig): Promise<OnboardingState> {\n  const { getExtensionInstallState, isIOSHomeScreenWebApp, isIOSSafari } = await import('./detect');\n\n  // GH #448: an iOS Home Screen web app (\"Open as Web App\" ON) strips the `Safari/`\n  // UA token, so isIOSSafari() is FALSE there and this early-return would swallow\n  // the page as a generic unsupported browser — the \"install again, still broken\"\n  // loop. The standalone guard therefore runs on the iOS-DEVICE test alone.\n  const homeScreenWebApp = typeof window !== 'undefined' && isIOSHomeScreenWebApp(navigator, window);\n  if (!isIOSSafari() && !homeScreenWebApp) return { kind: 'unsupported' };\n\n  const returnLink = buildReturnLink();\n  const setupUrl = buildSetupDeepLink(config);\n  const installState = await getExtensionInstallState();\n\n  if (installState === 'active') {\n    // Enabled globally, but is THIS origin granted? getAvailability()===false is\n    // the per-origin block — initBeacio's active-branch 'denied' refinement.\n    if (await isOriginDenied()) return { kind: 'denied', setupUrl, returnLink };\n    return { kind: 'ready' };\n  }\n\n  // GH #448: standalone goes FIRST among the non-active refinements. It is a\n  // deterministic platform API rather than a heuristic, and both of the recoveries\n  // below are impossible inside a web app — there are no tabs to reopen in, and the\n  // aA → \"Allow Every Website\" gesture does not exist — so \"open this page in\n  // Safari\" subsumes them. (The active-marker branch above still wins: a page that\n  // actually works is never downgraded to the diagnosis.)\n  if (homeScreenWebApp) {\n    // No `typeof window` ternary here: `homeScreenWebApp` is itself\n    // `typeof window !== 'undefined' && isIOSHomeScreenWebApp(navigator, window)`,\n    // so reaching this branch already proves the DOM exists and an off-DOM arm\n    // would be dead code (owner's no-undefined-checks rule). buildOpenInSafariUrl\n    // keeps its own '' sentinel for callers that DO have an empty page URL.\n    const pageUrl = window.location.href;\n    return { kind: 'home-screen-web-app', pageUrl, openInSafariUrl: buildOpenInSafariUrl(pageUrl) };\n  }\n\n  // Non-active on iOS Safari — mirror initBeacio's precedence exactly:\n  // 1) Private Browsing wins (extensions are globally inert; per-origin signals unreliable).\n  if (isPrivateBrowsingBestEffort()) return { kind: 'private-browsing', returnLink };\n  // 2) Marker-suppressed per-origin Deny (no markers, yet getAvailability()===false here).\n  if (await isOriginDenied()) return { kind: 'denied', setupUrl, returnLink };\n  // 3) Installed but the Safari extension toggle is off.\n  if (installState === 'installed-inactive') return { kind: 'installed-inactive', setupUrl, returnLink };\n\n  // 4) App not installed — the id-form App Store deep link (banner.ts's own builder,\n  //    imported lazily so a pure-headless consumer never pulls the banner DOM code).\n  const { buildOnboardingUrl } = await import('./banner');\n  return {\n    kind: 'not-installed',\n    installUrl: buildOnboardingUrl(APP_STORE_URL, { apiKey: config.apiKey, operatorName: config.operatorName }),\n    returnLink,\n  };\n}\n"]}