{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @carverjs/embed-sdk\n *\n * Tiny, dependency-free SDK for games embedded in the CarverJS\n * marketplace. Games run inside a sandboxed <iframe> on a per-game\n * origin (`https://g-{gameId}.carverjs.dev`) and talk to the parent\n * shell exclusively through `window.parent.postMessage`. This SDK\n * wraps that channel so game code never hand-rolls message shapes.\n *\n * Shell-side counterpart: `src/lib/games/embed-protocol.ts` in\n * carverjs-web. The shell validates `event.origin` against the exact\n * per-game origin and strictly validates every message shape — a\n * malformed message is silently dropped, never an error. Keep the\n * shapes here in lockstep with that parser.\n *\n * Usage:\n *\n *   import { carver } from \"@carverjs/embed-sdk\";\n *\n *   carver.progress(40);                  // loading…\n *   carver.ready();                       // first frame rendered\n *   carver.score(1280, \"points\");\n *   carver.event(\"level-complete\", { level: 3 });\n *   carver.requestFullscreen();\n *   carver.error(\"asset-load-failed\", \"texture atlas 404\");\n *   carver.exit();\n *\n *   // Prove which signed-in marketplace user is playing — to YOUR own\n *   // game backend. The shell mints a short-lived signed token from the\n *   // player's session; your server verifies it against the published\n *   // JWKS and ties its saved data to `userId`.\n *   const id = await carver.getIdentity();\n *   if (id.ok) saveToMyBackend(id.token, id.userId);\n *   else if (id.reason === \"signin-required\") promptSignIn();\n *\n *   const unsubscribe = carver.subscribe((msg) => {\n *     if (msg.type === \"carver:pause\") {/* pause loop *\\/}\n *   });\n *\n * Every method is a safe no-op when the game runs outside an iframe\n * (local dev by opening index.html directly) or in a non-browser\n * environment (SSR, tests). Nothing here ever throws.\n *\n * Security note: outbound messages are posted with targetOrigin \"*\"\n * by default. They intentionally carry NO secrets — ready/score/\n * progress signals only — and the game cannot know which shell origin\n * embeds it (production, staging, localhost preview). If you ship a\n * game exclusively for one shell, pin it with\n * `carver.configure({ targetOrigin: \"https://carverjs.dev\" })`.\n *\n * ---------------------------------------------------------------------\n * PROTOCOL v2 — the inbound channel (added in 1.1.0, fully additive)\n * ---------------------------------------------------------------------\n *\n * v1 was outbound-only. v2 adds a versioned SHELL -> GAME channel so a\n * shell can hand a game its runtime configuration instead of baking it\n * into the bundle: ephemeral TURN credentials, a session token, locale,\n * a consent flag, and multiplayer room details. It also adds one closed,\n * whitelisted outbound telemetry envelope.\n *\n *   carver.requestInit();                 // ask the shell for config\n *   const off = carver.onInit((msg) => {  // auto-ACKs carver:init\n *     if (msg.type === \"carver:init\") boot(msg.payload);\n *     else refreshIce(msg.payload.iceServers);   // carver:ice\n *   });\n *\n *   carver.telemetry({\n *     objectId: \"task-3\", kcCode: \"NCERT.G6.FRAC.EQUIV\",\n *     success: true, attempts: 2, hintsUsed: 1, latencyBucket: \"lt15s\",\n *   });\n *\n * DIRECTION OF SECRETS. v2 inverts the v1 rule in one direction only.\n * Secrets may flow SHELL -> GAME (`sessionToken`, TURN `credential`,\n * `room.signalingToken`): the shell already holds them and the game\n * needs them to reach its own backend. Secrets must NEVER flow\n * GAME -> SHELL. Every outbound message in this file — v1 and v2 —\n * is still free of them, which is what keeps `targetOrigin: \"*\"` safe.\n * `carver.telemetry()` takes a CLOSED struct precisely so a game cannot\n * smuggle a token out through a free-form field.\n *\n * BECAUSE THE INBOUND CHANNEL NOW CARRIES SECRETS, a game that ships to\n * exactly one shell should pin BOTH directions:\n *\n *   carver.configure({\n *     targetOrigin: \"https://carverjs.dev\",\n *     parentOrigin: \"https://carverjs.dev\",\n *   });\n *\n * Without `parentOrigin` the only inbound check is `event.source ===\n * window.parent` — which is sound (nothing but the real embedder can be\n * that object) but does not tell you WHO the embedder is. MDN's guidance\n * is to verify identity with `origin` and `source`, then still validate\n * the syntax of what arrives; this SDK does the source check and the\n * shape check for you, and `parentOrigin` adds the origin check.\n *\n * DUAL TRANSPORT. A game also runs top-level inside a native WebView,\n * where there is no parent frame to post to. When `window.parent ===\n * window` AND `window.__carverNativeBridge` exists, every outbound\n * message is routed through `__carverNativeBridge.postMessage(JSON)`\n * instead, and the native shell delivers inbound messages by calling\n * `window.__carverShellDeliver(msg)`. The message schemas are IDENTICAL\n * on both transports — there are no bridge-specific types — and with\n * neither a parent frame nor a bridge everything stays a safe no-op.\n *\n * ONE HONEST EXCEPTION TO \"IDENTICAL\", and it is about VALUES, not shapes.\n * The iframe path hands `postMessage` a live object (structured clone); the\n * bridge path hands it `JSON.stringify` text. Every field of every closed\n * message here is a JSON primitive, so those are byte-for-byte the same on\n * both. `carver:event.payload` is free-form and the two serializers disagree\n * about the edges: a cycle clones but does not stringify (the bridge drops the\n * message), a function stringifies-by-omission but does not clone (the iframe\n * drops the message), and NaN/Infinity/undefined survive a clone but become\n * null or vanish in JSON. `carver.score(NaN)` is the same story — the shell\n * drops it either way, but it arrives as NaN over one transport and null over\n * the other. Keep `carver:event` payloads to plain JSON data and the two\n * transports stay indistinguishable.\n */\n\n/**\n * The inbound/outbound protocol revision this SDK speaks. Sent as `v`\n * on every v2 message so a shell can tell an old game from a new one, and\n * on the two v1 messages that gained an optional `v` in 1.1.0\n * (`carver:exit`, `carver:request-fullscreen`). The rest of v1 —\n * `carver:ready` and friends — carries no `v` and never will.\n */\nexport const PROTOCOL_VERSION = 2;\n\n// ---------------------------------------------------------------------------\n// Message types — keep in lockstep with carverjs-web embed-protocol.ts\n// ---------------------------------------------------------------------------\n\n/** Game booted and rendered its first frame. Shell hides the loader. */\nexport type CarverReadyMessage = {\n  type: \"carver:ready\";\n};\n\n/** Fatal game error. Shell swaps the iframe for an error card. */\nexport type CarverErrorMessage = {\n  type: \"carver:error\";\n  /** Machine-readable code, e.g. `asset-load-failed`. Shell truncates at 64 chars. */\n  code: string;\n  /** Human-readable description. Shell truncates at 500 chars. */\n  message: string;\n};\n\n/**\n * Arbitrary gameplay event for play-stat aggregation.\n *\n * FREE-FORM, AND THEREFORE SHELL-LOCAL. Because `payload` is author\n * controlled and unvalidated, a shell must treat `carver:event` as\n * play telemetry that stops at the shell: aggregate it, log it, show it\n * on a dashboard — but never relay it to a learning-telemetry endpoint.\n * The message for that is `carver:telemetry`, whose closed tuple is the\n * privacy whitelist. See {@link CarverTelemetryTuple}.\n */\nexport type CarverEventMessage = {\n  type: \"carver:event\";\n  /** Event name, e.g. `level-complete`. Shell rejects names over 128 chars. */\n  name: string;\n  /** Free-form JSON-cloneable payload. */\n  payload?: unknown;\n};\n\n/** Ask the shell to toggle fullscreen on the game's behalf. */\nexport type CarverRequestFullscreenMessage = {\n  type: \"carver:request-fullscreen\";\n  /**\n   * Protocol revision, present from 1.1.0 on. OPTIONAL so that code\n   * written against 1.0.x, which constructed this message without a\n   * `v`, still type-checks unchanged.\n   */\n  v?: number;\n};\n\n/** Game is done; shell may navigate back / show an end card. */\nexport type CarverExitMessage = {\n  type: \"carver:exit\";\n  /**\n   * Protocol revision, present from 1.1.0 on. OPTIONAL for the same\n   * reason as on {@link CarverRequestFullscreenMessage}.\n   */\n  v?: number;\n};\n\n/** Score report for player profile stats. */\nexport type CarverScoreMessage = {\n  type: \"carver:score\";\n  /** Finite number — NaN/±Infinity are dropped by the shell. */\n  value: number;\n  /** Optional display label, e.g. `coins`. Shell truncates at 64 chars. */\n  label?: string;\n};\n\n/** Load/level progress. Shell clamps to [0, 100]. */\nexport type CarverProgressMessage = {\n  type: \"carver:progress\";\n  percent: number;\n};\n\n// --- v2 outbound -----------------------------------------------------------\n\n/** Ask the shell to send (or re-send) `carver:init`. Safe to call twice. */\nexport type CarverInitRequestMessage = {\n  type: \"carver:init-request\";\n  v: number;\n};\n\n/** The game received and accepted a `carver:init`. Sent automatically by\n *  {@link carver.onInit}, so a shell can prove the handshake completed. */\nexport type CarverInitAckMessage = {\n  type: \"carver:init-ack\";\n  v: number;\n};\n\n/**\n * The game's ICE credentials stopped working (a TURN allocation was\n * refused, or `expiresAt` has passed). Asks the shell for a fresh\n * `carver:ice`. Carries no credential material — only the fact.\n */\nexport type CarverIceExpiredMessage = {\n  type: \"carver:ice-expired\";\n  v: number;\n};\n\n/** How long the learner took, coarsened into buckets on purpose: an exact\n *  millisecond latency is a behavioural fingerprint, a bucket is not. */\nexport type CarverLatencyBucket = \"lt5s\" | \"lt15s\" | \"lt60s\" | \"gte60s\";\n\n/**\n * THE CANONICAL LEARNING-TELEMETRY UNIT. One interaction, one tuple.\n *\n * This struct is CLOSED — there is deliberately no free-form payload\n * field — because THE CLOSED TYPE IS THE PRIVACY WHITELIST. A game\n * cannot attach a name, a device id, a raw timestamp or a session token\n * to a learning event, because there is nowhere to put one. Widening\n * this type is a privacy review, not a refactor.\n *\n * It maps 1:1 onto the server's whitelisted telemetry columns:\n *\n *   objectId                        the IR object the learner acted on\n *   kcCode            -> kc_code    knowledge component, or null when\n *                                   the interaction maps to none\n *   success/attempts/hintsUsed\n *                     -> the closed evidence enum (the three fields are\n *                        folded server-side into one evidence value;\n *                        they are never stored raw)\n *   latencyBucket     -> latency_bucket\n *   misconceptions    -> misconception_code, one row per entry. Entries\n *                        are IR-REGISTERED DETECTOR IDS, not free text:\n *                        an id the IR does not declare is dropped.\n *   probeItemId                     correlates a retention-probe answer\n *                                   back to the probe item that asked it\n *\n * `carver:telemetry` is the ONLY message a shell may relay to a\n * learning-telemetry endpoint. `carver:event` stays free-form for\n * non-learning play stats and must never leave the shell.\n */\nexport type CarverTelemetryTuple = {\n  /** The IR object this interaction was about, e.g. a task or item id. */\n  objectId: string;\n  /** Knowledge component code, or `null` when the interaction maps to none. */\n  kcCode: string | null;\n  /** Did the learner get it right. */\n  success: boolean;\n  /** Attempts spent on this object, including the successful one. */\n  attempts: number;\n  /** Scaffolds/hints revealed before answering. */\n  hintsUsed: number;\n  /** Time-to-answer, bucketed. */\n  latencyBucket: CarverLatencyBucket;\n  /** IR-registered misconception detector ids that fired. */\n  misconceptions?: string[];\n  /** Retention-probe item this answer belongs to, when it is one. */\n  probeItemId?: number;\n};\n\n/** One learning interaction, in the only shape a shell may forward. */\nexport type CarverTelemetryMessage = {\n  type: \"carver:telemetry\";\n  v: number;\n  tuple: CarverTelemetryTuple;\n};\n\n/**\n * Every message kind the game can send to the shell.\n *\n * v2 WIDENS THIS UNION. A shell that switches exhaustively over it will\n * see new arms; that is the intended, documented cost of a minor bump,\n * and every new arm is safely ignorable by a shell that does not know\n * it. No existing arm changed except for an OPTIONAL `v` on\n * `carver:exit` and `carver:request-fullscreen`.\n */\nexport type CarverOutboundMessage =\n  | CarverReadyMessage\n  | CarverErrorMessage\n  | CarverEventMessage\n  | CarverRequestFullscreenMessage\n  | CarverExitMessage\n  | CarverScoreMessage\n  | CarverProgressMessage\n  | CarverInitRequestMessage\n  | CarverInitAckMessage\n  | CarverIceExpiredMessage\n  | CarverTelemetryMessage;\n\n/**\n * Messages the SHELL sends to the game. v1 defined the channel but never\n * used it; v2 fills it with `carver:init`, `carver:ice`, `carver:pause`\n * and `carver:resume` (see {@link CarverInboundV2Message}). Treat unknown\n * types as no-ops — that is what keeps the channel forward-compatible.\n *\n * DELIBERATELY STILL OPEN in v2. The concrete v2 inbound shapes below\n * are all assignable to it, so `subscribe()` keeps its exact 1.0.x\n * signature and no existing handler breaks. Narrow on `msg.type` and\n * cast to the concrete type, or use {@link carver.onInit}, which hands\n * you `carver:init` / `carver:ice` already typed.\n */\nexport type CarverInboundMessage = {\n  type: `carver:${string}`;\n  payload?: unknown;\n};\n\nexport type CarverInboundHandler = (message: CarverInboundMessage) => void;\n\n// --- v2 inbound ------------------------------------------------------------\n\n/** Whether the player (or their guardian) consented to data collection. */\nexport type CarverConsent = {\n  granted: boolean;\n};\n\n/**\n * ICE servers for WebRTC, as `RTCPeerConnection` takes them.\n *\n * TURN entries carry a `credential`. This is the one place a secret\n * legitimately crosses into the game, and it only ever travels\n * shell -> game. Never echo one back out.\n */\nexport type CarverIceBundle = {\n  iceServers: RTCIceServer[];\n  /** ISO-8601 instant after which these credentials stop working. */\n  expiresAt?: string;\n};\n\n/** Who the shell says is playing. Display data, not proof of identity. */\nexport type CarverPlayerInfo = {\n  displayName?: string;\n  id?: string;\n};\n\n/** Multiplayer room the shell has already placed this player in. */\nexport type CarverRoomInfo = {\n  roomId: string;\n  role: \"host\" | \"peer\";\n  /** Short-lived credential for the signaling backend. A secret. */\n  signalingToken?: string;\n  /** Backend-specific namespace/app id the room lives under. */\n  signalingNamespace?: string;\n};\n\n/** Where the game's own backend lives, when the shell chooses it. */\nexport type CarverApiInfo = {\n  baseUrl: string;\n};\n\n/**\n * Everything a shell can hand a game at boot. EVERY FIELD IS OPTIONAL:\n * a shell sends what it has, a game requires what it needs, and neither\n * has to agree with the other for the channel to work.\n *\n * INTENTIONALLY GENERIC. This is the public engine contract, so it\n * carries no product's vocabulary. A product narrows it to its own\n * strict type on arrival — `@moneytales/shell-config` is one such\n * narrowing — rather than pushing product fields down into here.\n */\nexport type CarverInitPayload = {\n  /** Bearer token for the game's own backend. A secret. */\n  sessionToken?: string;\n  /** BCP-47-ish language tag the shell wants the game rendered in. */\n  locale?: string;\n  consent?: CarverConsent;\n  ice?: CarverIceBundle;\n  player?: CarverPlayerInfo;\n  room?: CarverRoomInfo;\n  api?: CarverApiInfo;\n  /** Shell-specific extras. Unvalidated — narrow before you trust it. */\n  extra?: Record<string, unknown>;\n};\n\n/** Runtime configuration, sent by the shell at boot or on request. */\nexport type CarverInitMessage = {\n  type: \"carver:init\";\n  v: number;\n  payload: CarverInitPayload;\n};\n\n/**\n * Fresh ICE credentials, replacing whatever `carver:init` carried.\n * Shells send this unprompted before `expiresAt`, or in answer to\n * `carver:ice-expired`.\n */\nexport type CarverIceMessage = {\n  type: \"carver:ice\";\n  v: number;\n  payload: CarverIceBundle;\n};\n\n/** The shell backgrounded the game: halt timers, audio and the render loop. */\nexport type CarverPauseMessage = {\n  type: \"carver:pause\";\n  v: number;\n};\n\n/** The shell foregrounded the game again. */\nexport type CarverResumeMessage = {\n  type: \"carver:resume\";\n  v: number;\n};\n\n/** The concrete inbound shapes v2 defines. All assignable to\n *  {@link CarverInboundMessage}, which stays open. */\nexport type CarverInboundV2Message =\n  | CarverInitMessage\n  | CarverIceMessage\n  | CarverPauseMessage\n  | CarverResumeMessage;\n\n/** What {@link carver.onInit} delivers: config, and later ICE refreshes. */\nexport type CarverInitHandler = (\n  message: CarverInitMessage | CarverIceMessage,\n) => void;\n\n// ---------------------------------------------------------------------------\n// Native bridge — the second transport\n// ---------------------------------------------------------------------------\n\n/**\n * The object a native WebView shell injects to receive outbound\n * messages. Both WKWebView (`window.webkit.messageHandlers.<name>`) and\n * Android's `addJavascriptInterface` expose exactly this shape, so a\n * native shell aliases one onto `window.__carverNativeBridge`.\n */\nexport type CarverNativeBridge = {\n  postMessage: (json: string) => void;\n};\n\ndeclare global {\n  interface Window {\n    /** Set by a native WebView shell. See {@link CarverNativeBridge}. */\n    __carverNativeBridge?: CarverNativeBridge;\n    /**\n     * Set by this SDK once something subscribes. A native shell calls it\n     * to deliver an inbound message, passing either the object or its\n     * JSON text. Junk is ignored, never thrown.\n     */\n    __carverShellDeliver?: (message: unknown) => void;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Identity — carver.getIdentity()\n// ---------------------------------------------------------------------------\n\n/** Why an identity request could not return a token. */\nexport type CarverIdentityFailureReason =\n  /** No marketplace user is signed in (or the visitor is anonymous). */\n  | \"signin-required\"\n  /** Not running inside the marketplace shell (e.g. local dev). */\n  | \"not-embedded\"\n  /** The shell did not answer in time. */\n  | \"timeout\"\n  /** Too many identity requests in a short window. */\n  | \"rate-limited\"\n  /** Any other failure (network, shell error). */\n  | \"error\";\n\n/**\n * Result of `carver.getIdentity()`.\n *\n * On success, `token` is a short-lived RS256 JWT proving which\n * marketplace user is playing. Send it to YOUR OWN backend and verify it\n * against the marketplace JWKS (`https://www.carverjs.dev/.well-known/jwks.json`):\n * check `iss`, `aud === <your gameId>`, and `exp`. `userId` is a stable,\n * opaque per-game id (the token's `sub`) — the same player keeps the\n * same id for your game across sessions, and it cannot be correlated to\n * other games. `expiresAt` is a Unix epoch in milliseconds.\n */\nexport type CarverIdentity =\n  | { ok: true; token: string; userId: string; expiresAt: number }\n  | { ok: false; reason: CarverIdentityFailureReason };\n\nexport type CarverConfig = {\n  /**\n   * targetOrigin for outbound postMessage. Default \"*\" — outbound\n   * messages carry no secrets and the embedding shell origin is not\n   * knowable from inside the sandbox. Pin to a single origin if your\n   * game only ever ships to one shell.\n   */\n  targetOrigin?: string;\n  /**\n   * When set, inbound messages are additionally required to come from\n   * this exact origin (on top of the always-on `event.source ===\n   * window.parent` check). Default: source check only.\n   */\n  parentOrigin?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Internals\n// ---------------------------------------------------------------------------\n\nconst config: Required<CarverConfig> = {\n  targetOrigin: \"*\",\n  parentOrigin: \"\",\n};\n\n/** True when running in a browser AND inside someone else's frame. */\nfunction isEmbedded(): boolean {\n  try {\n    return typeof window !== \"undefined\" && window.parent !== window;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * The native bridge, when this game is top-level in a WebView that\n * injected one.\n *\n * RESOLVED PER CALL, not cached at module load. A WebView routinely\n * injects its bridge after the document starts evaluating scripts, so a\n * value captured at import time would be `undefined` on exactly the\n * shells this exists for. The check is three property reads.\n *\n * An iframe ALWAYS wins: if there is a parent frame, postMessage is the\n * transport, bridge or no bridge. A page cannot be in both situations,\n * and preferring the frame keeps the marketplace path untouched.\n */\nfunction nativeBridge(): CarverNativeBridge | null {\n  try {\n    if (typeof window === \"undefined\") return null;\n    if (window.parent !== window) return null;\n    const bridge = window.__carverNativeBridge;\n    return bridge != null && typeof bridge.postMessage === \"function\"\n      ? bridge\n      : null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Post to the shell over whichever transport exists. Never throws;\n * silently no-ops when there is neither a parent frame nor a bridge.\n */\nfunction post(message: CarverOutboundMessage): void {\n  const bridge = nativeBridge();\n  if (bridge !== null) {\n    try {\n      bridge.postMessage(JSON.stringify(message));\n    } catch {\n      // A cyclic carver:event payload (JSON.stringify throws on those,\n      // where structuredClone throws on functions) or a bridge the\n      // WebView already tore down. Same rule as the iframe path: a\n      // telemetry message must never crash the game.\n    }\n    return;\n  }\n  if (!isEmbedded()) return;\n  try {\n    window.parent.postMessage(message, config.targetOrigin);\n  } catch {\n    // DataCloneError (non-cloneable payload) or a detached parent.\n    // Swallow — telemetry messages must never crash the game.\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Inbound dispatch — one path, both transports\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribers, in an ARRAY rather than a Set.\n *\n * `subscribe(fn)` twice with the same function reference registered two\n * listeners in 1.0.x and called the handler twice; a Set would silently\n * dedupe it. Each entry is its own object so unsubscribe removes\n * exactly the registration it was handed back for.\n */\ntype Subscription = { handler: CarverInboundHandler };\nconst subscriptions: Subscription[] = [];\nlet inboundInstalled = false;\n\n/**\n * Validate an inbound message and hand it to every subscriber.\n *\n * THE ONLY DELIVERY PATH. Both the `message` listener and\n * `window.__carverShellDeliver` funnel through here, which is what\n * makes the two transports carry identical schemas rather than\n * near-identical ones that drift.\n *\n * Trust is established by the CALLER (source/origin for postMessage,\n * being on our own window for the bridge); this function only decides\n * whether the thing is shaped like a carver message at all. MDN's rule:\n * having verified identity, still verify the syntax.\n */\nfunction deliver(raw: unknown): void {\n  let data: unknown = raw;\n  // A native shell that mirrors our own outbound encoding hands us JSON\n  // text. Accept both rather than making the bridge asymmetric.\n  if (typeof data === \"string\") {\n    try {\n      data = JSON.parse(data);\n    } catch {\n      return;\n    }\n  }\n  if (typeof data !== \"object\" || data === null) return;\n  const type = (data as { type?: unknown }).type;\n  if (typeof type !== \"string\" || !type.startsWith(\"carver:\")) return;\n  const message = data as CarverInboundMessage;\n  // Copied first: a handler that unsubscribes itself (or subscribes\n  // another) must not reindex the array mid-dispatch.\n  for (const subscription of [...subscriptions]) {\n    try {\n      subscription.handler(message);\n    } catch {\n      // A throwing handler must not kill the message channel.\n    }\n  }\n}\n\n/**\n * Install the `message` listener and the native deliver hook, once.\n *\n * LAZY, NOT AT MODULE LOAD, because this package declares\n * `sideEffects: false` — a bundler is entitled to drop a module whose\n * only effect is at import time, and a game that tree-shook its own\n * inbound channel away would be a very quiet bug. Called from every\n * entry point that needs inbound to work.\n */\nfunction ensureInbound(): void {\n  if (inboundInstalled || typeof window === \"undefined\") return;\n  inboundInstalled = true;\n\n  window.addEventListener(\"message\", (e: MessageEvent): void => {\n    // Only the direct embedding frame is trusted — not siblings, not\n    // nested children, not random windows holding a reference to us.\n    if (e.source !== window.parent) return;\n    if (config.parentOrigin.length > 0 && e.origin !== config.parentOrigin) {\n      return;\n    }\n    deliver(e.data);\n  });\n\n  // The bridge's inbound half. It lives on our own window, so anything\n  // that can call it already runs in this game's document — there is no\n  // origin to check, only a shape.\n  window.__carverShellDeliver = (message: unknown): void => {\n    deliver(message);\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Identity internals (request/response over postMessage, correlated by id)\n// ---------------------------------------------------------------------------\n\n/** How long to wait for the shell to answer an identity request. */\nconst IDENTITY_TIMEOUT_MS = 10_000;\n\ntype PendingIdentity = {\n  resolve: (value: CarverIdentity) => void;\n  timer: ReturnType<typeof setTimeout>;\n};\n\nconst pendingIdentity = new Map<string, PendingIdentity>();\nlet identityListenerInstalled = false;\nlet identityCounter = 0;\n\nconst IDENTITY_REASONS: ReadonlySet<string> = new Set([\n  \"signin-required\",\n  \"not-embedded\",\n  \"timeout\",\n  \"rate-limited\",\n  \"error\",\n]);\n\nfunction toReason(value: unknown): CarverIdentityFailureReason {\n  return typeof value === \"string\" && IDENTITY_REASONS.has(value)\n    ? (value as CarverIdentityFailureReason)\n    : \"error\";\n}\n\n/** Correlation id for an identity round-trip. Uniqueness within one game\n *  session is all that's needed — a counter plus a random suffix. */\nfunction nextRequestId(): string {\n  identityCounter += 1;\n  const rand =\n    typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\"\n      ? crypto.randomUUID()\n      : Math.random().toString(36).slice(2);\n  return `cjs-id-${identityCounter}-${rand}`;\n}\n\n/** Lazily install the single listener that resolves identity results. */\nfunction ensureIdentityListener(): void {\n  if (identityListenerInstalled || typeof window === \"undefined\") return;\n  identityListenerInstalled = true;\n\n  window.addEventListener(\"message\", (e: MessageEvent): void => {\n    // Same trust rules as subscribe(): the direct parent only, plus the\n    // optional pinned parentOrigin.\n    if (e.source !== window.parent) return;\n    if (config.parentOrigin.length > 0 && e.origin !== config.parentOrigin) {\n      return;\n    }\n    const data: unknown = e.data;\n    if (typeof data !== \"object\" || data === null) return;\n    const d = data as {\n      type?: unknown;\n      requestId?: unknown;\n      ok?: unknown;\n      token?: unknown;\n      userId?: unknown;\n      expiresAt?: unknown;\n      reason?: unknown;\n    };\n    if (d.type !== \"carver:identity-result\") return;\n    if (typeof d.requestId !== \"string\") return;\n\n    const pending = pendingIdentity.get(d.requestId);\n    if (!pending) return;\n    pendingIdentity.delete(d.requestId);\n    clearTimeout(pending.timer);\n\n    if (\n      d.ok === true &&\n      typeof d.token === \"string\" &&\n      typeof d.userId === \"string\" &&\n      typeof d.expiresAt === \"number\"\n    ) {\n      pending.resolve({\n        ok: true,\n        token: d.token,\n        userId: d.userId,\n        expiresAt: d.expiresAt,\n      });\n    } else {\n      pending.resolve({ ok: false, reason: toReason(d.reason) });\n    }\n  });\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Override targetOrigin / parentOrigin. Call once at boot, before subscribe. */\nfunction configure(next: CarverConfig): void {\n  if (typeof next.targetOrigin === \"string\" && next.targetOrigin.length > 0) {\n    config.targetOrigin = next.targetOrigin;\n  }\n  if (typeof next.parentOrigin === \"string\") {\n    config.parentOrigin = next.parentOrigin;\n  }\n}\n\n/** Signal the game booted and rendered its first frame. */\nfunction ready(): void {\n  post({ type: \"carver:ready\" });\n}\n\n/** Report a fatal error. The shell replaces the game with an error card. */\nfunction error(code: string, message: string): void {\n  post({ type: \"carver:error\", code, message });\n}\n\n/** Emit a gameplay event for play-stat aggregation. */\nfunction event(name: string, payload?: unknown): void {\n  const msg: CarverEventMessage = { type: \"carver:event\", name };\n  if (payload !== undefined) msg.payload = payload;\n  post(msg);\n}\n\n/** Ask the shell to toggle fullscreen. Call from a user-gesture handler. */\nfunction requestFullscreen(): void {\n  post({ type: \"carver:request-fullscreen\", v: PROTOCOL_VERSION });\n}\n\n/** Signal the game is finished and ask the shell to close the player. */\nfunction exit(): void {\n  post({ type: \"carver:exit\", v: PROTOCOL_VERSION });\n}\n\n/** Report a score. `value` must be a finite number. */\nfunction score(value: number, label?: string): void {\n  const msg: CarverScoreMessage = { type: \"carver:score\", value };\n  if (label !== undefined) msg.label = label;\n  post(msg);\n}\n\n/** Report load/level progress. Clamped to [0, 100] on both ends. */\nfunction progress(percent: number): void {\n  const clamped = Number.isFinite(percent)\n    ? Math.min(100, Math.max(0, percent))\n    : 0;\n  post({ type: \"carver:progress\", percent: clamped });\n}\n\n/**\n * Listen for shell -> game messages. Only events whose `source` is the\n * direct parent window are delivered (plus an origin check when\n * `parentOrigin` is configured), and — in a native WebView — whatever\n * the shell hands to `window.__carverShellDeliver`. Returns an\n * unsubscribe function.\n */\nfunction subscribe(handler: CarverInboundHandler): () => void {\n  if (typeof window === \"undefined\") return () => {};\n  ensureInbound();\n\n  const subscription: Subscription = { handler };\n  subscriptions.push(subscription);\n\n  return () => {\n    const at = subscriptions.indexOf(subscription);\n    if (at !== -1) subscriptions.splice(at, 1);\n  };\n}\n\n/**\n * Ask the shell for a short-lived signed token identifying the\n * signed-in marketplace player, to send to YOUR OWN game backend.\n *\n * Always resolves (never rejects): `{ ok: true, token, userId, expiresAt }`\n * on success, or `{ ok: false, reason }` otherwise — `\"signin-required\"`\n * when no real account is signed in, `\"not-embedded\"` outside the\n * marketplace, `\"timeout\"` if the shell doesn't answer. Verify the token\n * server-side against the marketplace JWKS; never trust it client-side.\n *\n * IFRAME ONLY, BY DESIGN. This is the marketplace's identity feature and\n * it resolves `\"not-embedded\"` over the native bridge. A native shell\n * already holds the player's session and hands the game a `sessionToken`\n * through `carver:init` — that is the same job, done by the transport\n * that actually has the credential.\n */\nfunction getIdentity(): Promise<CarverIdentity> {\n  if (!isEmbedded()) {\n    return Promise.resolve({ ok: false, reason: \"not-embedded\" });\n  }\n  ensureIdentityListener();\n\n  const requestId = nextRequestId();\n  return new Promise<CarverIdentity>((resolve) => {\n    const timer = setTimeout(() => {\n      pendingIdentity.delete(requestId);\n      resolve({ ok: false, reason: \"timeout\" });\n    }, IDENTITY_TIMEOUT_MS);\n\n    pendingIdentity.set(requestId, { resolve, timer });\n\n    try {\n      window.parent.postMessage(\n        { type: \"carver:identity-request\", requestId },\n        config.targetOrigin,\n      );\n    } catch {\n      clearTimeout(timer);\n      pendingIdentity.delete(requestId);\n      resolve({ ok: false, reason: \"error\" });\n    }\n  });\n}\n\n// --- v2 helpers ------------------------------------------------------------\n\n/**\n * Ask the shell to send `carver:init`.\n *\n * Installs the inbound plumbing first, so a shell that answers\n * synchronously cannot beat the listener into place. Idempotent from\n * the game's side, and shells are required to treat a repeat as a\n * re-send rather than an error, so calling it on a retry is fine.\n */\nfunction requestInit(): void {\n  ensureInbound();\n  post({ type: \"carver:init-request\", v: PROTOCOL_VERSION });\n}\n\n/**\n * Subscribe to runtime configuration: `carver:init` once, then every\n * `carver:ice` refresh. ACKs each `carver:init` automatically with\n * `carver:init-ack` so the shell can prove the game took its config.\n *\n * Returns an unsubscribe function. Does NOT send `carver:init-request`\n * — call {@link carver.requestInit} for that, after subscribing.\n */\nfunction onInit(handler: CarverInitHandler): () => void {\n  return subscribe((message) => {\n    if (message.type === \"carver:init\") {\n      const init = message as CarverInitMessage;\n      // ACK BEFORE the handler runs: a handler that throws is caught by\n      // deliver() and would otherwise swallow the acknowledgement too,\n      // leaving the shell waiting on a game that did receive its config.\n      post({ type: \"carver:init-ack\", v: PROTOCOL_VERSION });\n      handler(init);\n      return;\n    }\n    if (message.type === \"carver:ice\") {\n      handler(message as CarverIceMessage);\n    }\n  });\n}\n\n/**\n * Report one learning interaction. See {@link CarverTelemetryTuple} —\n * the tuple is closed on purpose and is the whole privacy whitelist.\n *\n * THE FIELDS ARE PICKED, NOT FORWARDED. TypeScript's excess-property check\n * fires only on a fresh object literal at the call site, so\n *\n *     const t = { ...internalEvent, objectId, kcCode, success, attempts,\n *                 hintsUsed, latencyBucket };\n *     carver.telemetry(t);            // compiles clean\n *\n * would have shipped every other property of `internalEvent` — a device id, a\n * raw timestamp, a session token — out to the shell, and `carver:telemetry` is\n * the one message a shell is allowed to relay onward to a server. A\n * compile-time whitelist is not a whitelist when a variable defeats it, so the\n * eight known keys are copied out by hand and nothing else can ride along.\n */\nfunction telemetry(tuple: CarverTelemetryTuple): void {\n  const picked: CarverTelemetryTuple = {\n    objectId: tuple.objectId,\n    kcCode: tuple.kcCode,\n    success: tuple.success,\n    attempts: tuple.attempts,\n    hintsUsed: tuple.hintsUsed,\n    latencyBucket: tuple.latencyBucket,\n  };\n  // Copied element by element: the caller's array is theirs to mutate after\n  // this call, and a shared reference would let it change under the shell.\n  if (Array.isArray(tuple.misconceptions)) {\n    picked.misconceptions = tuple.misconceptions.filter(\n      (code): code is string => typeof code === \"string\",\n    );\n  }\n  if (typeof tuple.probeItemId === \"number\") {\n    picked.probeItemId = tuple.probeItemId;\n  }\n  post({ type: \"carver:telemetry\", v: PROTOCOL_VERSION, tuple: picked });\n}\n\n/**\n * Tell the shell the current ICE credentials no longer work, and ask\n * for a fresh `carver:ice`. Sends no credential material, only the fact.\n */\nfunction iceExpired(): void {\n  post({ type: \"carver:ice-expired\", v: PROTOCOL_VERSION });\n}\n\nexport const carver = {\n  configure,\n  isEmbedded,\n  ready,\n  error,\n  event,\n  requestFullscreen,\n  exit,\n  score,\n  progress,\n  subscribe,\n  getIdentity,\n  requestInit,\n  onInit,\n  telemetry,\n  iceExpired,\n} as const;\n\nexport default carver;\n"],"mappings":"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","names":[]}