{"version":3,"sources":["../src/loader/bundleLoader.ts","../src/loader/manifest.ts","../src/loader/deferredApi.ts","../src/loader/index.ts"],"sourcesContent":["/**\n * Inject the widget bundle as an SRI-protected <script> and wait for its\n * window global to appear.\n *\n * Why a <script> tag and not `import()`:\n *   `import()` can fetch cross-origin ESM but doesn't honor Subresource\n *   Integrity on the dynamic-import call itself. A <script> tag with\n *   `integrity=…` is the only path where the browser actually refuses\n *   to execute a bundle whose bytes don't match the manifest's hash.\n *\n * The bundle is built with `tsup --format iife --globalName MhosaicFeedback`,\n * so once it parses it sets `window.MhosaicFeedback = { createFeedback }`\n * (per `src/widget.ts`'s exports). We wait for that to appear.\n *\n * Idempotency: if multiple FeedbackProviders mount on the same page (e.g.\n * during a React fast-refresh cycle), we reuse an already-injected bundle\n * instead of fetching a second copy.\n */\n\nimport type { FeedbackApi, FeedbackConfig } from '../types'\nimport type { ErrorTrackingOptions } from '../modules/error-tracking'\n\n/** What the loader may pass to the bundle's `createFeedback`: the public\n *  config plus the bundle-only `errorTracking` knob the loader forwards\n *  from the manifest. */\nexport type BundleConfig = FeedbackConfig & {\n  errorTracking?: boolean | ErrorTrackingOptions\n}\n\ndeclare global {\n  interface Window {\n    MhosaicFeedback?: {\n      createFeedback(config: BundleConfig): FeedbackApi\n    }\n  }\n}\n\nconst SCRIPT_DATA_ATTR = 'data-mhosaic-feedback-bundle'\n\nexport interface BundleLoadOptions {\n  bundleUrl: string\n  sriHash: string\n  /** Default 30 s. Long enough for slow networks; short enough to surface\n   *  CDN outages quickly. */\n  timeoutMs?: number\n}\n\nexport interface BundleHandle {\n  createFeedback(config: BundleConfig): FeedbackApi\n}\n\nexport async function injectBundle(\n  opts: BundleLoadOptions,\n): Promise<BundleHandle> {\n  // Reuse a bundle already loaded this page.\n  if (window.MhosaicFeedback?.createFeedback) {\n    return window.MhosaicFeedback\n  }\n\n  const existing = document.querySelector<HTMLScriptElement>(\n    `script[${SCRIPT_DATA_ATTR}]`,\n  )\n  if (existing) {\n    // Another loader instance started the fetch; wait for the global.\n    return waitForGlobal(opts.timeoutMs ?? 30_000)\n  }\n\n  return new Promise<BundleHandle>((resolve, reject) => {\n    const script = document.createElement('script')\n    script.src = opts.bundleUrl\n    script.integrity = opts.sriHash\n    script.crossOrigin = 'anonymous'\n    script.async = true\n    script.setAttribute(SCRIPT_DATA_ATTR, '1')\n    script.onload = () => {\n      if (window.MhosaicFeedback?.createFeedback) {\n        resolve(window.MhosaicFeedback)\n      } else {\n        reject(\n          new Error(\n            'mhosaic-feedback: bundle loaded but window.MhosaicFeedback.createFeedback is missing — bundle/loader version mismatch?',\n          ),\n        )\n      }\n    }\n    script.onerror = () => {\n      reject(\n        new Error(\n          `mhosaic-feedback: failed to load bundle from ${opts.bundleUrl} ` +\n            '(network, CSP, or SRI mismatch)',\n        ),\n      )\n    }\n    document.head.appendChild(script)\n\n    const timeoutMs = opts.timeoutMs ?? 30_000\n    setTimeout(() => {\n      reject(\n        new Error(`mhosaic-feedback: bundle load timeout after ${timeoutMs}ms`),\n      )\n    }, timeoutMs)\n  })\n}\n\nfunction waitForGlobal(timeoutMs: number): Promise<BundleHandle> {\n  return new Promise((resolve, reject) => {\n    const start = Date.now()\n    const tick = () => {\n      if (window.MhosaicFeedback?.createFeedback) {\n        resolve(window.MhosaicFeedback)\n        return\n      }\n      if (Date.now() - start > timeoutMs) {\n        reject(\n          new Error(\n            'mhosaic-feedback: timed out waiting for bundle global (another loader instance started the fetch but never finished)',\n          ),\n        )\n        return\n      }\n      setTimeout(tick, 50)\n    }\n    tick()\n  })\n}\n","/**\n * Fetch the widget-manifest from the Mhosaic backend.\n *\n * The loader calls this on every page load. The backend reads `Project.\n * pinned_version` (or current stable) + `Project.widget_enabled` and\n * returns the bundle URL + SRI hash for the version this tenant should\n * receive — or `{enabled: false}` if the widget is killed for the tenant.\n *\n * Network failures are surfaced to the caller. The loader degrades to\n * \"widget not present\" rather than crashing the host page.\n */\n\nexport interface Manifest {\n  /** False when the widget is disabled for this project (kill switch) or\n   *  no stable release exists. Loader bails silently. */\n  enabled: boolean\n  /** Semver of the bundle being served. Present when enabled=true. */\n  version?: string\n  /** Fully-qualified URL to the version-pinned bundle (jsDelivr). */\n  bundle_url?: string\n  /** Subresource integrity hash for the bundle. The loader injects the\n   *  script tag with `integrity=…` so the browser refuses to run a\n   *  bundle whose bytes don't match. */\n  sri_hash?: string\n  /** Static config the manifest endpoint passes through to the widget. */\n  config?: {\n    endpoint: string\n    project_slug: string\n    share_reports_with_widget: boolean\n    /** Phase 4: project gates the widget per end-user (default false). */\n    requires_visibility_check?: boolean\n    /** Per-project switch for the bundle's default error capture. When the\n     *  backend omits it, the bundle falls back to its own default (on). */\n    error_tracking?: boolean\n  }\n  /** Human-readable detail when `enabled` is false. */\n  detail?: string\n}\n\nexport async function fetchManifest(\n  endpoint: string,\n  apiKey: string,\n  signal?: AbortSignal,\n): Promise<Manifest> {\n  const base = endpoint.replace(/\\/$/, '')\n  const url = `${base}/api/feedback/v1/widget-manifest/?pk=${encodeURIComponent(apiKey)}`\n  // credentials: 'omit' — we authenticate via the public pk_proj_ query\n  // param; sending cookies would just trigger CORS preflights for no\n  // reason and is unnecessary since the manifest endpoint doesn't read\n  // any session.\n  const init: RequestInit = { credentials: 'omit' }\n  if (signal) init.signal = signal\n  const res = await fetch(url, init)\n  if (!res.ok) {\n    throw new Error(\n      `mhosaic-feedback: manifest fetch failed (HTTP ${res.status})`,\n    )\n  }\n  const body = (await res.json()) as Manifest\n  return body\n}\n","/**\n * A FeedbackApi-shaped object that queues method calls until the real\n * widget bundle finishes loading, then drains the queue against the real\n * implementation.\n *\n * Why we need this: hosts that already have code like\n *   const fb = createFeedback({...})\n *   fb.identify({id, email, name})\n *   useEffect(() => fb.open(...), [])\n * shouldn't have to refactor for async. The loader's createFeedback\n * returns sync — a deferred handle that records intent, then replays it\n * once the real bundle arrives. Idiomatic queue-then-flush pattern.\n *\n * If the bundle never loads (network failure, SRI mismatch, kill switch),\n * `_fail()` is called instead. Void methods (identify/setMetadata/open/...)\n * silently drop in that case — the widget just doesn't appear. The\n * `submit` Promise rejects so callers can surface real errors.\n */\n\nimport type {\n  FeedbackApi,\n  ReportPayload,\n  SubmittedReport,\n  UserIdentity,\n} from '../types'\n\ntype VoidMethod = 'show' | 'hide' | 'shutdown'\ntype ArgMethod = 'open' | 'identify' | 'setMetadata'\n\ntype Queued =\n  | { kind: 'void'; method: VoidMethod }\n  | { kind: 'arg'; method: 'open'; arg: Parameters<FeedbackApi['open']>[0] }\n  | { kind: 'arg'; method: 'identify'; arg: UserIdentity }\n  | { kind: 'arg'; method: 'setMetadata'; arg: Record<string, unknown> }\n  | {\n      kind: 'submit'\n      payload: Partial<ReportPayload> & { description: string }\n      resolve: (r: SubmittedReport) => void\n      reject: (e: unknown) => void\n    }\n\nexport interface DeferredHandle extends FeedbackApi {\n  /** Called by the loader once the bundle is loaded and a real API exists. */\n  _attach(real: FeedbackApi): void\n  /** Called by the loader if the bundle fails to load. */\n  _fail(err: Error): void\n}\n\nexport function createDeferredApi(): DeferredHandle {\n  let real: FeedbackApi | null = null\n  let failure: Error | null = null\n  const queue: Queued[] = []\n\n  function flush(item: Queued): void {\n    if (real) {\n      if (item.kind === 'void') {\n        ;(real[item.method] as () => void)()\n      } else if (item.kind === 'arg') {\n        ;(real[item.method] as (a: unknown) => void)(item.arg)\n      } else {\n        // submit\n        Promise.resolve(real.submit(item.payload)).then(\n          item.resolve,\n          item.reject,\n        )\n      }\n      return\n    }\n    if (failure) {\n      if (item.kind === 'submit') item.reject(failure)\n      // Void / arg methods silently dropped — the widget never appeared.\n      return\n    }\n    queue.push(item)\n  }\n\n  return {\n    show() {\n      flush({ kind: 'void', method: 'show' })\n    },\n    hide() {\n      flush({ kind: 'void', method: 'hide' })\n    },\n    shutdown() {\n      flush({ kind: 'void', method: 'shutdown' })\n    },\n    open(arg) {\n      flush({ kind: 'arg', method: 'open', arg })\n    },\n    identify(arg) {\n      flush({ kind: 'arg', method: 'identify', arg })\n    },\n    setMetadata(arg) {\n      flush({ kind: 'arg', method: 'setMetadata', arg })\n    },\n    submit(payload) {\n      return new Promise<SubmittedReport>((resolve, reject) => {\n        flush({ kind: 'submit', payload, resolve, reject })\n      })\n    },\n    _attach(impl) {\n      real = impl\n      while (queue.length > 0) flush(queue.shift()!)\n    },\n    _fail(err) {\n      failure = err\n      // Drain any pending submits with the failure; drop void/arg.\n      for (const item of queue) {\n        if (item.kind === 'submit') item.reject(err)\n      }\n      queue.length = 0\n    },\n  }\n}\n","/**\n * `@mhosaic/feedback/loader` — public entry for the loader architecture.\n *\n * Same shape as the direct-import path (`@mhosaic/feedback`), but the\n * widget bundle is fetched at runtime from a CDN URL the Mhosaic backend\n * dictates via the manifest endpoint. Letting hosts switch from the\n * direct-import path to the loader path gives them auto-updates without\n * any further code changes — Mhosaic ships a new Release row, every host\n * picks up the new bundle on next page load.\n *\n * Public surface MUST mirror the direct-import path exactly:\n *   - `createFeedback(config)` returns a `FeedbackApi`-shaped handle.\n *   - Method calls before the bundle finishes loading queue up and replay.\n *   - `submit()` returns a Promise that resolves after the bundle is up\n *     and the real submit completes.\n *\n * If the manifest endpoint reports the widget is disabled for this\n * project (`enabled: false` — kill switch or no stable release configured),\n * the deferred handle silently no-ops void calls and rejects `submit()`\n * calls with a clear error. The host page never crashes.\n */\n\nimport type { FeedbackApi, FeedbackConfig } from '../types'\nimport type { ErrorTrackingOptions } from '../modules/error-tracking'\nimport { injectBundle } from './bundleLoader'\nimport { fetchManifest } from './manifest'\nimport { createDeferredApi, type DeferredHandle } from './deferredApi'\n\nexport type { FeedbackApi, FeedbackConfig } from '../types'\nexport type { Manifest } from './manifest'\n\n/** Loader config = the public widget config plus the loader-only knobs the\n *  host can set to override what the manifest dictates. */\nexport type LoaderConfig = FeedbackConfig & {\n  /** Override the bundle's default error capture. Omit (or pass `undefined`)\n   *  to let the project's manifest flag decide (which itself defaults on). */\n  errorTracking?: boolean | ErrorTrackingOptions | undefined\n}\n\nexport function createFeedback(config: LoaderConfig): FeedbackApi {\n  const deferred = createDeferredApi()\n  loadAndAttach(config, deferred).catch((err: Error) => {\n    // Surface to console — host devtools is the canonical channel for\n    // widget bootstrap errors. Production telemetry is the bundle's job\n    // (it can't run if it never loaded), so console.warn is the floor.\n    // eslint-disable-next-line no-console\n    console.warn('[mhosaic-feedback] widget did not load:', err.message)\n    deferred._fail(err)\n  })\n  return deferred\n}\n\nasync function loadAndAttach(\n  config: LoaderConfig,\n  deferred: DeferredHandle,\n): Promise<void> {\n  const manifest = await fetchManifest(config.endpoint, config.apiKey)\n  if (!manifest.enabled || !manifest.bundle_url || !manifest.sri_hash) {\n    // Disabled by Mhosaic (kill switch) or no stable release. Surface a\n    // clear failure so submit() callers see an error, but no host crash.\n    deferred._fail(\n      new Error(\n        `mhosaic-feedback: widget disabled for this project${\n          manifest.detail ? ` — ${manifest.detail}` : ''\n        }`,\n      ),\n    )\n    return\n  }\n  const bundle = await injectBundle({\n    bundleUrl: manifest.bundle_url,\n    sriHash: manifest.sri_hash,\n  })\n  const real: FeedbackApi = bundle.createFeedback({\n    ...config,\n    // Forward the project's per-end-user gating flag from the manifest unless\n    // the host set it explicitly. The widget then runs the visibility check.\n    requiresVisibilityCheck:\n      config.requiresVisibilityCheck ??\n      manifest.config?.requires_visibility_check ??\n      false,\n    // Error capture is armed INSIDE the bundle (the only auto-updating part\n    // of a loader install), so already-deployed hosts pick it up without a\n    // redeploy. Precedence: explicit host override > per-project manifest\n    // flag > on. The loader provider forwards its `errorTracking` prop here\n    // rather than wrapping host-side, so there's exactly one arm point (the\n    // real instance) — no deferred/real double-registration.\n    errorTracking:\n      config.errorTracking ?? manifest.config?.error_tracking ?? true,\n  })\n  deferred._attach(real)\n}\n"],"mappings":"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","names":[]}