{"version":3,"sources":["../../src/checkout/index.ts","../../src/stripe/client.ts","../../src/checkout/checkout.ts","../../src/cart/store.ts","../../src/cart/handlers.ts","../../src/checkout/handler.ts"],"sourcesContent":["export {\n  createCheckoutSession,\n  computeApplicationFee,\n  type CreateCheckoutOptions,\n  type ConnectOptions,\n  type CheckoutResult,\n} from './checkout';\nexport {\n  createCheckoutHandler,\n  type CheckoutHandlerOptions,\n} from './handler';\n","/**\n * F136 Phase 1 — Stripe SDK wrapper.\n *\n * Lazy-init: the Stripe client is constructed the first time it's needed,\n * not at import time. Two reasons:\n *   1. Tests can run without STRIPE_SECRET_KEY (calls are mocked elsewhere).\n *   2. Sites that don't actually use Stripe (free-tier marketing sites\n *      that only consume product collections) won't crash on missing env.\n *\n * Mode: per-site. Each site loads its own STRIPE_SECRET_KEY from env;\n * `getStripe(secretKey?)` accepts an explicit key for multi-tenant cms-admin\n * where the key lives in the site config rather than process.env.\n */\nimport Stripe from 'stripe';\n\nconst cache = new Map<string, Stripe>();\n\n/**\n * Get a memoized Stripe client.\n *\n * @param secretKey — explicit key (e.g. site.stripeSecretKey from cms-admin\n *                    site config). Falls back to process.env.STRIPE_SECRET_KEY.\n */\nexport function getStripe(secretKey?: string): Stripe {\n  const key = secretKey ?? process.env.STRIPE_SECRET_KEY;\n  if (!key) {\n    throw new Error(\n      '[cms-shop] STRIPE_SECRET_KEY is not set. ' +\n      'Set it as an env var on this site (or pass secretKey explicitly to getStripe).',\n    );\n  }\n  let client = cache.get(key);\n  if (!client) {\n    client = new Stripe(key, {\n      // Pin the API version so future Stripe upgrades don't silently\n      // change response shapes mid-deploy. We use the SDK's own\n      // pinned literal (`Stripe.API_VERSION`) so the value always\n      // matches the version the installed types were built against.\n      apiVersion: Stripe.API_VERSION,\n      typescript: true,\n      // Identify the integration in Stripe's logs — easier debugging\n      // when multiple webhouse sites share a Stripe account.\n      appInfo: {\n        name: '@webhouse/cms-shop',\n        version: '0.3.0',\n        url: 'https://docs.webhouse.app',\n      },\n    });\n    cache.set(key, client);\n  }\n  return client;\n}\n\n/** Test/cleanup hook — clears the memoized client cache. */\nexport function resetStripeClientCache(): void {\n  cache.clear();\n}\n","/**\n * F136 Phase 1 — Stripe Checkout Session creator.\n *\n * Translates a CMS cart into a Stripe Checkout Session and returns the\n * redirect URL. The actual order document is created later by the\n * webhook handler when Stripe confirms payment — never trust the\n * client to tell us \"the order is paid\".\n *\n * Two operating modes:\n *   1. Single-merchant: the site owns the Stripe account. Just pass\n *      `secretKey` (or set STRIPE_SECRET_KEY env). Money lands on that\n *      account directly.\n *   2. Marketplace (Stripe Connect): the platform (e.g. webhouse.app)\n *      owns the Stripe account, each merchant has a connected\n *      `acct_xxx`. Pass `connect: { destinationAccountId, applicationFeeAmount }`\n *      and money is transferred to the merchant minus the platform fee.\n *      Patterns lifted from the proven sanneandersen-site implementation\n *      (verified with real Stripe demo transactions, May 2026).\n *\n * Strategy: prefer existing `stripePriceIds` on each product (created\n * by syncProductToStripe). If a product hasn't been synced yet, fall\n * back to `price_data` inline — works but loses Stripe's reporting per\n * Price.\n */\nimport type Stripe from 'stripe';\nimport { getStripe } from '../stripe/client';\nimport type { ShopCart, ShopProduct } from '../types';\n\n// Stripe SDK declaration-merging on the Checkout namespace doesn't\n// surface SessionCreateParams reliably across module-resolution modes;\n// we infer the param type from the actual `create` call signature\n// instead — robust against namespace path quirks.\ntype CheckoutSessionCreateParams = NonNullable<\n  Parameters<Stripe['checkout']['sessions']['create']>[0]\n>;\ntype CheckoutLineItem = NonNullable<\n  CheckoutSessionCreateParams['line_items']\n>[number];\ntype AllowedCountry = NonNullable<\n  CheckoutSessionCreateParams['shipping_address_collection']\n>['allowed_countries'][number];\ntype CheckoutLocale = NonNullable<CheckoutSessionCreateParams['locale']>;\n\nexport interface ConnectOptions {\n  /** `acct_xxx` of the merchant's connected account. */\n  destinationAccountId: string;\n  /**\n   * Platform fee in minor units (øre/cents). Either supply this OR\n   * `applicationFeePercent` — not both. Wins if both are set.\n   */\n  applicationFeeAmount?: number;\n  /**\n   * Platform fee as a percentage of the cart total (0–100). Computed\n   * server-side at checkout time. Convenient for sites that price by\n   * percentage rather than fixed amount.\n   */\n  applicationFeePercent?: number;\n}\n\nexport interface CreateCheckoutOptions {\n  /** Stripe secret key (multi-tenant). Falls back to env. */\n  secretKey?: string;\n  /** Where Stripe redirects on success — `{CHECKOUT_SESSION_ID}` placeholder is allowed. */\n  successUrl: string;\n  /** Where Stripe redirects on cancel. */\n  cancelUrl: string;\n  /** Resolve a product id → ShopProduct so we can pull Stripe price ids. */\n  loadProduct(id: string): Promise<ShopProduct | null>;\n  /** Force Stripe to collect a shipping address (default: true if any cart line is physical). */\n  collectShippingAddress?: boolean;\n  /** Stripe shipping rate ids to offer at checkout. */\n  shippingRateIds?: string[];\n  /** Locale shown in Stripe Checkout UI (e.g. 'da', 'en'). Default: cart.locale. */\n  locale?: CheckoutLocale;\n  /** Allowed countries for shipping. ISO 3166-1 alpha-2. */\n  allowedShippingCountries?: string[];\n  /** Extra metadata to attach to the Stripe Session. */\n  metadata?: Record<string, string>;\n  /** Stripe Connect — turn this on for marketplace mode. */\n  connect?: ConnectOptions;\n  /**\n   * Force Stripe to create+attach a Customer object even for guest\n   * checkouts. Without this the Customer column in the Stripe dashboard\n   * is blank — which makes the merchant's life much harder.\n   * Default: true.\n   */\n  alwaysCreateCustomer?: boolean;\n  /**\n   * Short reference shown in the Stripe dashboard's payments table.\n   * The webhook handler uses this to find the matching CMS document\n   * by order/cart id without parsing metadata. Default: cart.id.\n   */\n  clientReferenceId?: string;\n  /**\n   * Human-friendly description shown in Stripe dashboard's payments\n   * table — what is this charge actually for? Default: short summary\n   * built from the first line item title + total.\n   */\n  description?: string;\n  /**\n   * Send Stripe-generated receipt email. Default: cart.email if present.\n   * Set to false to disable (e.g. if the site sends its own receipts).\n   */\n  receiptEmail?: string | false;\n  /**\n   * Discriminator written to `session.metadata.kind`. Lets the webhook\n   * handler dispatch one event type to multiple flows (e.g. 'order',\n   * 'subscription', 'donation'). Default: 'order'.\n   */\n  kind?: string;\n}\n\nexport interface CheckoutResult {\n  sessionId: string;\n  url: string;\n}\n\nfunction buildDefaultDescription(cart: ShopCart): string {\n  const first = cart.items[0];\n  if (!first) return `Order ${cart.id}`;\n  if (cart.items.length === 1) {\n    return `${first.titleSnapshot} (×${first.quantity})`;\n  }\n  const more = cart.items.length - 1;\n  return `${first.titleSnapshot} +${more} more`;\n}\n\nexport async function createCheckoutSession(\n  cart: ShopCart,\n  opts: CreateCheckoutOptions,\n): Promise<CheckoutResult> {\n  if (cart.items.length === 0) {\n    throw new Error('[cms-shop] cannot checkout an empty cart');\n  }\n\n  const stripe = getStripe(opts.secretKey);\n\n  const lineItems: CheckoutLineItem[] = [];\n  let needsShipping = false;\n\n  for (const line of cart.items) {\n    const product = await opts.loadProduct(line.productId);\n    if (!product) {\n      throw new Error(\n        `[cms-shop] cart references missing product ${line.productId}`,\n      );\n    }\n    if (\n      product.data.productType === 'physical' ||\n      product.data.deliveryType === 'shipping'\n    ) {\n      needsShipping = true;\n    }\n\n    const priceId =\n      product.data.stripePriceIds?.[cart.currency.toUpperCase()];\n\n    if (priceId) {\n      lineItems.push({ price: priceId, quantity: line.quantity });\n    } else {\n      lineItems.push({\n        quantity: line.quantity,\n        price_data: {\n          currency: cart.currency.toLowerCase(),\n          unit_amount: line.unitPrice,\n          product_data: {\n            name: line.titleSnapshot,\n            ...(line.imageSnapshot ? { images: [line.imageSnapshot] } : {}),\n            metadata: {\n              cms_product_id: line.productId,\n              ...(line.variantId ? { cms_variant_id: line.variantId } : {}),\n            },\n          },\n        },\n      });\n    }\n  }\n\n  const collectShipping = opts.collectShippingAddress ?? needsShipping;\n  const description = opts.description ?? buildDefaultDescription(cart);\n  const kind = opts.kind ?? 'order';\n\n  const params: CheckoutSessionCreateParams = {\n    mode: 'payment',\n    line_items: lineItems,\n    success_url: opts.successUrl,\n    cancel_url: opts.cancelUrl,\n    automatic_tax: { enabled: false },\n    client_reference_id: opts.clientReferenceId ?? cart.id,\n    metadata: {\n      cms_cart_id: cart.id,\n      kind,\n      ...(cart.locale ? { cms_locale: cart.locale } : {}),\n      ...(opts.metadata ?? {}),\n    },\n  };\n\n  if (opts.alwaysCreateCustomer !== false) {\n    params.customer_creation = 'always';\n  }\n\n  if (cart.email) params.customer_email = cart.email;\n  if (opts.locale) params.locale = opts.locale;\n\n  // ── Connect / marketplace mode ──────────────────────────────────────\n  // Pattern from sanneandersen-site (verified on real Stripe transactions).\n  // application_fee_amount + transfer_data routes money to the merchant\n  // minus platform fee; on_behalf_of makes the charge appear \"owned\" by\n  // the merchant so they see customer details on their dashboard.\n  if (opts.connect) {\n    const fee = computeApplicationFee(cart, opts.connect);\n    params.payment_intent_data = {\n      application_fee_amount: fee,\n      transfer_data: { destination: opts.connect.destinationAccountId },\n      on_behalf_of: opts.connect.destinationAccountId,\n      description,\n      metadata: {\n        cms_cart_id: cart.id,\n        kind,\n        ...(opts.metadata ?? {}),\n      },\n      ...(opts.receiptEmail !== false && (opts.receiptEmail || cart.email)\n        ? { receipt_email: (opts.receiptEmail || cart.email) as string }\n        : {}),\n    };\n  } else {\n    // Single-merchant mode still benefits from description + receipt_email\n    // for dashboard quality.\n    params.payment_intent_data = {\n      description,\n      metadata: {\n        cms_cart_id: cart.id,\n        kind,\n        ...(opts.metadata ?? {}),\n      },\n      ...(opts.receiptEmail !== false && (opts.receiptEmail || cart.email)\n        ? { receipt_email: (opts.receiptEmail || cart.email) as string }\n        : {}),\n    };\n  }\n\n  if (collectShipping) {\n    params.shipping_address_collection = {\n      allowed_countries:\n        (opts.allowedShippingCountries as AllowedCountry[]) ??\n        (['DK', 'SE', 'NO', 'DE'] as AllowedCountry[]),\n    };\n    if (opts.shippingRateIds && opts.shippingRateIds.length > 0) {\n      params.shipping_options = opts.shippingRateIds.map((id) => ({\n        shipping_rate: id,\n      }));\n    }\n  }\n\n  const session = await stripe.checkout.sessions.create(params);\n  if (!session.url) {\n    throw new Error('[cms-shop] Stripe returned a session without a url');\n  }\n  return { sessionId: session.id, url: session.url };\n}\n\n/**\n * Compute the platform fee in minor units. Exported so consumers (and\n * tests) can reason about the math without going through Stripe.\n */\nexport function computeApplicationFee(\n  cart: ShopCart,\n  connect: ConnectOptions,\n): number {\n  if (typeof connect.applicationFeeAmount === 'number') {\n    return Math.max(0, Math.floor(connect.applicationFeeAmount));\n  }\n  if (typeof connect.applicationFeePercent === 'number') {\n    const pct = Math.max(0, Math.min(100, connect.applicationFeePercent));\n    return Math.round((cart.total * pct) / 100);\n  }\n  return 0;\n}\n","/**\n * F136 Phase 1 — Cart storage adapter.\n *\n * Default implementation: in-process Map keyed by cartId. That's enough\n * for filesystem-adapter sites running on a single Node process and for\n * the unit tests.\n *\n * Sites running on multiple instances (Fly machine pairs, Vercel\n * serverless) should pass a `CartStore` backed by their shared cache —\n * Redis, Upstash KV, Cloudflare KV. The interface is small on purpose.\n *\n * Carts have a 48 h TTL. The in-memory store sweeps expired entries on\n * each `get()` so it doesn't grow unbounded.\n */\nimport type { ShopCart } from '../types';\n\nexport interface CartStore {\n  get(id: string): Promise<ShopCart | null>;\n  set(cart: ShopCart): Promise<void>;\n  delete(id: string): Promise<void>;\n}\n\nconst TTL_MS = 48 * 60 * 60 * 1000;\n\nexport function createInMemoryCartStore(): CartStore {\n  const data = new Map<string, ShopCart>();\n\n  function reap() {\n    const now = Date.now();\n    for (const [id, cart] of data) {\n      if (Date.parse(cart.expiresAt) <= now) data.delete(id);\n    }\n  }\n\n  return {\n    async get(id: string): Promise<ShopCart | null> {\n      reap();\n      return data.get(id) ?? null;\n    },\n    async set(cart: ShopCart): Promise<void> {\n      data.set(cart.id, cart);\n    },\n    async delete(id: string): Promise<void> {\n      data.delete(id);\n    },\n  };\n}\n\nexport const CART_TTL_MS = TTL_MS;\n","/**\n * F136 Phase 1 — Framework-agnostic cart HTTP handlers.\n *\n * Returns plain `Request → Response` functions so consumers can wire\n * them into Next.js Route Handlers, Hono, Fastify, or Bun.serve with\n * one line each.\n *\n * Cart id transport: `cms_shop_cart` cookie. The handler reads it on\n * every request, generates one on first add, and re-issues a Set-Cookie\n * so the client persists it. SameSite=Lax is the default — anything\n * stricter blocks Stripe Checkout's redirect-back flow.\n */\nimport type { ShopProduct } from '../types';\nimport {\n  addItem,\n  clearCart,\n  getCart,\n  removeItem,\n  setEmail,\n  setShippingAddress,\n  updateItemQuantity,\n  type CartContext,\n} from './cart';\nimport type { CartStore } from './store';\nimport { CART_TTL_MS } from './store';\n\nconst COOKIE_NAME = 'cms_shop_cart';\n\nexport interface CartHandlerOptions {\n  store: CartStore;\n  /**\n   * Resolve a product by id from the site's CMS. The shop module is\n   * data-source agnostic so we don't import filesystem readers here —\n   * the host wires this in.\n   */\n  loadProduct(id: string): Promise<ShopProduct | null>;\n  /** Called whenever a cart mutates — useful for analytics/abandonment. */\n  onCartChanged?(cartId: string): void;\n  /** Cookie domain (default: omit, browser scopes to current host). */\n  cookieDomain?: string;\n  /** Set Secure flag on the cookie (default: true in prod, false in dev). */\n  cookieSecure?: boolean;\n}\n\nexport interface CartHandlers {\n  /** GET — returns the current cart (creates one if cookie missing). */\n  get(req: Request): Promise<Response>;\n  /** POST { productId, variantId?, quantity?, currency, locale? }. */\n  add(req: Request): Promise<Response>;\n  /** PATCH { productId, variantId?, quantity }. */\n  update(req: Request): Promise<Response>;\n  /** DELETE { productId, variantId? }. */\n  remove(req: Request): Promise<Response>;\n  /** POST { email } — captures email for abandonment + receipt. */\n  setEmail(req: Request): Promise<Response>;\n  /** POST { address } — for shipping rate calc + checkout. */\n  setAddress(req: Request): Promise<Response>;\n  /** DELETE — wipes the current cart. */\n  clear(req: Request): Promise<Response>;\n}\n\nfunction readCookie(req: Request, name: string): string | undefined {\n  const header = req.headers.get('cookie');\n  if (!header) return undefined;\n  for (const part of header.split(';')) {\n    const [k, v] = part.trim().split('=');\n    if (k === name) return v;\n  }\n  return undefined;\n}\n\nfunction buildSetCookie(\n  cartId: string,\n  opts: { domain?: string; secure: boolean },\n): string {\n  const maxAge = Math.floor(CART_TTL_MS / 1000);\n  const parts = [\n    `${COOKIE_NAME}=${cartId}`,\n    'Path=/',\n    'HttpOnly',\n    'SameSite=Lax',\n    `Max-Age=${maxAge}`,\n  ];\n  if (opts.domain) parts.push(`Domain=${opts.domain}`);\n  if (opts.secure) parts.push('Secure');\n  return parts.join('; ');\n}\n\nfunction json(\n  body: unknown,\n  init: ResponseInit & { setCartCookie?: string } = {},\n): Response {\n  const headers = new Headers(init.headers);\n  headers.set('content-type', 'application/json; charset=utf-8');\n  if (init.setCartCookie) headers.set('set-cookie', init.setCartCookie);\n  return new Response(JSON.stringify(body), {\n    status: init.status ?? 200,\n    headers,\n  });\n}\n\nfunction badRequest(message: string): Response {\n  return json({ error: message }, { status: 400 });\n}\n\nasync function readJson(req: Request): Promise<Record<string, unknown>> {\n  try {\n    const body = (await req.json()) as Record<string, unknown>;\n    return body && typeof body === 'object' ? body : {};\n  } catch {\n    return {};\n  }\n}\n\nexport function createCartHandlers(opts: CartHandlerOptions): CartHandlers {\n  const ctx: CartContext = { store: opts.store };\n  const secure =\n    opts.cookieSecure ?? process.env.NODE_ENV === 'production';\n  const cookieOpts = {\n    secure,\n    ...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}),\n  };\n\n  function setCookie(cartId: string): string {\n    return buildSetCookie(cartId, cookieOpts);\n  }\n\n  return {\n    async get(req: Request): Promise<Response> {\n      const cartId = readCookie(req, COOKIE_NAME);\n      if (!cartId) return json({ cart: null });\n      const cart = await getCart(ctx, cartId);\n      return json({ cart });\n    },\n\n    async add(req: Request): Promise<Response> {\n      const body = await readJson(req);\n      const productId = String(body.productId ?? '');\n      if (!productId) return badRequest('productId required');\n      const product = await opts.loadProduct(productId);\n      if (!product) return badRequest(`product ${productId} not found`);\n      const currency = String(body.currency ?? '').trim();\n      if (!currency) return badRequest('currency required');\n\n      const cartId = readCookie(req, COOKIE_NAME);\n      const cart = await addItem(ctx, cartId, {\n        product,\n        currency,\n        ...(body.variantId ? { variantId: String(body.variantId) } : {}),\n        ...(typeof body.quantity === 'number'\n          ? { quantity: body.quantity }\n          : {}),\n        ...(body.locale ? { locale: String(body.locale) } : {}),\n      });\n      opts.onCartChanged?.(cart.id);\n      return json({ cart }, { setCartCookie: setCookie(cart.id) });\n    },\n\n    async update(req: Request): Promise<Response> {\n      const cartId = readCookie(req, COOKIE_NAME);\n      if (!cartId) return badRequest('no cart');\n      const body = await readJson(req);\n      const productId = String(body.productId ?? '');\n      const quantity = Number(body.quantity);\n      if (!productId || !Number.isFinite(quantity)) {\n        return badRequest('productId + quantity required');\n      }\n      const cart = await updateItemQuantity(\n        ctx,\n        cartId,\n        productId,\n        body.variantId ? String(body.variantId) : undefined,\n        quantity,\n      );\n      opts.onCartChanged?.(cart.id);\n      return json({ cart });\n    },\n\n    async remove(req: Request): Promise<Response> {\n      const cartId = readCookie(req, COOKIE_NAME);\n      if (!cartId) return badRequest('no cart');\n      const body = await readJson(req);\n      const productId = String(body.productId ?? '');\n      if (!productId) return badRequest('productId required');\n      const cart = await removeItem(\n        ctx,\n        cartId,\n        productId,\n        body.variantId ? String(body.variantId) : undefined,\n      );\n      opts.onCartChanged?.(cart.id);\n      return json({ cart });\n    },\n\n    async setEmail(req: Request): Promise<Response> {\n      const cartId = readCookie(req, COOKIE_NAME);\n      if (!cartId) return badRequest('no cart');\n      const body = await readJson(req);\n      const email = String(body.email ?? '').trim();\n      if (!email || !email.includes('@')) return badRequest('email required');\n      const cart = await setEmail(ctx, cartId, email);\n      opts.onCartChanged?.(cart.id);\n      return json({ cart });\n    },\n\n    async setAddress(req: Request): Promise<Response> {\n      const cartId = readCookie(req, COOKIE_NAME);\n      if (!cartId) return badRequest('no cart');\n      const body = await readJson(req);\n      const address = body.address as\n        | Record<string, unknown>\n        | undefined;\n      if (!address || typeof address !== 'object') {\n        return badRequest('address required');\n      }\n      const required = ['name', 'line1', 'postalCode', 'city', 'country'];\n      for (const k of required) {\n        if (!address[k] || typeof address[k] !== 'string') {\n          return badRequest(`address.${k} required`);\n        }\n      }\n      const cart = await setShippingAddress(ctx, cartId, {\n        name: String(address.name),\n        line1: String(address.line1),\n        postalCode: String(address.postalCode),\n        city: String(address.city),\n        country: String(address.country),\n        ...(address.line2 ? { line2: String(address.line2) } : {}),\n        ...(address.region ? { region: String(address.region) } : {}),\n        ...(address.phone ? { phone: String(address.phone) } : {}),\n        ...(address.kind\n          ? { kind: address.kind as 'shipping' | 'billing' | 'both' }\n          : {}),\n      });\n      opts.onCartChanged?.(cart.id);\n      return json({ cart });\n    },\n\n    async clear(req: Request): Promise<Response> {\n      const cartId = readCookie(req, COOKIE_NAME);\n      if (cartId) await clearCart(ctx, cartId);\n      const expired = `${COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax${secure ? '; Secure' : ''}`;\n      return json({ cleared: true }, { setCartCookie: expired });\n    },\n  };\n}\n\nexport const CART_COOKIE_NAME = COOKIE_NAME;\n","/**\n * F136 Phase 1 — POST /api/shop/checkout HTTP handler.\n *\n * Reads the cart cookie, validates the cart is non-empty, creates a\n * Stripe Checkout Session, returns `{ url }` for the client to redirect\n * to. Returns 4xx with a clear message instead of a stack trace if the\n * cart is missing or empty (UX matters here).\n */\nimport { CART_COOKIE_NAME } from '../cart/handlers';\nimport type { CartStore } from '../cart/store';\nimport {\n  createCheckoutSession,\n  type CreateCheckoutOptions,\n} from './checkout';\n\nexport interface CheckoutHandlerOptions\n  extends Omit<CreateCheckoutOptions, 'successUrl' | 'cancelUrl'> {\n  store: CartStore;\n  /** Build success_url from cart id — typically `${siteOrigin}/checkout/success?session_id={CHECKOUT_SESSION_ID}`. */\n  successUrl: string | ((cartId: string) => string);\n  cancelUrl: string | ((cartId: string) => string);\n}\n\nfunction readCookie(req: Request, name: string): string | undefined {\n  const header = req.headers.get('cookie');\n  if (!header) return undefined;\n  for (const part of header.split(';')) {\n    const [k, v] = part.trim().split('=');\n    if (k === name) return v;\n  }\n  return undefined;\n}\n\nfunction json(body: unknown, status = 200): Response {\n  return new Response(JSON.stringify(body), {\n    status,\n    headers: { 'content-type': 'application/json; charset=utf-8' },\n  });\n}\n\nexport function createCheckoutHandler(opts: CheckoutHandlerOptions) {\n  return async function handle(req: Request): Promise<Response> {\n    const cartId = readCookie(req, CART_COOKIE_NAME);\n    if (!cartId) return json({ error: 'no cart' }, 400);\n    const cart = await opts.store.get(cartId);\n    if (!cart) return json({ error: 'cart expired' }, 404);\n    if (cart.items.length === 0) {\n      return json({ error: 'cart is empty' }, 400);\n    }\n\n    const successUrl =\n      typeof opts.successUrl === 'function'\n        ? opts.successUrl(cart.id)\n        : opts.successUrl;\n    const cancelUrl =\n      typeof opts.cancelUrl === 'function'\n        ? opts.cancelUrl(cart.id)\n        : opts.cancelUrl;\n\n    try {\n      const result = await createCheckoutSession(cart, {\n        ...opts,\n        successUrl,\n        cancelUrl,\n      });\n      return json(result);\n    } catch (err) {\n      const message = err instanceof Error ? err.message : 'checkout failed';\n      // eslint-disable-next-line no-console\n      console.error('[cms-shop] checkout failed', err);\n      return json({ error: message }, 500);\n    }\n  };\n}\n"],"mappings":"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","names":["Stripe"]}