{"version":3,"sources":["../src/error-taxonomy.ts"],"names":["IOS_GRANT_WORDING_CANONICAL","RETRIABLE_CODES","RETRY_AFTER_MS","COMPETITOR_TOKENS","detail","code","restatement","sentenceFragment","message","sanitizeNativeMessage","NOT_FOUND_FRAGMENTS","GRANT_WALL","CDN_DORMANT","CDN_RESTORE_PENDING","ACTIVATION_ENABLE_TIMED_OUT","UNSUPPORTED_PLATFORM_INSTALL","notFoundReason","rawMessage","lower","fragment","classification","classifyError","domName","fallback","special","classifyThrown","error","carried","carriedConditionCode","raw","line"],"mappings":"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","file":"chunk-JZXASXCZ.mjs","sourcesContent":["/**\n * The beacio error TAXONOMY — the ONE definition of what a native BLE failure\n * means, shared by every layer that has to decide.\n *\n * Until 2026-08-12 this knowledge existed twice — `../errors.ts` and\n * `./detect/error-presenter.ts` each re-declared the code union, the retriable\n * set, the NotFoundError fragment table and a message classifier — and the two\n * had silently diverged on eleven inputs. The duplication was 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 core\n * itself, and the detect modules already import `../events` / `../urls`.\n *\n * WHY THIS FILE IS (STILL) A LEAF — S5 amendment: it imports ONLY\n * `./error-conditions`, itself a runtime-import-free leaf, so the transitive\n * runtime closure is these two files and nothing else.\n * `dist/browser-auto.global.js` — the zero-config drop-in — bundles the detect\n * presenter but deliberately NOT the full BLE wrapper graph, and\n * `tests/bundle-size-budget.test.ts` caps it at 19,456 B gzip. Reaching the\n * classifier through `../errors.ts` instead would drag in `BeacioError`, the\n * `SUGGESTIONS` copy table and `withRetry` (+1,385 B gzip measured at the\n * split; re-measured per release). Those three stay in `../errors.ts` and\n * remain unreachable from here.\n */\nimport {\n  ACTIVATION_ENABLE_TIMED_OUT,\n  CDN_DORMANT,\n  CDN_RESTORE_PENDING,\n  GRANT_WALL,\n  UNSUPPORTED_PLATFORM_INSTALL,\n  carriedConditionCode,\n} from './error-conditions';\n\n/**\n * Machine-readable error codes for all Beacio operations.\n * Use in catch blocks to handle specific failure modes.\n *\n * @example\n * ```typescript\n * try {\n *   await device.read('heart_rate', 'heart_rate_measurement')\n * } catch (e) {\n *   if (e instanceof BeacioError) {\n *     switch (e.code) {\n *       case 'DEVICE_DISCONNECTED': await device.connect(); break;\n *       case 'CHARACTERISTIC_NOT_READABLE': device.subscribe(...); break;\n *       default: console.error(e.suggestion);\n *     }\n *   }\n * }\n * ```\n */\nexport type BeacioErrorCode =\n  /** Invalid argument passed to an SDK method (e.g. negative timeout, malformed UUID). Not retriable. */\n  | 'INVALID_PARAMETER'\n  /** Browser or platform does not support Web Bluetooth at all. Not retriable. */\n  | 'BLUETOOTH_UNAVAILABLE'\n  /** The Beacio Safari extension is not installed. Show an install banner via `@beacio/core/detect`. Not retriable. */\n  | 'EXTENSION_NOT_INSTALLED'\n  /**\n   * The Beacio Safari extension is installed but has not been enabled for THIS\n   * origin (Safari's per-site extension grant), so `navigator.bluetooth` is inert\n   * here. Distinct from {@link EXTENSION_NOT_INSTALLED} (nothing installed at all)\n   * and from `DEVICE_NOT_FOUND` (the radio ran and found nothing) — the recovery is\n   * \"aA → Manage Extensions → {@link IOS_GRANT_WORDING_CANONICAL}\", not \"move the device closer\".\n   * Not retriable.\n   */\n  | 'EXTENSION_NOT_ENABLED'\n  /** User denied Bluetooth permission, or the call was not triggered by a user gesture. Not retriable. */\n  | 'PERMISSION_DENIED'\n  /** No BLE device matched the given scan filters, or the device picker returned empty. Not retriable. */\n  | 'DEVICE_NOT_FOUND'\n  /** GATT operation attempted on a disconnected device. Retriable -- call `connect()` first. */\n  | 'DEVICE_DISCONNECTED'\n  /** Device did not respond within the connection timeout window. Retriable -- check range and advertising state. */\n  | 'CONNECTION_TIMEOUT'\n  /** The requested GATT service UUID was not found on the connected device. Not retriable. */\n  | 'SERVICE_NOT_FOUND'\n  /** The requested characteristic UUID was not found in the specified service. Not retriable. */\n  | 'CHARACTERISTIC_NOT_FOUND'\n  /** The characteristic does not support the read property. Use `subscribe()` for notify-only characteristics. Not retriable. */\n  | 'CHARACTERISTIC_NOT_READABLE'\n  /** The characteristic does not support write or writeWithoutResponse. Not retriable. */\n  | 'CHARACTERISTIC_NOT_WRITABLE'\n  /** The characteristic does not support notify or indicate. Use `read()` for polling. Not retriable. */\n  | 'CHARACTERISTIC_NOT_NOTIFIABLE'\n  /** Generic GATT failure (device busy, stack error, disconnected mid-operation). Retriable. */\n  | 'GATT_OPERATION_FAILED'\n  /** A BLE scan is already running. Stop the current scan before starting a new one. Retriable. */\n  | 'SCAN_ALREADY_IN_PROGRESS'\n  /** `Beacio.maxConnections` limit reached. Disconnect another device before connecting. Not retriable. */\n  | 'CONNECTION_LIMIT_REACHED'\n  /** User dismissed the device picker without selecting a device. Not retriable. */\n  | 'USER_CANCELLED'\n  /** A read/write/connect operation did not complete within the specified timeout. Retriable. */\n  | 'TIMEOUT'\n  /** A chunked write was only partially completed. Retry with smaller chunks or reconnect. Retriable. */\n  | 'WRITE_INCOMPLETE';\n\n/**\n * The ONE canonical label of Safari's per-extension grant control — the\n * aA → Manage Extensions → beacio gesture target. Defined exactly once here,\n * in the shared leaf both the SDK entry and the detect subpath already bundle\n * (so composing it adds no bundle weight to either), and composed into every\n * user-facing surface: `errors.ts` SUGGESTIONS and `detect/i18n.ts`\n * EN_STRINGS. Truths it pins:\n *  - U9-DOCS-COHERENCE — this is the ~30-site canonical spelling;\n *  - R-43's GRANT_WORDING_CANONICAL anchor — canonical REQUIRED on live\n *    surfaces; the dropped-\"on\" legacy spelling is tolerated only on archived\n *    leaving artifacts;\n *  - the OUTCOME F-B device split (iOS 26.6) — the Settings-app row is a\n *    DIFFERENT control (\"Other Websites\") and must never be canonicalized\n *    into this label.\n */\nexport const IOS_GRANT_WORDING_CANONICAL = 'Allow on Every Website';\n\n/**\n * Codes that are safe to retry. `BeacioError.isRetriable` is computed from this\n * set, `withRetry` obeys it, and the branded card shows a retry affordance for\n * exactly these — one set, so the SDK and the UI can never disagree.\n */\nexport const RETRIABLE_CODES: ReadonlySet<BeacioErrorCode> = new Set<BeacioErrorCode>([\n  'DEVICE_DISCONNECTED',\n  'CONNECTION_TIMEOUT',\n  'GATT_OPERATION_FAILED',\n  'TIMEOUT',\n  'SCAN_ALREADY_IN_PROGRESS',\n  'WRITE_INCOMPLETE',\n]);\n\n/**\n * Per-code backoff hint applied to a CLASSIFIED native failure\n * (`BeacioError.from`). Reproduces the `{ retryAfterMs: 1000 }` that used to be\n * repeated at each of the four disconnect/timeout classification sites. A code\n * absent here yields `undefined`, i.e. \"no hint — use the caller's backoff\".\n */\nexport const RETRY_AFTER_MS: Readonly<Partial<Record<BeacioErrorCode, number>>> = {\n  DEVICE_DISCONNECTED: 1000,\n  TIMEOUT: 1000,\n};\n\n/** Known competitor/product names that must never surface to a Beacio user. */\nconst COMPETITOR_TOKENS = /\\b(bluefy|web ble browser|webble browser)\\b/gi;\n\n/**\n * What the NATIVE sentence is still worth once the code is known — the ONE\n * consumer-visible difference between two paths that reach the SAME code. The\n * branded card ignores this (it never renders native text); `BeacioError.from`\n * uses it to decide whether `.message` echoes the device or falls back to the\n * per-code SUGGESTION.\n */\nexport type NativeMessageValue =\n  /** The sentence carries detail the SUGGESTION does not (a UUID, an operation, a cause) — carry it. */\n  | 'adds-detail'\n  /** The sentence only restates the code (\"no devices found\", a dismissed chooser) — drop it. */\n  | 'restates-the-code';\n\n/** A classification decision: the code, plus what the native sentence is worth. */\nexport interface Classification {\n  readonly code: BeacioErrorCode;\n  readonly nativeMessage: NativeMessageValue;\n}\n\nconst detail = (code: BeacioErrorCode): Classification => ({ code, nativeMessage: 'adds-detail' });\nconst restatement = (code: BeacioErrorCode): Classification => ({ code, nativeMessage: 'restates-the-code' });\n\n/**\n * S5-B (table-first, R3 EXTENDED): a first-party fragment is the SANITIZE\n * FIXED-POINT of the row's frozen sentence, lower-cased. Why not the raw\n * sentence: `BeacioError.message` stores the SANITIZED line (`errors.ts`), and\n * an unknown-code BeacioError re-entering the presenter's raw branch — the\n * version-skew scenario this table must survive — carries that form, with the\n * trailing period stripped AND any URL removed (the auto.ts sentence ends in\n * one; a minus-period-only fragment would still miss it). The fixed-point is\n * contained in both the raw and the sanitized form, and `sanitizeNativeMessage`\n * is idempotent on all three sentences (verified 2026-08-12). Each fragment\n * CONTAINS its old hand-narrowed fragment, so this only NARROWS matching —\n * the adjudicated §4 narrowing, pinned in tests/error-conditions.test.ts.\n */\nconst sentenceFragment = (message: string): string => sanitizeNativeMessage(message).toLowerCase();\n\n/**\n * The message fragments that discriminate the overloaded NotFoundError, in\n * PRIORITY order — the first fragment contained in the (lower-cased) message\n * wins, so a message carrying two of them still classifies deterministically.\n *\n * WHY A MESSAGE TABLE AT ALL: Web Bluetooth OVERLOADS `NotFoundError`. Chromium\n * maps CHOOSER_CANCELLED (the user dismissing the chooser) AND\n * CHOOSER_NOT_SHOWN_API_LOCALLY_DISABLED (the API is switched off for this\n * origin) onto the SAME DOMException name as a pile of genuine failures\n * (NO_BLUETOOTH_ADAPTER, CHOSEN_DEVICE_VANISHED, WEB_BLUETOOTH_NOT_SUPPORTED,\n * NO_SERVICES_FOUND, …). See\n * `third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc` lines\n * 148-178. The MESSAGE is therefore the only discriminator.\n *\n * Each row carries the FULL {@link Classification} it means — the code AND what\n * the native sentence is still worth — so the two can never be encoded apart. A\n * cancellation deliberately RESTATES: \"no device selected\" is not a failure\n * worth echoing Chromium's wording for.\n *\n * Two layers in one mechanism (S5-B): the FOREIGN rows stay hand-written\n * heuristics (Chromium's bytes are not ours to single-source), while the\n * first-party rows are the emitters' own frozen sentences referenced from\n * `./error-conditions` — table edits and emitter output can no longer drift.\n * Every fragment is deliberately NARROW, and each row records what a wider one\n * would have stolen. This table used to exist twice (core + presenter), pinned to\n * itself by a regex-scraping test; there is now one table, so that test is gone.\n */\nconst NOT_FOUND_FRAGMENTS: readonly { readonly fragment: string; readonly classification: Classification }[] = [\n  // Chromium's CHOOSER_CANCELLED sentence, which the beacio polyfill emits\n  // verbatim (`src/extension/page-bootstrap.ts`, `src/beacio/api/bluetooth.ts`).\n  // No other NotFoundError message in Chromium's table contains it. Both\n  // spellings are matched because both ship in the wild. FOREIGN rows — kept\n  // FIRST and load-bearing forever (version skew: older emitters + Chromium\n  // itself keep producing these bytes).\n  { fragment: 'user cancelled', classification: restatement('USER_CANCELLED') },\n  { fragment: 'user canceled', classification: restatement('USER_CANCELLED') },\n  // Grant wall: \"User has not enabled Web Bluetooth for this origin.\" beacio IS\n  // installed but inert here, so the recovery is a beacio setup step and never\n  // \"go look for your device\".\n  // Full-sentence on purpose — NOT 'web bluetooth' (that steals Chromium's\n  // WEB_BLUETOOTH_NOT_SUPPORTED and \"Web Bluetooth API globally disabled.\") and\n  // NOT 'not enabled' (that steals adapter-state wording such as\n  // \"Bluetooth is not enabled.\"). Bytes single-sourced from the emitter's row.\n  { fragment: sentenceFragment(GRANT_WALL.message), classification: detail('EXTENSION_NOT_ENABLED') },\n  // CDN loader: \"Beacio is installed but not active. Follow the Beacio setup\n  // prompt, then retry.\" — the SAME condition as the grant wall, reached on a\n  // different surface (src/cdn/beacio.ts replaces requestDevice while the\n  // extension is dormant). NOT 'not active', which would reach adapter/scan\n  // wording that has nothing to do with beacio.\n  { fragment: sentenceFragment(CDN_DORMANT.message), classification: detail('EXTENSION_NOT_ENABLED') },\n  // CDN loader inside Safari's post-restore window: \"Beacio is still starting up\n  // in this tab. Wait a moment, then retry.\" The extension IS installed and IS\n  // enabled here — Safari has not loaded its contexts into this restored tab\n  // yet — so it takes the same reason as its cdn-dormant sibling: \"the API is\n  // not usable on this surface right now\", never DEVICE_NOT_FOUND, which would\n  // send the user hunting for a device that is sitting right there. Full-sentence\n  // on purpose: 'starting up' alone would steal adapter/power-state wording.\n  { fragment: sentenceFragment(CDN_RESTORE_PENDING.message), classification: detail('EXTENSION_NOT_ENABLED') },\n  // Enable-flow timeout (V7): \"Beacio activation timed out\" — beacio-owned; the\n  // enable timed out at onboarding, NOT a cancellation and NEVER DEVICE_NOT_FOUND.\n  // Its own sentence (no longer the frozen cancel bytes) needs its own\n  // full-sentence row — otherwise the sanitize re-entry path (BeacioError stores\n  // the sanitized line) silently re-classifies it to DEVICE_NOT_FOUND (the V7\n  // regression this row prevents).\n  { fragment: sentenceFragment(ACTIVATION_ENABLE_TIMED_OUT.message), classification: detail('EXTENSION_NOT_ENABLED') },\n  // Unsupported-platform stub: \"Web Bluetooth is not supported on this platform.\n  // On iOS Safari, install the Beacio extension. See: https://beacio.com\" —\n  // @beacio/core's own stub (packages/core/src/auto.ts), which pops the install\n  // banner on the very call that throws. NOT installed at all, so the recovery is\n  // \"install it\": a different instruction, and a different card, from the rows above.\n  // The full sentence CONTAINS Chromium's WEB_BLUETOOTH_NOT_SUPPORTED string as\n  // its verbatim LEADING sentence — matching the whole (sanitized) sentence\n  // keeps Chromium's own short sentence classifying DEVICE_NOT_FOUND, exactly\n  // as the old beacio-specific-tail fragment did.\n  { fragment: sentenceFragment(UNSUPPORTED_PLATFORM_INSTALL.message), classification: detail('EXTENSION_NOT_INSTALLED') },\n];\n\n/**\n * The single definition of \"what this native NotFoundError message actually\n * means\", used by every classification path so they can never disagree.\n *\n * Takes the RAW text and lower-cases INTERNALLY, so a stack-suffixed or\n * differently-cased native message still classifies and no caller can forget to\n * lower-case first. `null` means \"no fragment matched\" — deliberately NOT\n * DEVICE_NOT_FOUND, because the message rules below the DOM-name switch must\n * still get their turn; each caller decides what a non-match is worth.\n */\nfunction notFoundReason(rawMessage: string): Classification | null {\n  const lower = rawMessage.toLowerCase();\n  for (const { fragment, classification } of NOT_FOUND_FRAGMENTS) {\n    if (lower.includes(fragment)) return classification;\n  }\n  return null;\n}\n\n/**\n * THE classifier — the pure decision over an ALREADY-EXTRACTED carrier. Both the\n * SDK (`BeacioError.from`) and the branded card (`presentError`) route through\n * {@link classifyThrown}, which extracts and delegates here, so a DOMException\n * can never be coded one way for a caller's `switch` and another way for the\n * card the same user is looking at.\n *\n * All three arguments are REQUIRED (no optional arguments): `domName` is `''`\n * when the carrier had no DOM name (a plain Error re-thrown across a bridge) —\n * the message rules below still apply to it — and `fallback` is the caller's own\n * fallback code for the SDK, `'GATT_OPERATION_FAILED'` for the presenter. The\n * carried-code authority (GH #354) lives in {@link classifyThrown}: it needs the\n * THROWN value, not an extracted field, so this signature stays a flat,\n * unswappable (domName, rawMessage, fallback).\n *\n * Every rule below was ADJUDICATED on 2026-08-12, when the twins were collapsed:\n * ten of the eleven inputs on which they disagreed were resolved in core's\n * favour, one (`timeout`) in the presenter's. Each decision and its reasoning is\n * recorded EXECUTABLY, in the `ADJUDICATED` table of\n * tests/detect/error-presenter-core-parity.test.ts, which also pins what the\n * losing side used to answer — change a rule here and that table names it.\n */\nexport function classifyError(domName: string, rawMessage: string, fallback: BeacioErrorCode): Classification {\n  const lower = rawMessage.toLowerCase();\n\n  switch (domName) {\n    // Convention 5: the polyfill rehydrates native validation failures as\n    // REAL TypeErrors (invalid UUIDs, malformed filters — Web Bluetooth §7).\n    case 'TypeError':\n      return detail('INVALID_PARAMETER');\n    // NotFoundError is overloaded (see NOT_FOUND_FRAGMENTS): a dismissed chooser, an\n    // origin that never enabled the extension, and a genuine \"nothing to connect\n    // to\" all arrive under the same DOM name. Disambiguate HERE, by message — if\n    // this returned a flat DEVICE_NOT_FOUND, `USER_CANCELLED` /\n    // `EXTENSION_NOT_ENABLED` would be unreachable and callers would be forced to\n    // re-sniff the raw message (or, worse, suppress every NotFoundError and hide\n    // real failures). Reporting an un-enabled origin as \"device not found\" is the\n    // worst of those: it sends a user hunting for their device at the exact moment\n    // they need to finish setup.\n    case 'NotFoundError':\n      return notFoundReason(rawMessage) ?? detail('DEVICE_NOT_FOUND');\n    case 'NotAllowedError':\n    case 'SecurityError':\n      return detail('PERMISSION_DENIED');\n    case 'NetworkError':\n      return detail('DEVICE_DISCONNECTED');\n    case 'TimeoutError':\n      return detail('TIMEOUT');\n    case 'InvalidStateError':\n      if (lower.includes('disconnect')) return detail('DEVICE_DISCONNECTED');\n      break;\n    default:\n      break;\n  }\n\n  // The NOT_FOUND_FRAGMENTS rows are MESSAGE-keyed, so they also apply to a carrier\n  // that never had the DOM name. A `null` here means only \"the message identifies\n  // no special row\" — it must NOT short-circuit into DEVICE_NOT_FOUND, or the rules\n  // below never run.\n  const special = notFoundReason(rawMessage);\n  if (special !== null) return special;\n\n  if (lower.includes('no devices found') || rawMessage.includes('No Devices')) return restatement('DEVICE_NOT_FOUND');\n  if (rawMessage.includes('No Services matching') || lower.includes('service not found')) return detail('SERVICE_NOT_FOUND');\n  if (rawMessage.includes('No Characteristics matching') || lower.includes('characteristic not found')) {\n    return detail('CHARACTERISTIC_NOT_FOUND');\n  }\n  if (rawMessage.includes('GATT Server is disconnected') || lower.includes('disconnected')) return detail('DEVICE_DISCONNECTED');\n  if (lower.includes('not supported') && lower.includes('read')) return detail('CHARACTERISTIC_NOT_READABLE');\n  if (lower.includes('not supported') && lower.includes('write')) return detail('CHARACTERISTIC_NOT_WRITABLE');\n  if (lower.includes('not supported') && lower.includes('notif')) return detail('CHARACTERISTIC_NOT_NOTIFIABLE');\n  if (lower.includes('permission')) return detail('PERMISSION_DENIED');\n  // D9: LAST, immediately before the fallback — see the adjudication note above.\n  if (lower.includes('timeout')) return detail('TIMEOUT');\n\n  return detail(fallback);\n}\n\n/**\n * THE thrown-value seam (R-14): classify whatever a catch block actually\n * received — a DOMException, an Error, a bare string, any unknown carrier. Both\n * production callers (`BeacioError.from`, `presentError`) route through this ONE\n * function, so the carrier destructuring (the DOM-name dance, the message\n * extraction, the carried-code read) exists exactly once, and the two adjacent\n * `BeacioErrorCode` positional parameters the old four-argument classifyError\n * exposed can no longer be swapped at a call site.\n *\n * `fallback` is REQUIRED (no optional arguments): the SDK passes the caller's\n * own fallback code, the presenter passes `'GATT_OPERATION_FAILED'`.\n *\n * GH #354: the validated out-of-band discriminator a minted DOMException carries\n * under `Symbol.for('beacio.conditionCode')` is authoritative OVER the\n * (byte-shared) message heuristics — chooser-cancelled and activation-dismissed\n * share one byte-frozen cancel sentence (both USER_CANCELLED), while\n * activation-enable-timed-out (V7) carries its own beacio-owned sentence. A\n * carried `USER_CANCELLED` restates the code (drop the native text); every other\n * carried code adds-detail — mirroring {@link NOT_FOUND_FRAGMENTS} — EXCEPT a\n * carried `EXTENSION_NOT_ENABLED` whose message text-classifies as the cancel\n * sentence: that is a VERSION-SKEW GUARD for pre-V7 emitters\n * (activation-enable-timed-out used to reuse the frozen cancel bytes), so R3\n * drops that native text too, MESSAGE-AWARE (cancel sentence → drop, NOT\n * code-keyed), and `.message` falls back to the EXTENSION_NOT_ENABLED SUGGESTION\n * instead of echoing a cancellation the user did not perform. The LIVE\n * enable-timed-out row carries its own sentence, which text-classifies\n * 'extension-not-enabled' (NOT_FOUND_FRAGMENTS), so it never reaches that branch\n * and is ECHOED — same as grant-wall / cdn-dormant / cdn-restore-pending.\n *\n * The name/message rules themselves stay in {@link classifyError}, the ONE\n * definition every classification path answers through.\n */\nexport function classifyThrown(error: unknown, fallback: BeacioErrorCode): Classification {\n  const rawMessage = error instanceof Error ? error.message : String(error);\n  const carried = carriedConditionCode(error);\n  if (carried) {\n    if (carried === 'USER_CANCELLED') return restatement('USER_CANCELLED');\n    if (carried === 'EXTENSION_NOT_ENABLED' && notFoundReason(rawMessage)?.code === 'USER_CANCELLED') {\n      return restatement('EXTENSION_NOT_ENABLED');\n    }\n    return detail(carried);\n  }\n  // '' means \"this carrier had no DOM name\" (a plain Error re-thrown across a\n  // bridge); classifyError's message-keyed rules still apply to it.\n  const domName =\n    typeof error === 'object' && error !== null && 'name' in error && typeof (error as { name: string }).name === 'string'\n      ? (error as { name: string }).name\n      : '';\n  return classifyError(domName, rawMessage, fallback);\n}\n\n/**\n * SB-SDK-05 AC6: reduce a raw native error message to a single, complete-sentence\n * line that is safe to show via a bare `alert(error.toString())` — no stack frames,\n * no native `webkit://`/`http(s)://` URLs, and no competitor names. A clean,\n * single-line native message (e.g. \"Invalid UUID: 'bogus'\") is preserved verbatim\n * so the message-passthrough contracts in errors.test.ts do not regress; only the\n * unsafe trailing content is stripped. Returns '' when nothing meaningful remains,\n * so the caller can fall back to branded copy (the per-code SUGGESTION in the SDK,\n * the generic card body in the presenter).\n */\nexport function sanitizeNativeMessage(raw: string): string {\n  // Keep only the first line — everything from the first newline (where V8/WebKit\n  // append \"    at …\" stack frames) onward is dropped.\n  let line = raw.split('\\n', 1)[0] ?? '';\n  // Strip native/internal URLs (webkit://…, http(s)://…) wherever they appear.\n  line = line.replace(/\\b(?:webkit|https?|chrome|moz-extension|safari-web-extension):\\/\\/\\S+/gi, '');\n  // Strip any residual single-line \"at file.js:line:col\" stack fragment.\n  line = line.replace(/\\bat\\s+\\S+:\\d+:\\d+\\)?/gi, '');\n  // Redact competitor names rather than leak them.\n  line = line.replace(COMPETITOR_TOKENS, '');\n  // Collapse whitespace left by the redactions and tidy dangling punctuation.\n  line = line.replace(/\\s{2,}/g, ' ').replace(/\\s+([.,;:])/g, '$1').trim();\n  line = line.replace(/[\\s.,;:]+$/g, '').trim();\n  return line;\n}\n"]}