@skuio/storefront 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skuio/storefront",
3
- "version": "0.6.1",
4
- "description": "The store platform behind SKU.io storefronts: catalogue, checkout, orders, buyer portal and the Store API over one SQL driver \u2014 Cloudflare D1 or SQLite on any Node host. Framework-agnostic; pairs with the Astro reference template.",
3
+ "version": "0.7.0",
4
+ "description": "The store platform behind SKU.io storefronts: catalog, checkout, orders, buyer portal and the Store API over one SQL driver — Cloudflare D1 or SQLite on any Node host. Framework-agnostic; pairs with the Astro reference template.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/catalog.ts","../src/shipping.ts","../src/discounts.ts","../src/slug.ts","../src/cart.ts","../src/address.ts"],"sourcesContent":["import type { Category, CategoryNode, Product } from './types'\n\n/**\n * An in-memory snapshot of the catalogue with the tree and the lookups the\n * pages need. Built from plain data — the store service builds one from the\n * database per request (cached briefly per isolate); tests build one from\n * fixtures.\n */\nexport class Catalog {\n readonly products: Product[]\n readonly categories: Category[]\n private readonly bySku = new Map<string, Product>()\n private readonly bySlug = new Map<string, Product>()\n private readonly categoryById = new Map<string, Category>()\n private readonly nodeById = new Map<string, CategoryNode>()\n private readonly roots: CategoryNode[]\n\n private constructor(products: Product[], categories: Category[]) {\n this.products = products\n this.categories = categories\n for (const p of products) {\n if (this.bySku.has(p.sku)) throw new Error(`Duplicate product sku: ${p.sku}`)\n this.bySku.set(p.sku, p)\n this.bySlug.set(p.slug, p)\n }\n for (const c of categories) this.categoryById.set(c.id, c)\n this.roots = this.buildTree()\n }\n\n static fromData(products: Product[], categories: Category[]): Catalog {\n return new Catalog(products, categories)\n }\n\n product(sku: string): Product | undefined {\n return this.bySku.get(sku)\n }\n\n productBySlug(slug: string): Product | undefined {\n return this.bySlug.get(slug)\n }\n\n /** Products directly in a category, or in it and every descendant with `deep`. */\n productsIn(categoryId: string, opts: { deep?: boolean } = {}): Product[] {\n const ids = new Set<string>([categoryId])\n if (opts.deep) {\n const node = this.nodeById.get(categoryId)\n if (node) this.collectIds(node, ids)\n }\n return this.products.filter((p) => p.categoryIds.some((id) => ids.has(id)))\n }\n\n tree(): CategoryNode[] {\n return this.roots\n }\n\n category(id: string): CategoryNode | undefined {\n return this.nodeById.get(id)\n }\n\n /** Find a node by its slug path from the root, e.g. ['toyota', 'tacoma']. */\n categoryByPath(path: string[]): CategoryNode | undefined {\n let level = this.roots\n let found: CategoryNode | undefined\n for (const slug of path) {\n found = level.find((n) => n.slug === slug)\n if (!found) return undefined\n level = found.children\n }\n return found\n }\n\n allCategories(): CategoryNode[] {\n return [...this.nodeById.values()]\n }\n\n /** The chain of categories from the root to the product's deepest category. */\n breadcrumbs(product: Product): CategoryNode[] {\n let best: CategoryNode | undefined\n for (const id of product.categoryIds) {\n const node = this.nodeById.get(id)\n if (node && (!best || node.depth > best.depth)) best = node\n }\n if (!best) return []\n const chain: CategoryNode[] = []\n let cur: CategoryNode | undefined = best\n while (cur) {\n chain.unshift(cur)\n cur = cur.parentId ? this.nodeById.get(cur.parentId) : undefined\n }\n return chain\n }\n\n /** Every node with at least one product in it or below it. */\n nonEmptyCategories(): CategoryNode[] {\n return this.allCategories().filter((n) => n.productCount > 0)\n }\n\n private collectIds(node: CategoryNode, into: Set<string>): void {\n for (const child of node.children) {\n into.add(child.id)\n this.collectIds(child, into)\n }\n }\n\n private buildTree(): CategoryNode[] {\n const nodes = new Map<string, CategoryNode>()\n for (const c of this.categories) {\n nodes.set(c.id, { ...c, children: [], path: [], depth: 0, productCount: 0 })\n }\n const roots: CategoryNode[] = []\n for (const node of nodes.values()) {\n const parent = node.parentId ? nodes.get(node.parentId) : undefined\n if (parent) parent.children.push(node)\n else roots.push(node)\n }\n const byName = (a: CategoryNode, b: CategoryNode) =>\n (a.position ?? 0) - (b.position ?? 0) || a.name.localeCompare(b.name)\n const walk = (node: CategoryNode, path: string[], depth: number): Set<string> => {\n node.path = [...path, node.slug]\n node.depth = depth\n node.children.sort(byName)\n const skus = new Set<string>()\n for (const p of this.products) if (p.categoryIds.includes(node.id)) skus.add(p.sku)\n for (const child of node.children) for (const sku of walk(child, node.path, depth + 1)) skus.add(sku)\n node.productCount = skus.size\n this.nodeById.set(node.id, node)\n return skus\n }\n roots.sort(byName)\n for (const root of roots) walk(root, [], 0)\n return roots\n }\n}\n","import type { Money } from './types.js'\n\nexport interface ShippingOption {\n id: string\n label: string\n price: Money\n /** Business days, inclusive range. */\n estimate?: { min: number; max: number }\n /**\n * Destinations this rate applies to (ISO 3166-1 alpha-2). Omit for \"every\n * country not claimed by another option\". Naming countries on at least one\n * option turns on region enforcement: a checkout is opened for one region at\n * a time, with only that region's rates and addresses. That is how a store\n * stops an international customer choosing the free domestic rate — Stripe\n * Checkout cannot filter rates by country on its own.\n */\n countries?: string[]\n}\n\nexport interface ShippingOptionExtras {\n estimate?: ShippingOption['estimate']\n countries?: string[]\n}\n\nexport function freeShipping(id: string, label: string, extras: ShippingOptionExtras = {}): ShippingOption {\n return { id, label, price: { amount: 0, currency: 'usd' }, ...extras }\n}\n\nexport function flatRate(id: string, label: string, price: Money, extras: ShippingOptionExtras = {}): ShippingOption {\n return { id, label, price, ...extras }\n}\n\nexport type ShippingRegion = 'domestic' | 'international'\n\nexport interface ResolvedShipping {\n options: ShippingOption[]\n countries: string[]\n /** False when no option names countries: one region, every rate, every country. */\n enforced: boolean\n}\n\n/**\n * The rates and destinations a checkout may offer for a region.\n *\n * domestic = options that name countries, shipped to those countries\n * international = options that name none, shipped to every remaining ship-to country\n */\nexport function shippingForRegion(\n shipping: ShippingOption[],\n shipToCountries: string[],\n region: ShippingRegion\n): ResolvedShipping {\n const scoped = shipping.filter((o) => o.countries && o.countries.length > 0)\n if (scoped.length === 0) return { options: shipping, countries: shipToCountries, enforced: false }\n\n const domestic = [...new Set(scoped.flatMap((o) => o.countries!))].filter((c) => shipToCountries.includes(c))\n if (region === 'domestic') return { options: scoped, countries: domestic, enforced: true }\n\n const rest = shipping.filter((o) => !o.countries || o.countries.length === 0)\n const international = shipToCountries.filter((c) => !domestic.includes(c))\n return { options: rest, countries: international, enforced: true }\n}\n\n/** Whether a store has anything to offer outside its domestic region. */\nexport function hasInternationalRegion(shipping: ShippingOption[], shipToCountries: string[]): boolean {\n const r = shippingForRegion(shipping, shipToCountries, 'international')\n return r.enforced && r.options.length > 0 && r.countries.length > 0\n}\n\n/** Which region a destination country falls in; null when the store does not ship there. */\nexport function regionForCountry(\n shipping: ShippingOption[],\n shipToCountries: string[],\n country: string\n): ShippingRegion | null {\n const code = country.toUpperCase()\n if (!shipToCountries.includes(code)) return null\n const domestic = shippingForRegion(shipping, shipToCountries, 'domestic')\n if (!domestic.enforced) return 'domestic'\n return domestic.countries.includes(code) ? 'domestic' : 'international'\n}\n\n/** The options a destination country may choose from. Empty when the store does not ship there. */\nexport function shippingForCountry(shipping: ShippingOption[], shipToCountries: string[], country: string): ShippingOption[] {\n const region = regionForCountry(shipping, shipToCountries, country)\n return region ? shippingForRegion(shipping, shipToCountries, region).options : []\n}\n","import { formatMoney } from './money'\n\n/**\n * Discount codes the store owns.\n *\n * A code lives in the store's configuration, is priced into the order by\n * `/api/checkout` like every other amount, and is recorded on the order —\n * so it works the same under any payment adapter, and a partner's sales are\n * read from the store's own orders rather than from a processor dashboard.\n *\n * A code may name a `partner` (an affiliate, a podcast, a reseller). The\n * order then carries that partner and the commission owed at the rate in\n * force when it was placed, so changing a rate later never rewrites history.\n */\nexport interface DiscountCode {\n /** What the buyer types. Matched case-insensitively, ignoring spaces. */\n code: string\n /** Percent off the merchandise subtotal, 1–100. Exactly one of percentOff / amountOff. */\n percentOff?: number\n /** Fixed amount off the merchandise subtotal, in minor units of the store currency. */\n amountOff?: number\n /** Shown to the buyer next to the code. Defaults to \"15% off\" / \"$10 off\". */\n label?: string\n /** Who the code attributes sales to. Orders placed with it are reported under this key. */\n partner?: string\n /** Commission owed to the partner, percent of the discounted merchandise subtotal (shipping and tax excluded). */\n commissionPercent?: number\n /** ISO timestamps; the code is valid from startsAt (inclusive) to endsAt (exclusive). */\n startsAt?: string\n endsAt?: string\n /** False switches the code off without deleting its history. Default true. */\n active?: boolean\n}\n\n/**\n * Quantity pricing: the more units in the order, the bigger the percentage off\n * the merchandise subtotal. Tiers count units across every line.\n *\n * It does not stack with a discount code: the buyer gets whichever saves\n * more, and a partner's code still attributes the order (and earns its\n * commission on what was actually charged) even when the tier wins.\n */\nexport interface QuantityDiscount {\n minQuantity: number\n percentOff: number\n /** Shown to the buyer, e.g. \"Two-miner pack\". Defaults to \"Buy N+, save X%\". */\n label?: string\n}\n\nexport function validateQuantityDiscounts(tiers: QuantityDiscount[]): void {\n const seen = new Set<number>()\n for (const t of tiers) {\n if (!(Number.isInteger(t.minQuantity) && t.minQuantity >= 2)) throw new Error(`quantity discount minQuantity must be an integer >= 2, got ${t.minQuantity}`)\n if (!(t.percentOff > 0 && t.percentOff < 100)) throw new Error(`quantity discount for ${t.minQuantity}+ needs percentOff in (0, 100)`)\n if (seen.has(t.minQuantity)) throw new Error(`quantity discount for ${t.minQuantity}+ is defined twice`)\n seen.add(t.minQuantity)\n }\n}\n\n/** The best tier the unit count reaches, or null. */\nexport function quantityTier(tiers: QuantityDiscount[] | undefined, units: number): QuantityDiscount | null {\n return (tiers ?? []).filter((t) => units >= t.minQuantity).sort((a, b) => b.percentOff - a.percentOff)[0] ?? null\n}\n\nexport function quantityLabel(t: QuantityDiscount): string {\n return t.label ?? `Buy ${t.minQuantity}+, save ${t.percentOff}%`\n}\n\n/**\n * Who a partner is, for the people they send and for their own portal.\n * `id` is what discount codes name in `partner`.\n */\nexport interface PartnerProfile {\n id: string\n /** The public name visitors know — a show, channel or brand: \"Welcome, friend of <name>\". */\n name: string\n /** The person behind it: a store may greet referred visitors as their friend, and the dashboard and sign-in email greet them by first name. Defaults to `name`. */\n contact?: string\n /** Where their portal sign-in link is sent. Without it they can only use a key link. */\n email?: string\n /** A line in their voice for the welcome banner, e.g. \"Stack sats, solo mine, have fun.\" */\n welcome?: string\n /** Absolute URL or site path of a square photo or logo for the welcome banner. */\n avatar?: string\n}\n\n/** What a buyer is shown about a code. Never includes the partner or the commission. */\nexport interface PublicDiscount {\n code: string\n label: string\n percentOff: number | null\n amountOff: number | null\n}\n\n/** How a code is carried from a partner's link to the checkout page. */\nexport interface ReferralSettings {\n /** Query parameter on any page that applies a code: /?code=PODCAST. */\n param: string\n /** How long the code stays applied in the buyer's browser. */\n cookieDays: number\n}\n\nexport const DISCOUNT_COOKIE = 'sf_discount'\n\n/** Canonical form of a code: upper case, no whitespace. */\nexport function normalizeDiscountCode(input: string): string {\n return input.replace(/\\s+/g, '').toUpperCase()\n}\n\n/** Throw on a configuration mistake; called from defineStorefront. */\nexport function validateDiscounts(codes: DiscountCode[], currency: string): void {\n const seen = new Set<string>()\n for (const d of codes) {\n const code = normalizeDiscountCode(d.code ?? '')\n if (!/^[A-Z0-9_-]{2,40}$/.test(code)) throw new Error(`discount code \"${d.code}\" must be 2–40 letters, digits, - or _`)\n if (seen.has(code)) throw new Error(`discount code ${code} is defined twice`)\n seen.add(code)\n const hasPercent = d.percentOff !== undefined\n const hasAmount = d.amountOff !== undefined\n if (hasPercent === hasAmount) throw new Error(`discount code ${code} needs exactly one of percentOff or amountOff`)\n if (hasPercent && !(d.percentOff! > 0 && d.percentOff! <= 100)) throw new Error(`discount code ${code}: percentOff must be in (0, 100]`)\n if (hasAmount && !(Number.isInteger(d.amountOff) && d.amountOff! > 0)) throw new Error(`discount code ${code}: amountOff must be a positive integer of minor units (${currency})`)\n if (d.commissionPercent !== undefined && !(d.commissionPercent >= 0 && d.commissionPercent <= 100)) throw new Error(`discount code ${code}: commissionPercent must be in [0, 100]`)\n if (d.commissionPercent && !d.partner) throw new Error(`discount code ${code}: commissionPercent needs a partner to owe it to`)\n for (const k of ['startsAt', 'endsAt'] as const) if (d[k] && Number.isNaN(Date.parse(d[k]!))) throw new Error(`discount code ${code}: ${k} is not a date`)\n }\n}\n\n/** The code a buyer typed, if it is valid right now. */\nexport function findDiscount(codes: DiscountCode[] | undefined, input: string | null | undefined, now: Date = new Date()): DiscountCode | null {\n if (!codes?.length || !input) return null\n const wanted = normalizeDiscountCode(input)\n const d = codes.find((c) => normalizeDiscountCode(c.code) === wanted)\n if (!d || d.active === false) return null\n if (d.startsAt && now.getTime() < Date.parse(d.startsAt)) return null\n if (d.endsAt && now.getTime() >= Date.parse(d.endsAt)) return null\n return d\n}\n\n/** Minor units off a merchandise subtotal. Percentages round half up; never more than the subtotal. */\nexport function discountAmount(d: DiscountCode, subtotal: number): number {\n const raw = d.percentOff !== undefined ? Math.round((subtotal * d.percentOff) / 100) : d.amountOff ?? 0\n return Math.max(0, Math.min(raw, subtotal))\n}\n\n/** Commission owed on an order, in minor units, at the code's rate. */\nexport function commissionAmount(d: DiscountCode, subtotal: number, discount: number): number {\n if (!d.partner || !d.commissionPercent) return 0\n return Math.round(((subtotal - discount) * d.commissionPercent) / 100)\n}\n\nexport function publicDiscount(d: DiscountCode, currency: string, locale = 'en-US'): PublicDiscount {\n const label =\n d.label ??\n (d.percentOff !== undefined\n ? `${d.percentOff}% off`\n : `${formatMoney({ amount: d.amountOff ?? 0, currency }, locale)} off`)\n return { code: normalizeDiscountCode(d.code), label, percentOff: d.percentOff ?? null, amountOff: d.amountOff ?? null }\n}\n","/** URL-safe slug: lower case ASCII letters, digits and single hyphens. */\nexport function slugify(input: string): string {\n return input\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 200) || 'item'\n}\n","import type { Money } from './types.js'\nimport { addMoney, multiplyMoney } from './money.js'\n\n/**\n * A cart is client-side state — lines the customer has chosen, with a snapshot\n * of what they saw (name, price) so the cart page can render without the\n * catalogue. It is never trusted for money: checkout re-prices every SKU on the\n * server. These are pure functions; the browser store wraps them.\n */\nexport interface CartLine {\n sku: string\n quantity: number\n name: string\n unitPrice: Money\n url?: string\n image?: string\n}\n\nexport interface Cart {\n lines: CartLine[]\n updatedAt: string\n}\n\nexport const MAX_CART_LINES = 50\n\nexport const emptyCart = (): Cart => ({ lines: [], updatedAt: new Date().toISOString() })\n\nfunction touch(lines: CartLine[]): Cart {\n return { lines, updatedAt: new Date().toISOString() }\n}\n\nexport function addLine(cart: Cart, line: Omit<CartLine, 'quantity'>, quantity: number, maxQuantity: number): Cart {\n const qty = clampQty(quantity, maxQuantity)\n const existing = cart.lines.find((l) => l.sku === line.sku)\n if (existing) {\n return touch(\n cart.lines.map((l) => (l.sku === line.sku ? { ...l, ...line, quantity: clampQty(l.quantity + qty, maxQuantity) } : l))\n )\n }\n if (cart.lines.length >= MAX_CART_LINES) throw new Error(`Cart is limited to ${MAX_CART_LINES} lines`)\n return touch([...cart.lines, { ...line, quantity: qty }])\n}\n\nexport function setQuantity(cart: Cart, sku: string, quantity: number, maxQuantity: number): Cart {\n if (quantity <= 0) return removeLine(cart, sku)\n return touch(cart.lines.map((l) => (l.sku === sku ? { ...l, quantity: clampQty(quantity, maxQuantity) } : l)))\n}\n\nexport function removeLine(cart: Cart, sku: string): Cart {\n return touch(cart.lines.filter((l) => l.sku !== sku))\n}\n\nexport function cartCount(cart: Cart): number {\n return cart.lines.reduce((n, l) => n + l.quantity, 0)\n}\n\n/** Snapshot subtotal for display only. The server computes the real one. */\nexport function cartSubtotal(cart: Cart, currency: string): Money {\n return cart.lines.reduce((sum, l) => addMoney(sum, multiplyMoney(l.unitPrice, l.quantity)), { amount: 0, currency })\n}\n\n/** What the checkout endpoint needs — nothing but SKUs and quantities. */\nexport function cartToCheckoutItems(cart: Cart): { sku: string; quantity: number }[] {\n return cart.lines.map((l) => ({ sku: l.sku, quantity: l.quantity }))\n}\n\n/**\n * Parse stored JSON defensively: anything malformed yields an empty cart\n * rather than a broken page. Money amounts must be integers.\n */\nexport function parseCart(raw: string | null | undefined): Cart {\n if (!raw) return emptyCart()\n try {\n const data = JSON.parse(raw) as Partial<Cart>\n if (!Array.isArray(data.lines)) return emptyCart()\n const lines: CartLine[] = []\n for (const l of data.lines as Partial<CartLine>[]) {\n if (\n typeof l?.sku !== 'string' || !l.sku ||\n typeof l.name !== 'string' ||\n !Number.isInteger(l.quantity) || (l.quantity as number) < 1 ||\n !l.unitPrice || !Number.isInteger(l.unitPrice.amount) || typeof l.unitPrice.currency !== 'string'\n ) continue\n lines.push({\n sku: l.sku, name: l.name, quantity: l.quantity as number,\n unitPrice: { amount: l.unitPrice.amount, currency: l.unitPrice.currency },\n ...(typeof l.url === 'string' ? { url: l.url } : {}),\n ...(typeof l.image === 'string' ? { image: l.image } : {}),\n })\n }\n return { lines: lines.slice(0, MAX_CART_LINES), updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : new Date().toISOString() }\n } catch {\n return emptyCart()\n }\n}\n\nexport function serializeCart(cart: Cart): string {\n return JSON.stringify(cart)\n}\n\nfunction clampQty(n: number, max: number): number {\n return Number.isFinite(n) ? Math.min(Math.max(Math.trunc(n), 1), max) : 1\n}\n","/**\n * How an address is asked for in a given country: what the region and postal\n * fields are called, whether they are required, and — where carriers expect a\n * code — the list of regions to choose from. Countries not listed get a\n * neutral, optional \"State / province / region\" and \"Postal code\".\n */\nexport interface AddressFormat {\n regionLabel: string\n /** Choose-from list as [code, name]; the code is what is stored and sent to carriers. */\n regions?: [string, string][]\n regionRequired: boolean\n postalLabel: string\n postalRequired: boolean\n /** Checked on the server and as the input's pattern. */\n postalPattern?: string\n postalExample?: string\n}\n\nconst US_STATES: [string, string][] = [\n ['AL', 'Alabama'], ['AK', 'Alaska'], ['AZ', 'Arizona'], ['AR', 'Arkansas'], ['CA', 'California'], ['CO', 'Colorado'],\n ['CT', 'Connecticut'], ['DE', 'Delaware'], ['DC', 'District of Columbia'], ['FL', 'Florida'], ['GA', 'Georgia'], ['HI', 'Hawaii'],\n ['ID', 'Idaho'], ['IL', 'Illinois'], ['IN', 'Indiana'], ['IA', 'Iowa'], ['KS', 'Kansas'], ['KY', 'Kentucky'], ['LA', 'Louisiana'],\n ['ME', 'Maine'], ['MD', 'Maryland'], ['MA', 'Massachusetts'], ['MI', 'Michigan'], ['MN', 'Minnesota'], ['MS', 'Mississippi'],\n ['MO', 'Missouri'], ['MT', 'Montana'], ['NE', 'Nebraska'], ['NV', 'Nevada'], ['NH', 'New Hampshire'], ['NJ', 'New Jersey'],\n ['NM', 'New Mexico'], ['NY', 'New York'], ['NC', 'North Carolina'], ['ND', 'North Dakota'], ['OH', 'Ohio'], ['OK', 'Oklahoma'],\n ['OR', 'Oregon'], ['PA', 'Pennsylvania'], ['RI', 'Rhode Island'], ['SC', 'South Carolina'], ['SD', 'South Dakota'],\n ['TN', 'Tennessee'], ['TX', 'Texas'], ['UT', 'Utah'], ['VT', 'Vermont'], ['VA', 'Virginia'], ['WA', 'Washington'],\n ['WV', 'West Virginia'], ['WI', 'Wisconsin'], ['WY', 'Wyoming'],\n ['PR', 'Puerto Rico'], ['GU', 'Guam'], ['VI', 'U.S. Virgin Islands'], ['AS', 'American Samoa'], ['MP', 'Northern Mariana Islands'],\n ['AA', 'Armed Forces Americas'], ['AE', 'Armed Forces Europe'], ['AP', 'Armed Forces Pacific'],\n]\nconst CA_PROVINCES: [string, string][] = [\n ['AB', 'Alberta'], ['BC', 'British Columbia'], ['MB', 'Manitoba'], ['NB', 'New Brunswick'], ['NL', 'Newfoundland and Labrador'],\n ['NS', 'Nova Scotia'], ['NT', 'Northwest Territories'], ['NU', 'Nunavut'], ['ON', 'Ontario'], ['PE', 'Prince Edward Island'],\n ['QC', 'Quebec'], ['SK', 'Saskatchewan'], ['YT', 'Yukon'],\n]\nconst AU_STATES: [string, string][] = [\n ['ACT', 'Australian Capital Territory'], ['NSW', 'New South Wales'], ['NT', 'Northern Territory'], ['QLD', 'Queensland'],\n ['SA', 'South Australia'], ['TAS', 'Tasmania'], ['VIC', 'Victoria'], ['WA', 'Western Australia'],\n]\n\nconst FORMATS: Record<string, AddressFormat> = {\n US: { regionLabel: 'State', regions: US_STATES, regionRequired: true, postalLabel: 'ZIP code', postalRequired: true, postalPattern: '^\\\\d{5}(-\\\\d{4})?$', postalExample: '89101' },\n CA: { regionLabel: 'Province', regions: CA_PROVINCES, regionRequired: true, postalLabel: 'Postal code', postalRequired: true, postalPattern: '^[A-Za-z]\\\\d[A-Za-z] ?\\\\d[A-Za-z]\\\\d$', postalExample: 'K1A 0B1' },\n AU: { regionLabel: 'State / territory', regions: AU_STATES, regionRequired: true, postalLabel: 'Postcode', postalRequired: true, postalPattern: '^\\\\d{4}$', postalExample: '2000' },\n GB: { regionLabel: 'County (optional)', regionRequired: false, postalLabel: 'Postcode', postalRequired: true },\n IE: { regionLabel: 'County', regionRequired: false, postalLabel: 'Eircode (optional)', postalRequired: false },\n NZ: { regionLabel: 'Region (optional)', regionRequired: false, postalLabel: 'Postcode', postalRequired: true, postalPattern: '^\\\\d{4}$' },\n MX: { regionLabel: 'State', regionRequired: true, postalLabel: 'Postal code', postalRequired: true, postalPattern: '^\\\\d{5}$' },\n BR: { regionLabel: 'State', regionRequired: true, postalLabel: 'CEP', postalRequired: true, postalPattern: '^\\\\d{5}-?\\\\d{3}$' },\n JP: { regionLabel: 'Prefecture', regionRequired: true, postalLabel: 'Postal code', postalRequired: true, postalPattern: '^\\\\d{3}-?\\\\d{4}$' },\n DE: { regionLabel: 'State (optional)', regionRequired: false, postalLabel: 'Postcode', postalRequired: true, postalPattern: '^\\\\d{5}$' },\n}\nconst DEFAULT_FORMAT: AddressFormat = { regionLabel: 'State / province / region', regionRequired: false, postalLabel: 'Postal code', postalRequired: false }\n\nexport function addressFormat(country: string | null | undefined): AddressFormat {\n return FORMATS[(country ?? '').toUpperCase()] ?? DEFAULT_FORMAT\n}\n\n/** Formats for a set of countries, for the checkout page to switch between without a round trip. */\nexport function addressFormats(countries: string[]): Record<string, AddressFormat> {\n return Object.fromEntries(countries.map((c) => [c.toUpperCase(), addressFormat(c)]))\n}\n\n/**\n * Field errors for an address against its country's format; also normalises\n * a region given by name (\"Nevada\") to its code (\"NV\"). Mutates `address`.\n */\nexport function checkAddressFormat(address: { country?: string; state?: string; postalCode?: string }): Record<string, string> {\n const f = addressFormat(address.country)\n const details: Record<string, string> = {}\n const state = address.state?.trim() ?? ''\n if (f.regions && state) {\n const hit = f.regions.find(([code, name]) => code.toLowerCase() === state.toLowerCase() || name.toLowerCase() === state.toLowerCase())\n if (hit) address.state = hit[0]\n else details['shipping_address.state'] = `is not a ${f.regionLabel.toLowerCase()}`\n } else if (f.regionRequired && !state) details['shipping_address.state'] = 'is required'\n const postal = address.postalCode?.trim() ?? ''\n if (!postal && f.postalRequired) details['shipping_address.postal_code'] = 'is required'\n else if (postal && f.postalPattern && !new RegExp(f.postalPattern).test(postal)) details['shipping_address.postal_code'] = `doesn't look like a ${f.postalLabel}${f.postalExample ? ` (e.g. ${f.postalExample})` : ''}`\n return details\n}\n"],"mappings":";;;;;;;AAQO,IAAM,UAAN,MAAM,SAAQ;AAAA,EACV;AAAA,EACA;AAAA,EACQ,QAAQ,oBAAI,IAAqB;AAAA,EACjC,SAAS,oBAAI,IAAqB;AAAA,EAClC,eAAe,oBAAI,IAAsB;AAAA,EACzC,WAAW,oBAAI,IAA0B;AAAA,EACzC;AAAA,EAET,YAAY,UAAqB,YAAwB;AAC/D,SAAK,WAAW;AAChB,SAAK,aAAa;AAClB,eAAW,KAAK,UAAU;AACxB,UAAI,KAAK,MAAM,IAAI,EAAE,GAAG,EAAG,OAAM,IAAI,MAAM,0BAA0B,EAAE,GAAG,EAAE;AAC5E,WAAK,MAAM,IAAI,EAAE,KAAK,CAAC;AACvB,WAAK,OAAO,IAAI,EAAE,MAAM,CAAC;AAAA,IAC3B;AACA,eAAW,KAAK,WAAY,MAAK,aAAa,IAAI,EAAE,IAAI,CAAC;AACzD,SAAK,QAAQ,KAAK,UAAU;AAAA,EAC9B;AAAA,EAEA,OAAO,SAAS,UAAqB,YAAiC;AACpE,WAAO,IAAI,SAAQ,UAAU,UAAU;AAAA,EACzC;AAAA,EAEA,QAAQ,KAAkC;AACxC,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,cAAc,MAAmC;AAC/C,WAAO,KAAK,OAAO,IAAI,IAAI;AAAA,EAC7B;AAAA;AAAA,EAGA,WAAW,YAAoB,OAA2B,CAAC,GAAc;AACvE,UAAM,MAAM,oBAAI,IAAY,CAAC,UAAU,CAAC;AACxC,QAAI,KAAK,MAAM;AACb,YAAM,OAAO,KAAK,SAAS,IAAI,UAAU;AACzC,UAAI,KAAM,MAAK,WAAW,MAAM,GAAG;AAAA,IACrC;AACA,WAAO,KAAK,SAAS,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC;AAAA,EAC5E;AAAA,EAEA,OAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS,IAAsC;AAC7C,WAAO,KAAK,SAAS,IAAI,EAAE;AAAA,EAC7B;AAAA;AAAA,EAGA,eAAe,MAA0C;AACvD,QAAI,QAAQ,KAAK;AACjB,QAAI;AACJ,eAAW,QAAQ,MAAM;AACvB,cAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AACzC,UAAI,CAAC,MAAO,QAAO;AACnB,cAAQ,MAAM;AAAA,IAChB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,gBAAgC;AAC9B,WAAO,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC;AAAA,EACnC;AAAA;AAAA,EAGA,YAAY,SAAkC;AAC5C,QAAI;AACJ,eAAW,MAAM,QAAQ,aAAa;AACpC,YAAM,OAAO,KAAK,SAAS,IAAI,EAAE;AACjC,UAAI,SAAS,CAAC,QAAQ,KAAK,QAAQ,KAAK,OAAQ,QAAO;AAAA,IACzD;AACA,QAAI,CAAC,KAAM,QAAO,CAAC;AACnB,UAAM,QAAwB,CAAC;AAC/B,QAAI,MAAgC;AACpC,WAAO,KAAK;AACV,YAAM,QAAQ,GAAG;AACjB,YAAM,IAAI,WAAW,KAAK,SAAS,IAAI,IAAI,QAAQ,IAAI;AAAA,IACzD;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,qBAAqC;AACnC,WAAO,KAAK,cAAc,EAAE,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC;AAAA,EAC9D;AAAA,EAEQ,WAAW,MAAoB,MAAyB;AAC9D,eAAW,SAAS,KAAK,UAAU;AACjC,WAAK,IAAI,MAAM,EAAE;AACjB,WAAK,WAAW,OAAO,IAAI;AAAA,IAC7B;AAAA,EACF;AAAA,EAEQ,YAA4B;AAClC,UAAM,QAAQ,oBAAI,IAA0B;AAC5C,eAAW,KAAK,KAAK,YAAY;AAC/B,YAAM,IAAI,EAAE,IAAI,EAAE,GAAG,GAAG,UAAU,CAAC,GAAG,MAAM,CAAC,GAAG,OAAO,GAAG,cAAc,EAAE,CAAC;AAAA,IAC7E;AACA,UAAM,QAAwB,CAAC;AAC/B,eAAW,QAAQ,MAAM,OAAO,GAAG;AACjC,YAAM,SAAS,KAAK,WAAW,MAAM,IAAI,KAAK,QAAQ,IAAI;AAC1D,UAAI,OAAQ,QAAO,SAAS,KAAK,IAAI;AAAA,UAChC,OAAM,KAAK,IAAI;AAAA,IACtB;AACA,UAAM,SAAS,CAAC,GAAiB,OAC9B,EAAE,YAAY,MAAM,EAAE,YAAY,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI;AACtE,UAAM,OAAO,CAAC,MAAoB,MAAgB,UAA+B;AAC/E,WAAK,OAAO,CAAC,GAAG,MAAM,KAAK,IAAI;AAC/B,WAAK,QAAQ;AACb,WAAK,SAAS,KAAK,MAAM;AACzB,YAAM,OAAO,oBAAI,IAAY;AAC7B,iBAAW,KAAK,KAAK,SAAU,KAAI,EAAE,YAAY,SAAS,KAAK,EAAE,EAAG,MAAK,IAAI,EAAE,GAAG;AAClF,iBAAW,SAAS,KAAK,SAAU,YAAW,OAAO,KAAK,OAAO,KAAK,MAAM,QAAQ,CAAC,EAAG,MAAK,IAAI,GAAG;AACpG,WAAK,eAAe,KAAK;AACzB,WAAK,SAAS,IAAI,KAAK,IAAI,IAAI;AAC/B,aAAO;AAAA,IACT;AACA,UAAM,KAAK,MAAM;AACjB,eAAW,QAAQ,MAAO,MAAK,MAAM,CAAC,GAAG,CAAC;AAC1C,WAAO;AAAA,EACT;AACF;;;AC5GO,SAAS,aAAa,IAAY,OAAe,SAA+B,CAAC,GAAmB;AACzG,SAAO,EAAE,IAAI,OAAO,OAAO,EAAE,QAAQ,GAAG,UAAU,MAAM,GAAG,GAAG,OAAO;AACvE;AAEO,SAAS,SAAS,IAAY,OAAe,OAAc,SAA+B,CAAC,GAAmB;AACnH,SAAO,EAAE,IAAI,OAAO,OAAO,GAAG,OAAO;AACvC;AAiBO,SAAS,kBACd,UACA,iBACA,QACkB;AAClB,QAAM,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,SAAS,CAAC;AAC3E,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,SAAS,UAAU,WAAW,iBAAiB,UAAU,MAAM;AAEjG,QAAM,WAAW,CAAC,GAAG,IAAI,IAAI,OAAO,QAAQ,CAAC,MAAM,EAAE,SAAU,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,gBAAgB,SAAS,CAAC,CAAC;AAC5G,MAAI,WAAW,WAAY,QAAO,EAAE,SAAS,QAAQ,WAAW,UAAU,UAAU,KAAK;AAEzF,QAAM,OAAO,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,aAAa,EAAE,UAAU,WAAW,CAAC;AAC5E,QAAM,gBAAgB,gBAAgB,OAAO,CAAC,MAAM,CAAC,SAAS,SAAS,CAAC,CAAC;AACzE,SAAO,EAAE,SAAS,MAAM,WAAW,eAAe,UAAU,KAAK;AACnE;AAGO,SAAS,uBAAuB,UAA4B,iBAAoC;AACrG,QAAM,IAAI,kBAAkB,UAAU,iBAAiB,eAAe;AACtE,SAAO,EAAE,YAAY,EAAE,QAAQ,SAAS,KAAK,EAAE,UAAU,SAAS;AACpE;AAGO,SAAS,iBACd,UACA,iBACA,SACuB;AACvB,QAAM,OAAO,QAAQ,YAAY;AACjC,MAAI,CAAC,gBAAgB,SAAS,IAAI,EAAG,QAAO;AAC5C,QAAM,WAAW,kBAAkB,UAAU,iBAAiB,UAAU;AACxE,MAAI,CAAC,SAAS,SAAU,QAAO;AAC/B,SAAO,SAAS,UAAU,SAAS,IAAI,IAAI,aAAa;AAC1D;AAGO,SAAS,mBAAmB,UAA4B,iBAA2B,SAAmC;AAC3H,QAAM,SAAS,iBAAiB,UAAU,iBAAiB,OAAO;AAClE,SAAO,SAAS,kBAAkB,UAAU,iBAAiB,MAAM,EAAE,UAAU,CAAC;AAClF;;;ACrCO,SAAS,0BAA0B,OAAiC;AACzE,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,OAAO,UAAU,EAAE,WAAW,KAAK,EAAE,eAAe,GAAI,OAAM,IAAI,MAAM,8DAA8D,EAAE,WAAW,EAAE;AAC3J,QAAI,EAAE,EAAE,aAAa,KAAK,EAAE,aAAa,KAAM,OAAM,IAAI,MAAM,yBAAyB,EAAE,WAAW,gCAAgC;AACrI,QAAI,KAAK,IAAI,EAAE,WAAW,EAAG,OAAM,IAAI,MAAM,yBAAyB,EAAE,WAAW,oBAAoB;AACvG,SAAK,IAAI,EAAE,WAAW;AAAA,EACxB;AACF;AAGO,SAAS,aAAa,OAAuC,OAAwC;AAC1G,UAAQ,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,SAAS,EAAE,WAAW,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC,KAAK;AAC/G;AAEO,SAAS,cAAc,GAA6B;AACzD,SAAO,EAAE,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,UAAU;AAC/D;AAoCO,IAAM,kBAAkB;AAGxB,SAAS,sBAAsB,OAAuB;AAC3D,SAAO,MAAM,QAAQ,QAAQ,EAAE,EAAE,YAAY;AAC/C;AAGO,SAAS,kBAAkB,OAAuB,UAAwB;AAC/E,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,OAAO;AACrB,UAAM,OAAO,sBAAsB,EAAE,QAAQ,EAAE;AAC/C,QAAI,CAAC,qBAAqB,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,kBAAkB,EAAE,IAAI,6CAAwC;AACtH,QAAI,KAAK,IAAI,IAAI,EAAG,OAAM,IAAI,MAAM,iBAAiB,IAAI,mBAAmB;AAC5E,SAAK,IAAI,IAAI;AACb,UAAM,aAAa,EAAE,eAAe;AACpC,UAAM,YAAY,EAAE,cAAc;AAClC,QAAI,eAAe,UAAW,OAAM,IAAI,MAAM,iBAAiB,IAAI,+CAA+C;AAClH,QAAI,cAAc,EAAE,EAAE,aAAc,KAAK,EAAE,cAAe,KAAM,OAAM,IAAI,MAAM,iBAAiB,IAAI,kCAAkC;AACvI,QAAI,aAAa,EAAE,OAAO,UAAU,EAAE,SAAS,KAAK,EAAE,YAAa,GAAI,OAAM,IAAI,MAAM,iBAAiB,IAAI,0DAA0D,QAAQ,GAAG;AACjL,QAAI,EAAE,sBAAsB,UAAa,EAAE,EAAE,qBAAqB,KAAK,EAAE,qBAAqB,KAAM,OAAM,IAAI,MAAM,iBAAiB,IAAI,yCAAyC;AAClL,QAAI,EAAE,qBAAqB,CAAC,EAAE,QAAS,OAAM,IAAI,MAAM,iBAAiB,IAAI,kDAAkD;AAC9H,eAAW,KAAK,CAAC,YAAY,QAAQ,EAAY,KAAI,EAAE,CAAC,KAAK,OAAO,MAAM,KAAK,MAAM,EAAE,CAAC,CAAE,CAAC,EAAG,OAAM,IAAI,MAAM,iBAAiB,IAAI,KAAK,CAAC,gBAAgB;AAAA,EAC3J;AACF;AAGO,SAAS,aAAa,OAAmC,OAAkC,MAAY,oBAAI,KAAK,GAAwB;AAC7I,MAAI,CAAC,OAAO,UAAU,CAAC,MAAO,QAAO;AACrC,QAAM,SAAS,sBAAsB,KAAK;AAC1C,QAAM,IAAI,MAAM,KAAK,CAAC,MAAM,sBAAsB,EAAE,IAAI,MAAM,MAAM;AACpE,MAAI,CAAC,KAAK,EAAE,WAAW,MAAO,QAAO;AACrC,MAAI,EAAE,YAAY,IAAI,QAAQ,IAAI,KAAK,MAAM,EAAE,QAAQ,EAAG,QAAO;AACjE,MAAI,EAAE,UAAU,IAAI,QAAQ,KAAK,KAAK,MAAM,EAAE,MAAM,EAAG,QAAO;AAC9D,SAAO;AACT;AAGO,SAAS,eAAe,GAAiB,UAA0B;AACxE,QAAM,MAAM,EAAE,eAAe,SAAY,KAAK,MAAO,WAAW,EAAE,aAAc,GAAG,IAAI,EAAE,aAAa;AACtG,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC5C;AAGO,SAAS,iBAAiB,GAAiB,UAAkB,UAA0B;AAC5F,MAAI,CAAC,EAAE,WAAW,CAAC,EAAE,kBAAmB,QAAO;AAC/C,SAAO,KAAK,OAAQ,WAAW,YAAY,EAAE,oBAAqB,GAAG;AACvE;AAEO,SAAS,eAAe,GAAiB,UAAkB,SAAS,SAAyB;AAClG,QAAM,QACJ,EAAE,UACD,EAAE,eAAe,SACd,GAAG,EAAE,UAAU,UACf,GAAG,YAAY,EAAE,QAAQ,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,CAAC;AACpE,SAAO,EAAE,MAAM,sBAAsB,EAAE,IAAI,GAAG,OAAO,YAAY,EAAE,cAAc,MAAM,WAAW,EAAE,aAAa,KAAK;AACxH;;;AC7JO,SAAS,QAAQ,OAAuB;AAC7C,SAAO,MACJ,UAAU,MAAM,EAChB,QAAQ,UAAU,EAAE,EACpB,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,GAAG,KAAK;AACtB;;;ACcO,IAAM,iBAAiB;AAEvB,IAAM,YAAY,OAAa,EAAE,OAAO,CAAC,GAAG,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE;AAEvF,SAAS,MAAM,OAAyB;AACtC,SAAO,EAAE,OAAO,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE;AACtD;AAEO,SAAS,QAAQ,MAAY,MAAkC,UAAkB,aAA2B;AACjH,QAAM,MAAM,SAAS,UAAU,WAAW;AAC1C,QAAM,WAAW,KAAK,MAAM,KAAK,CAAC,MAAM,EAAE,QAAQ,KAAK,GAAG;AAC1D,MAAI,UAAU;AACZ,WAAO;AAAA,MACL,KAAK,MAAM,IAAI,CAAC,MAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,GAAG,GAAG,GAAG,MAAM,UAAU,SAAS,EAAE,WAAW,KAAK,WAAW,EAAE,IAAI,CAAE;AAAA,IACvH;AAAA,EACF;AACA,MAAI,KAAK,MAAM,UAAU,eAAgB,OAAM,IAAI,MAAM,sBAAsB,cAAc,QAAQ;AACrG,SAAO,MAAM,CAAC,GAAG,KAAK,OAAO,EAAE,GAAG,MAAM,UAAU,IAAI,CAAC,CAAC;AAC1D;AAEO,SAAS,YAAY,MAAY,KAAa,UAAkB,aAA2B;AAChG,MAAI,YAAY,EAAG,QAAO,WAAW,MAAM,GAAG;AAC9C,SAAO,MAAM,KAAK,MAAM,IAAI,CAAC,MAAO,EAAE,QAAQ,MAAM,EAAE,GAAG,GAAG,UAAU,SAAS,UAAU,WAAW,EAAE,IAAI,CAAE,CAAC;AAC/G;AAEO,SAAS,WAAW,MAAY,KAAmB;AACxD,SAAO,MAAM,KAAK,MAAM,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,CAAC;AACtD;AAEO,SAAS,UAAU,MAAoB;AAC5C,SAAO,KAAK,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,UAAU,CAAC;AACtD;AAGO,SAAS,aAAa,MAAY,UAAyB;AAChE,SAAO,KAAK,MAAM,OAAO,CAAC,KAAK,MAAM,SAAS,KAAK,cAAc,EAAE,WAAW,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,CAAC;AACrH;AAGO,SAAS,oBAAoB,MAAiD;AACnF,SAAO,KAAK,MAAM,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,SAAS,EAAE;AACrE;AAMO,SAAS,UAAU,KAAsC;AAC9D,MAAI,CAAC,IAAK,QAAO,UAAU;AAC3B,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,QAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAG,QAAO,UAAU;AACjD,UAAM,QAAoB,CAAC;AAC3B,eAAW,KAAK,KAAK,OAA8B;AACjD,UACE,OAAO,GAAG,QAAQ,YAAY,CAAC,EAAE,OACjC,OAAO,EAAE,SAAS,YAClB,CAAC,OAAO,UAAU,EAAE,QAAQ,KAAM,EAAE,WAAsB,KAC1D,CAAC,EAAE,aAAa,CAAC,OAAO,UAAU,EAAE,UAAU,MAAM,KAAK,OAAO,EAAE,UAAU,aAAa,SACzF;AACF,YAAM,KAAK;AAAA,QACT,KAAK,EAAE;AAAA,QAAK,MAAM,EAAE;AAAA,QAAM,UAAU,EAAE;AAAA,QACtC,WAAW,EAAE,QAAQ,EAAE,UAAU,QAAQ,UAAU,EAAE,UAAU,SAAS;AAAA,QACxE,GAAI,OAAO,EAAE,QAAQ,WAAW,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,QAClD,GAAI,OAAO,EAAE,UAAU,WAAW,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,MAC1D,CAAC;AAAA,IACH;AACA,WAAO,EAAE,OAAO,MAAM,MAAM,GAAG,cAAc,GAAG,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE;AAAA,EAC5I,QAAQ;AACN,WAAO,UAAU;AAAA,EACnB;AACF;AAEO,SAAS,cAAc,MAAoB;AAChD,SAAO,KAAK,UAAU,IAAI;AAC5B;AAEA,SAAS,SAAS,GAAW,KAAqB;AAChD,SAAO,OAAO,SAAS,CAAC,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI;AAC1E;;;ACpFA,IAAM,YAAgC;AAAA,EACpC,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,QAAQ;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,YAAY;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EACnH,CAAC,MAAM,aAAa;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,sBAAsB;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,QAAQ;AAAA,EAChI,CAAC,MAAM,OAAO;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,MAAM;AAAA,EAAG,CAAC,MAAM,QAAQ;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,WAAW;AAAA,EAChI,CAAC,MAAM,OAAO;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,eAAe;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,WAAW;AAAA,EAAG,CAAC,MAAM,aAAa;AAAA,EAC3H,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,QAAQ;AAAA,EAAG,CAAC,MAAM,eAAe;AAAA,EAAG,CAAC,MAAM,YAAY;AAAA,EACzH,CAAC,MAAM,YAAY;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,gBAAgB;AAAA,EAAG,CAAC,MAAM,cAAc;AAAA,EAAG,CAAC,MAAM,MAAM;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAC7H,CAAC,MAAM,QAAQ;AAAA,EAAG,CAAC,MAAM,cAAc;AAAA,EAAG,CAAC,MAAM,cAAc;AAAA,EAAG,CAAC,MAAM,gBAAgB;AAAA,EAAG,CAAC,MAAM,cAAc;AAAA,EACjH,CAAC,MAAM,WAAW;AAAA,EAAG,CAAC,MAAM,OAAO;AAAA,EAAG,CAAC,MAAM,MAAM;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,YAAY;AAAA,EAChH,CAAC,MAAM,eAAe;AAAA,EAAG,CAAC,MAAM,WAAW;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAC9D,CAAC,MAAM,aAAa;AAAA,EAAG,CAAC,MAAM,MAAM;AAAA,EAAG,CAAC,MAAM,qBAAqB;AAAA,EAAG,CAAC,MAAM,gBAAgB;AAAA,EAAG,CAAC,MAAM,0BAA0B;AAAA,EACjI,CAAC,MAAM,uBAAuB;AAAA,EAAG,CAAC,MAAM,qBAAqB;AAAA,EAAG,CAAC,MAAM,sBAAsB;AAC/F;AACA,IAAM,eAAmC;AAAA,EACvC,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,kBAAkB;AAAA,EAAG,CAAC,MAAM,UAAU;AAAA,EAAG,CAAC,MAAM,eAAe;AAAA,EAAG,CAAC,MAAM,2BAA2B;AAAA,EAC9H,CAAC,MAAM,aAAa;AAAA,EAAG,CAAC,MAAM,uBAAuB;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,SAAS;AAAA,EAAG,CAAC,MAAM,sBAAsB;AAAA,EAC3H,CAAC,MAAM,QAAQ;AAAA,EAAG,CAAC,MAAM,cAAc;AAAA,EAAG,CAAC,MAAM,OAAO;AAC1D;AACA,IAAM,YAAgC;AAAA,EACpC,CAAC,OAAO,8BAA8B;AAAA,EAAG,CAAC,OAAO,iBAAiB;AAAA,EAAG,CAAC,MAAM,oBAAoB;AAAA,EAAG,CAAC,OAAO,YAAY;AAAA,EACvH,CAAC,MAAM,iBAAiB;AAAA,EAAG,CAAC,OAAO,UAAU;AAAA,EAAG,CAAC,OAAO,UAAU;AAAA,EAAG,CAAC,MAAM,mBAAmB;AACjG;AAEA,IAAM,UAAyC;AAAA,EAC7C,IAAI,EAAE,aAAa,SAAS,SAAS,WAAW,gBAAgB,MAAM,aAAa,YAAY,gBAAgB,MAAM,eAAe,sBAAsB,eAAe,QAAQ;AAAA,EACjL,IAAI,EAAE,aAAa,YAAY,SAAS,cAAc,gBAAgB,MAAM,aAAa,eAAe,gBAAgB,MAAM,eAAe,yCAAyC,eAAe,UAAU;AAAA,EAC/M,IAAI,EAAE,aAAa,qBAAqB,SAAS,WAAW,gBAAgB,MAAM,aAAa,YAAY,gBAAgB,MAAM,eAAe,YAAY,eAAe,OAAO;AAAA,EAClL,IAAI,EAAE,aAAa,qBAAqB,gBAAgB,OAAO,aAAa,YAAY,gBAAgB,KAAK;AAAA,EAC7G,IAAI,EAAE,aAAa,UAAU,gBAAgB,OAAO,aAAa,sBAAsB,gBAAgB,MAAM;AAAA,EAC7G,IAAI,EAAE,aAAa,qBAAqB,gBAAgB,OAAO,aAAa,YAAY,gBAAgB,MAAM,eAAe,WAAW;AAAA,EACxI,IAAI,EAAE,aAAa,SAAS,gBAAgB,MAAM,aAAa,eAAe,gBAAgB,MAAM,eAAe,WAAW;AAAA,EAC9H,IAAI,EAAE,aAAa,SAAS,gBAAgB,MAAM,aAAa,OAAO,gBAAgB,MAAM,eAAe,mBAAmB;AAAA,EAC9H,IAAI,EAAE,aAAa,cAAc,gBAAgB,MAAM,aAAa,eAAe,gBAAgB,MAAM,eAAe,mBAAmB;AAAA,EAC3I,IAAI,EAAE,aAAa,oBAAoB,gBAAgB,OAAO,aAAa,YAAY,gBAAgB,MAAM,eAAe,WAAW;AACzI;AACA,IAAM,iBAAgC,EAAE,aAAa,6BAA6B,gBAAgB,OAAO,aAAa,eAAe,gBAAgB,MAAM;AAEpJ,SAAS,cAAc,SAAmD;AAC/E,SAAO,SAAS,WAAW,IAAI,YAAY,CAAC,KAAK;AACnD;AAGO,SAAS,eAAe,WAAoD;AACjF,SAAO,OAAO,YAAY,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;AACrF;AAMO,SAAS,mBAAmB,SAA4F;AAC7H,QAAM,IAAI,cAAc,QAAQ,OAAO;AACvC,QAAM,UAAkC,CAAC;AACzC,QAAM,QAAQ,QAAQ,OAAO,KAAK,KAAK;AACvC,MAAI,EAAE,WAAW,OAAO;AACtB,UAAM,MAAM,EAAE,QAAQ,KAAK,CAAC,CAAC,MAAM,IAAI,MAAM,KAAK,YAAY,MAAM,MAAM,YAAY,KAAK,KAAK,YAAY,MAAM,MAAM,YAAY,CAAC;AACrI,QAAI,IAAK,SAAQ,QAAQ,IAAI,CAAC;AAAA,QACzB,SAAQ,wBAAwB,IAAI,YAAY,EAAE,YAAY,YAAY,CAAC;AAAA,EAClF,WAAW,EAAE,kBAAkB,CAAC,MAAO,SAAQ,wBAAwB,IAAI;AAC3E,QAAM,SAAS,QAAQ,YAAY,KAAK,KAAK;AAC7C,MAAI,CAAC,UAAU,EAAE,eAAgB,SAAQ,8BAA8B,IAAI;AAAA,WAClE,UAAU,EAAE,iBAAiB,CAAC,IAAI,OAAO,EAAE,aAAa,EAAE,KAAK,MAAM,EAAG,SAAQ,8BAA8B,IAAI,uBAAuB,EAAE,WAAW,GAAG,EAAE,gBAAgB,UAAU,EAAE,aAAa,MAAM,EAAE;AACrN,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/mail/templates.ts"],"sourcesContent":["import type { StorefrontConfig } from '../config'\nimport { formatMoney } from '../money'\nimport type { Address, Fulfillment, Order } from '../types'\n\nexport interface RenderedMail {\n subject: string\n html: string\n text: string\n}\n\nexport interface MailContext {\n config: StorefrontConfig\n /** Absolute origin for links, no trailing slash. */\n siteUrl: string\n}\n\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c]!)\n}\n\nfunction addressLines(a: Address | null | undefined): string[] {\n if (!a) return []\n return [a.name, a.line1, a.line2, [a.city, a.state, a.postalCode].filter(Boolean).join(', '), a.country].filter(\n (x): x is string => !!x\n )\n}\n\nfunction layout(config: StorefrontConfig, title: string, bodyHtml: string, footer: string): string {\n return `<!doctype html><html><body style=\"margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#1a1a1a\">\n<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"background:#f4f4f5;padding:24px 0\"><tr><td align=\"center\">\n<table role=\"presentation\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" style=\"max-width:600px;width:100%;background:#ffffff;border-radius:8px;overflow:hidden\">\n<tr><td style=\"background:#1f2933;color:#ffffff;padding:20px 28px;font-size:18px;font-weight:700\">${escapeHtml(config.name)}</td></tr>\n<tr><td style=\"padding:28px\"><h1 style=\"margin:0 0 16px;font-size:22px\">${escapeHtml(title)}</h1>${bodyHtml}</td></tr>\n<tr><td style=\"padding:16px 28px;border-top:1px solid #e6e6e6;font-size:12px;color:#666\">${footer}</td></tr>\n</table></td></tr></table></body></html>`\n}\n\nfunction linesTable(order: Order, locale: string): string {\n const rows = order.lines\n .map(\n (l) =>\n `<tr><td style=\"padding:8px 0;border-bottom:1px solid #eee\">${escapeHtml(l.name)}<br><span style=\"color:#666;font-size:12px\">SKU ${escapeHtml(l.sku)} × ${l.quantity}</span></td><td align=\"right\" style=\"padding:8px 0;border-bottom:1px solid #eee;white-space:nowrap\">${formatMoney(l.totalAmount, locale)}</td></tr>`\n )\n .join('')\n const totals = [\n ['Subtotal', formatMoney(order.subtotal, locale)],\n ['Shipping', order.shipping.amount ? formatMoney(order.shipping, locale) : 'Free'],\n ...(order.discount.amount ? [['Discount', `−${formatMoney(order.discount, locale)}`]] : []),\n ...(order.tax.amount ? [['Tax', formatMoney(order.tax, locale)]] : []),\n ['Total', formatMoney(order.total, locale)],\n ]\n .map(([k, v], i, arr) => `<tr><td style=\"padding:6px 0;${i === arr.length - 1 ? 'font-weight:700' : 'color:#666'}\">${k}</td><td align=\"right\" style=\"padding:6px 0;${i === arr.length - 1 ? 'font-weight:700' : ''}\">${v}</td></tr>`)\n .join('')\n return `<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-size:14px\">${rows}${totals}</table>`\n}\n\nfunction linesText(order: Order, locale: string): string[] {\n return [\n ...order.lines.map((l) => `${l.quantity} × ${l.name} (${l.sku}) — ${formatMoney(l.totalAmount, locale)}`),\n '',\n `Subtotal: ${formatMoney(order.subtotal, locale)}`,\n `Shipping: ${order.shipping.amount ? formatMoney(order.shipping, locale) : 'Free'}`,\n ...(order.discount.amount ? [`Discount: -${formatMoney(order.discount, locale)}`] : []),\n ...(order.tax.amount ? [`Tax: ${formatMoney(order.tax, locale)}`] : []),\n `Total: ${formatMoney(order.total, locale)}`,\n ]\n}\n\nfunction contactFooter(config: StorefrontConfig): string {\n const bits = [config.contact?.phone ? `Call ${escapeHtml(config.contact.phone)}` : '', config.contact?.email ? `<a href=\"mailto:${escapeHtml(config.contact.email)}\">${escapeHtml(config.contact.email)}</a>` : '']\n .filter(Boolean)\n .join(' · ')\n return bits ? `Questions? ${bits}` : escapeHtml(config.name)\n}\n\nexport function orderConfirmationMail(order: Order, ctx: MailContext): RenderedMail {\n const { config, siteUrl } = ctx\n const number = `${config.orders.numberPrefix}${order.number}`\n const statusUrl = `${siteUrl}/checkout/success/?order=${encodeURIComponent(order.id)}&key=${encodeURIComponent(order.accessKey)}`\n const ship = addressLines(order.shippingAddress)\n const paid = order.payment.status === 'paid'\n const intro = paid\n ? `Thanks for your order. We have received your payment and will email you again when it ships.`\n : `Thanks for your order. It is reserved and will ship once payment is received.`\n const html = layout(\n config,\n `Order ${number} ${paid ? 'confirmed' : 'received'}`,\n `<p style=\"margin:0 0 16px;font-size:14px\">${intro}</p>\n${linesTable(order, config.locale)}\n${ship.length ? `<h2 style=\"font-size:14px;margin:24px 0 6px\">Shipping to</h2><p style=\"margin:0;font-size:14px;color:#444\">${ship.map(escapeHtml).join('<br>')}</p>` : ''}\n${order.shippingMethod ? `<p style=\"margin:12px 0 0;font-size:13px;color:#666\">${escapeHtml(order.shippingMethod.name)}</p>` : ''}\n<p style=\"margin:24px 0 0;font-size:14px\"><a href=\"${escapeHtml(statusUrl)}\" style=\"color:#0f6fff\">View your order</a></p>`,\n contactFooter(config)\n )\n const text = [\n `${config.name} — order ${number} ${paid ? 'confirmed' : 'received'}`,\n '',\n intro,\n '',\n ...linesText(order, config.locale),\n '',\n ...(ship.length ? ['Shipping to:', ...ship, ''] : []),\n ...(order.shippingMethod ? [order.shippingMethod.name, ''] : []),\n `View your order: ${statusUrl}`,\n ].join('\\n')\n return { subject: `Order ${number} ${paid ? 'confirmed' : 'received'} — ${config.name}`, html, text }\n}\n\nexport function shippingConfirmationMail(order: Order, fulfillment: Fulfillment, ctx: MailContext): RenderedMail {\n const { config, siteUrl } = ctx\n const number = `${config.orders.numberPrefix}${order.number}`\n const shipped = fulfillment.lines\n .map((fl) => {\n const line = order.lines.find((l) => l.sku === fl.sku)\n return { name: line?.name ?? fl.sku, sku: fl.sku, quantity: fl.quantity }\n })\n const tracking = fulfillment.trackingNumber\n ? fulfillment.trackingUrl\n ? `<a href=\"${escapeHtml(fulfillment.trackingUrl)}\" style=\"color:#0f6fff\">${escapeHtml(fulfillment.trackingNumber)}</a>`\n : `<strong>${escapeHtml(fulfillment.trackingNumber)}</strong>`\n : ''\n const carrier = [fulfillment.carrier, fulfillment.service].filter(Boolean).join(' ')\n const portalUrl = `${siteUrl}/account/`\n const html = layout(\n config,\n `Order ${number} is on its way`,\n `<p style=\"margin:0 0 16px;font-size:14px\">Good news — ${shipped.length === order.lines.length && shipped.every((s) => (order.lines.find((l) => l.sku === s.sku)?.quantity ?? 0) === s.quantity) ? 'your order' : 'part of your order'} has shipped${carrier ? ` with ${escapeHtml(carrier)}` : ''}.</p>\n${tracking ? `<p style=\"margin:0 0 16px;font-size:14px\">Tracking number: ${tracking}</p>` : ''}\n<ul style=\"padding-left:18px;font-size:14px;margin:0 0 16px\">${shipped.map((s) => `<li>${s.quantity} × ${escapeHtml(s.name)} <span style=\"color:#666\">(${escapeHtml(s.sku)})</span></li>`).join('')}</ul>\n<p style=\"margin:0;font-size:14px\"><a href=\"${escapeHtml(portalUrl)}\" style=\"color:#0f6fff\">Track it in your account</a></p>`,\n contactFooter(config)\n )\n const text = [\n `${config.name} — order ${number} has shipped${carrier ? ` with ${carrier}` : ''}`,\n '',\n ...(fulfillment.trackingNumber ? [`Tracking: ${fulfillment.trackingNumber}${fulfillment.trackingUrl ? ` (${fulfillment.trackingUrl})` : ''}`, ''] : []),\n ...shipped.map((s) => `${s.quantity} × ${s.name} (${s.sku})`),\n '',\n `Track it in your account: ${portalUrl}`,\n ].join('\\n')\n return { subject: `Order ${number} has shipped — ${config.name}`, html, text }\n}\n\nexport function partnerSignInMail(input: { url: string; name: string; expiresMinutes: number }, ctx: MailContext): RenderedMail {\n const { config } = ctx\n const html = layout(\n config,\n 'Your partner dashboard',\n `<p style=\"margin:0 0 16px;font-size:14px\">Hi ${escapeHtml(input.name)} — click the button to open your ${escapeHtml(config.name)} partner dashboard: visits through your links, orders with your code, and your commission. The link works once and expires in ${input.expiresMinutes} minutes.</p>\n<p style=\"margin:0 0 16px\"><a href=\"${escapeHtml(input.url)}\" style=\"display:inline-block;background:#0f6fff;color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:700;font-size:14px\">Open my dashboard</a></p>\n<p style=\"margin:0;font-size:12px;color:#666\">If you did not request this, you can ignore this email.</p>`,\n escapeHtml(config.name)\n )\n const text = [`Your ${config.name} partner dashboard`, '', `Open this link (it works once and expires in ${input.expiresMinutes} minutes):`, input.url, '', 'If you did not request this, ignore this email.'].join('\\n')\n return { subject: `Your ${config.name} partner dashboard`, html, text }\n}\n\nexport function magicLinkMail(input: { url: string; expiresMinutes: number }, ctx: MailContext): RenderedMail {\n const { config } = ctx\n const html = layout(\n config,\n 'Your sign-in link',\n `<p style=\"margin:0 0 16px;font-size:14px\">Click the button to sign in to your ${escapeHtml(config.name)} account. The link works once and expires in ${input.expiresMinutes} minutes.</p>\n<p style=\"margin:0 0 16px\"><a href=\"${escapeHtml(input.url)}\" style=\"display:inline-block;background:#0f6fff;color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:700;font-size:14px\">Sign in</a></p>\n<p style=\"margin:0;font-size:12px;color:#666\">If you did not request this, you can ignore this email.</p>`,\n escapeHtml(config.name)\n )\n const text = [\n `Sign in to ${config.name}`,\n '',\n `Open this link to sign in (it works once and expires in ${input.expiresMinutes} minutes):`,\n input.url,\n '',\n 'If you did not request this, ignore this email.',\n ].join('\\n')\n return { subject: `Sign in to ${config.name}`, html, text }\n}\n\nexport function ownerPasswordResetMail(input: { url: string; expiresMinutes: number }, ctx: MailContext): RenderedMail {\n const { config } = ctx\n const html = layout(\n config,\n 'Reset your password',\n `<p style=\"margin:0 0 16px;font-size:14px\">Someone asked to reset the owner password for ${escapeHtml(config.name)}. If that was you, click the button. The link works once and expires in ${input.expiresMinutes} minutes.</p>\n<p style=\"margin:0 0 16px\"><a href=\"${escapeHtml(input.url)}\" style=\"display:inline-block;background:#0f6fff;color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:700;font-size:14px\">Choose a new password</a></p>\n<p style=\"margin:0;font-size:12px;color:#666\">If you did not ask for this, ignore this email — your password stays as it is.</p>`,\n escapeHtml(config.name)\n )\n const text = [\n `Reset the owner password for ${config.name}`,\n '',\n `Open this link to choose a new password (it works once and expires in ${input.expiresMinutes} minutes):`,\n input.url,\n '',\n 'If you did not ask for this, ignore this email.',\n ].join('\\n')\n return { subject: `Reset your ${config.name} owner password`, html, text }\n}\n\nexport function merchantNewOrderMail(order: Order, ctx: MailContext): RenderedMail {\n const { config } = ctx\n const number = `${config.orders.numberPrefix}${order.number}`\n const ship = addressLines(order.shippingAddress)\n const text = [\n `New order ${number} — ${formatMoney(order.total, config.locale)}`,\n '',\n ...linesText(order, config.locale),\n '',\n `Customer: ${order.customer.name ?? '(not given)'}`,\n `Email: ${order.customer.email}`,\n `Phone: ${order.customer.phone ?? '(not given)'}`,\n '',\n ...(ship.length ? ['Ship to:', ...ship, ''] : []),\n ...(order.shippingMethod ? [`Shipping: ${order.shippingMethod.name}`] : []),\n `Payment: ${order.payment.status}${order.payment.provider ? ` via ${order.payment.provider}` : ''}${order.payment.reference ? ` (${order.payment.reference})` : ''}`,\n ...(order.notes ? ['', `Notes: ${order.notes}`] : []),\n '',\n `Order id: ${order.id}`,\n ].join('\\n')\n const html = layout(\n config,\n `New order ${number}`,\n `<pre style=\"font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:pre-wrap;margin:0\">${escapeHtml(text)}</pre>`,\n escapeHtml(config.name)\n )\n return { subject: `New order ${number} — ${formatMoney(order.total, config.locale)}`, html, text }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/store/errors.ts","../src/store/crypto.ts","../src/store/ratelimit.ts","../src/store/context.ts","../src/store/connections.ts"],"sourcesContent":["export type StoreErrorCode = 'not_found' | 'validation_failed' | 'conflict' | 'unavailable' | 'rate_limited' | 'invalid_grant'\n\n/** A domain error the API maps straight to a status code. */\nexport class StoreError extends Error {\n readonly code: StoreErrorCode\n readonly details: Record<string, string> | null\n\n constructor(code: StoreErrorCode, message: string, details: Record<string, string> | null = null) {\n super(message)\n this.name = 'StoreError'\n this.code = code\n this.details = details\n }\n\n get status(): number {\n switch (this.code) {\n case 'not_found':\n return 404\n case 'validation_failed':\n return 422\n case 'conflict':\n return 409\n case 'unavailable':\n return 503\n case 'rate_limited':\n return 429\n case 'invalid_grant':\n return 400\n }\n }\n}\n\nexport const notFound = (what: string) => new StoreError('not_found', `${what} not found`)\nexport const invalid = (message: string, details: Record<string, string> | null = null) =>\n new StoreError('validation_failed', message, details)\nexport const conflict = (message: string) => new StoreError('conflict', message)\n","/**\n * Token and hashing helpers on WebCrypto, which both Workers and Node 20+ expose\n * as the global `crypto`.\n */\nconst encoder = new TextEncoder()\n\nconst ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'\n\n/** Random id with a type prefix: ord_…, cus_…, ful_… */\nexport function newId(prefix: string, length = 20): string {\n const bytes = new Uint8Array(length)\n crypto.getRandomValues(bytes)\n let out = ''\n for (const b of bytes) out += ALPHABET[b % ALPHABET.length]\n return `${prefix}_${out}`\n}\n\n/** Random secret token, hex. */\nexport function randomToken(bytes = 32): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return toHex(buf)\n}\n\n/** Random secret as URL-safe base64 (no padding); 32 bytes → 43 characters. */\nexport function randomBase64Url(bytes = 32): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n let bin = ''\n for (const b of buf) bin += String.fromCharCode(b)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nexport function toHex(buf: ArrayBuffer | Uint8Array): string {\n const view = buf instanceof Uint8Array ? buf : new Uint8Array(buf)\n let out = ''\n for (const b of view) out += b.toString(16).padStart(2, '0')\n return out\n}\n\nexport async function sha256Hex(input: string): Promise<string> {\n return toHex(await crypto.subtle.digest('SHA-256', encoder.encode(input)))\n}\n\n/**\n * Hash a bearer token for storage. With a secret the hash is an HMAC, so a\n * copy of the database alone cannot be used to forge a session.\n */\nexport async function hashToken(token: string, secret?: string): Promise<string> {\n if (!secret) return sha256Hex(token)\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n return toHex(await crypto.subtle.sign('HMAC', key, encoder.encode(token)))\n}\n\n/** Compare two secrets without leaking where they differ. Hashes first so lengths never leak either. */\nexport async function secretsEqual(a: string, b: string): Promise<boolean> {\n const [ha, hb] = await Promise.all([sha256Hex(a), sha256Hex(b)])\n let diff = 0\n for (let i = 0; i < ha.length; i++) diff |= ha.charCodeAt(i) ^ hb.charCodeAt(i)\n return diff === 0\n}\n","import type { DbStatement } from '../db/driver'\nimport type { StoreContext } from './context'\nimport { StoreError } from './errors'\n\nexport interface RateLimitPolicy {\n /** Failures allowed inside one window before the key locks. */\n maxFailures: number\n windowMinutes: number\n lockMinutes: number\n}\n\n/**\n * A failure counter in the database, keyed by whatever the caller wants to\n * throttle (sign-ins per email + address, code exchanges per address). Lives\n * in D1 rather than isolate memory so every Worker instance sees the same\n * count. Successes clear the key; nothing is counted for them.\n */\nexport class RateLimiter {\n constructor(private readonly ctx: StoreContext, private readonly policy: RateLimitPolicy) {}\n\n /** Throws `rate_limited` (429) while the key is locked. */\n async assertAllowed(key: string, message: string): Promise<void> {\n const now = this.ctx.now()\n const row = await this.ctx.db.prepare(`SELECT locked_until FROM auth_attempts WHERE key = ?`).bind(key).first<{ locked_until: string | null }>()\n if (row?.locked_until && row.locked_until > now) throw new StoreError('rate_limited', message)\n }\n\n async recordFailure(key: string): Promise<void> {\n const now = this.ctx.now()\n const nowMs = new Date(now).getTime()\n const row = await this.ctx.db\n .prepare(`SELECT failures, window_started_at, locked_until FROM auth_attempts WHERE key = ?`)\n .bind(key)\n .first<{ failures: number; window_started_at: string; locked_until: string | null }>()\n const inWindow = row && nowMs - new Date(row.window_started_at).getTime() < this.policy.windowMinutes * 60_000\n const failures = inWindow ? row!.failures + 1 : 1\n const windowStart = inWindow ? row!.window_started_at : now\n const lockedUntil = failures >= this.policy.maxFailures ? new Date(nowMs + this.policy.lockMinutes * 60_000).toISOString() : null\n await this.ctx.db\n .prepare(\n `INSERT INTO auth_attempts (key, failures, window_started_at, locked_until) VALUES (?, ?, ?, ?)\n ON CONFLICT (key) DO UPDATE SET failures = excluded.failures, window_started_at = excluded.window_started_at, locked_until = excluded.locked_until`\n )\n .bind(key, failures, windowStart, lockedUntil)\n .run()\n }\n\n clearStatement(key: string): DbStatement {\n return this.ctx.db.prepare(`DELETE FROM auth_attempts WHERE key = ?`).bind(key)\n }\n\n /** Rows whose window and lock have both passed carry no information; drop them. */\n pruneStatement(): DbStatement {\n const cutoff = new Date(new Date(this.ctx.now()).getTime() - Math.max(this.policy.windowMinutes, this.policy.lockMinutes) * 60_000).toISOString()\n return this.ctx.db.prepare(`DELETE FROM auth_attempts WHERE window_started_at <= ? AND (locked_until IS NULL OR locked_until <= ?)`).bind(cutoff, this.ctx.now())\n }\n}\n","import type { StorefrontConfig } from '../config'\nimport type { Database } from '../db/driver'\nimport type { MailAdapter } from '../mail/types'\n\n/** Environment the store reads. Every key is optional; missing ones degrade to safe defaults. */\nexport interface StoreEnv {\n /** Absolute public origin used in emails and return URLs. Defaults to config.url. */\n PUBLIC_SITE_URL?: string\n /** HMAC key for session and magic-link hashes. */\n SESSION_SECRET?: string\n /** Merchant address that hears about every paid order. */\n ORDER_NOTIFICATION_EMAIL?: string\n [key: string]: string | undefined\n}\n\nexport interface StoreContext {\n db: Database\n config: StorefrontConfig\n env: StoreEnv\n mail: MailAdapter\n /** ISO timestamp, injectable for tests. */\n now(): string\n siteUrl: string\n log(message: string, data?: unknown): void\n}\n\n/** Split an IN (...) list so no statement exceeds D1's bound-parameter limit. */\nexport function chunk<T>(items: T[], size = 80): T[][] {\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\nexport const placeholders = (n: number) => Array.from({ length: n }, () => '?').join(',')\n\nexport function parseJson<T>(raw: unknown, fallback: T): T {\n if (typeof raw !== 'string' || !raw) return fallback\n try {\n return JSON.parse(raw) as T\n } catch {\n return fallback\n }\n}\n","import { parseJson, type StoreContext } from './context'\nimport { newId, randomBase64Url, randomToken, sha256Hex } from './crypto'\nimport { StoreError } from './errors'\nimport { RateLimiter } from './ratelimit'\n\nexport const CONNECT_CODE_MINUTES = 10\nexport const API_KEY_PREFIX = 'sk_store_'\n/**\n * Operations systems the store can name on its approval screen. Any other\n * client id may connect too — an ERP, an OMS, a script — because nothing is\n * granted until the owner approves the request in /admin/connect, which shows\n * the client, the account and the exact host the browser returns to.\n */\nexport const KNOWN_CONNECT_CLIENTS: Readonly<Record<string, string>> = { skuio: 'SKU.io' }\n/** @deprecated Every well-formed client id is accepted; see KNOWN_CONNECT_CLIENTS. */\nexport const CONNECT_CLIENT_IDS = Object.keys(KNOWN_CONNECT_CLIENTS) as readonly string[]\nconst CLIENT_ID = /^[a-z0-9][a-z0-9._-]{1,62}$/\n\n/** The display name for a client id: a known system's name, else the id itself. */\nexport function connectClientName(clientId: string): string {\n return KNOWN_CONNECT_CLIENTS[clientId] ?? clientId\n}\nexport const CONNECT_SCOPES = ['orders', 'payments', 'fulfillments', 'inventory', 'catalog'] as const\nexport type ConnectScope = (typeof CONNECT_SCOPES)[number]\n\nexport const SCOPE_DESCRIPTIONS: Record<ConnectScope, string> = {\n orders: 'Read orders, stamp its own order number on them and cancel them',\n payments: 'Mark orders paid, refunded or failed (for payments taken outside this store)',\n fulfillments: 'Push shipments and tracking to orders',\n inventory: 'Update available stock',\n catalog: 'Read and update products and categories',\n}\n\nexport interface Connection {\n id: string\n name: string\n tenant: string | null\n redirectHost: string | null\n scopes: ConnectScope[]\n keyPrefix: string | null\n createdByOwnerId: string | null\n createdAt: string\n issuedAt: string | null\n lastUsedAt: string | null\n revokedAt: string | null\n}\n\nexport interface AuthorizeParams {\n client_id?: string | null\n redirect_uri?: string | null\n state?: string | null\n tenant?: string | null\n scope?: string | null\n}\n\n/** A validated connect request, ready to show the owner. */\nexport interface AuthorizeRequest {\n clientId: string\n /** Display name: \"SKU.io\" for skuio, else the client id. */\n clientName: string\n redirectUri: string\n redirectHost: string\n state: string\n tenant: string\n scopes: ConnectScope[]\n}\n\nexport interface IssuedKey {\n apiKey: string\n connection: Connection\n}\n\ninterface ConnectionRow {\n id: string\n name: string\n tenant: string | null\n redirect_host: string | null\n scopes_json: string\n key_hash: string | null\n key_prefix: string | null\n created_by_owner_id: string | null\n created_at: string\n issued_at: string | null\n last_used_at: string | null\n revoked_at: string | null\n}\n\nconst toConnection = (r: ConnectionRow): Connection => ({\n id: r.id,\n name: r.name,\n tenant: r.tenant,\n redirectHost: r.redirect_host,\n scopes: parseJson<string[]>(r.scopes_json, []).filter((s): s is ConnectScope => (CONNECT_SCOPES as readonly string[]).includes(s)),\n keyPrefix: r.key_prefix,\n createdByOwnerId: r.created_by_owner_id,\n createdAt: r.created_at,\n issuedAt: r.issued_at,\n lastUsedAt: r.last_used_at,\n revokedAt: r.revoked_at,\n})\n\n/** http is allowed only where TLS cannot exist: a developer's machine. */\nexport function isLocalHost(hostname: string): boolean {\n const h = hostname.toLowerCase()\n return h === 'localhost' || h === '127.0.0.1' || h === '[::1]' || h.endsWith('.localhost') || h.endsWith('.test')\n}\n\n/**\n * Consumers the owner approved. The store is the authorization server: an\n * owner approves a request in the browser, the consumer trades the resulting\n * one-time code for a per-connection API key, and only the key's SHA-256 is\n * ever stored. Revoking a connection turns its key into a 401.\n */\nexport class ConnectionsService {\n private readonly limiter: RateLimiter\n\n constructor(private readonly ctx: StoreContext) {\n this.limiter = new RateLimiter(ctx, { maxFailures: 20, windowMinutes: 15, lockMinutes: 15 })\n }\n\n /** Validate the query of a connect request. Throws `validation_failed` with a field per problem. */\n validateAuthorize(params: AuthorizeParams): AuthorizeRequest {\n const details: Record<string, string> = {}\n const clientId = (params.client_id ?? '').trim()\n if (!CLIENT_ID.test(clientId)) details.client_id = 'must be 2-63 lowercase letters, digits, dots, dashes or underscores'\n const state = (params.state ?? '').trim()\n if (!state) details.state = 'is required'\n else if (state.length > 1024) details.state = 'must be at most 1024 characters'\n const tenant = (params.tenant ?? '').trim()\n if (!tenant) details.tenant = 'is required'\n else if (tenant.length > 120) details.tenant = 'must be at most 120 characters'\n\n let redirect: URL | null = null\n const raw = (params.redirect_uri ?? '').trim()\n if (!raw) details.redirect_uri = 'is required'\n else {\n try {\n redirect = new URL(raw)\n } catch {\n details.redirect_uri = 'must be an absolute URL'\n }\n }\n if (redirect) {\n if (redirect.hash) details.redirect_uri = 'must not carry a fragment'\n else if (redirect.protocol !== 'https:' && !(redirect.protocol === 'http:' && isLocalHost(redirect.hostname))) details.redirect_uri = 'must use https'\n else if (redirect.username || redirect.password) details.redirect_uri = 'must not carry credentials'\n }\n\n const scopeRaw = (params.scope ?? '').split(/[\\s,]+/).map((s) => s.trim()).filter(Boolean)\n const scopes = [...new Set(scopeRaw)] as string[]\n const unknown = scopes.filter((s) => !(CONNECT_SCOPES as readonly string[]).includes(s))\n if (!scopes.length) details.scope = 'is required'\n else if (unknown.length) details.scope = `unknown scope: ${unknown.join(', ')}`\n\n if (Object.keys(details).length) throw new StoreError('validation_failed', 'Invalid connect request', details)\n return { clientId, clientName: connectClientName(clientId), redirectUri: redirect!.href, redirectHost: redirect!.host, state, tenant, scopes: scopes as ConnectScope[] }\n }\n\n /** The owner said yes: create the connection and a one-time code. Returns the URL to send the browser to. */\n async approve(request: AuthorizeRequest, ownerId: string | null): Promise<{ connection: Connection; code: string; redirectTo: string }> {\n const now = this.ctx.now()\n const id = newId('con')\n const code = randomToken(32)\n const expiresAt = new Date(new Date(now).getTime() + CONNECT_CODE_MINUTES * 60_000).toISOString()\n const name = `${request.tenant} on ${request.clientName}`\n await this.ctx.db.batch([\n this.ctx.db\n .prepare(`INSERT INTO connections (id, name, tenant, redirect_host, scopes_json, created_by_owner_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`)\n .bind(id, name, request.tenant, request.redirectHost, JSON.stringify(request.scopes), ownerId, now),\n this.ctx.db\n .prepare(`INSERT INTO connect_codes (code_hash, connection_id, redirect_uri, expires_at, created_at) VALUES (?, ?, ?, ?, ?)`)\n .bind(await sha256Hex(code), id, request.redirectUri, expiresAt, now),\n ])\n const connection = (await this.byId(id))!\n return { connection, code, redirectTo: withQuery(request.redirectUri, { code, state: request.state }) }\n }\n\n /** The owner said no. */\n denyUrl(request: AuthorizeRequest): string {\n return withQuery(request.redirectUri, { error: 'access_denied', state: request.state })\n }\n\n /**\n * Trade a code for the connection's key. Exactly once: the code is marked used\n * in the same batch that stores the key hash, so a replay finds it used.\n * `address` keys the failure counter.\n */\n async exchange(input: { code?: unknown; redirect_uri?: unknown }, address: string): Promise<IssuedKey> {\n const limitKey = `token:${address}`\n await this.limiter.assertAllowed(limitKey, 'Too many failed exchanges. Try again in a few minutes.')\n const fail = async (message: string) => {\n await this.limiter.recordFailure(limitKey)\n return new StoreError('invalid_grant', message)\n }\n const code = typeof input.code === 'string' ? input.code.trim() : ''\n const redirectUri = typeof input.redirect_uri === 'string' ? input.redirect_uri.trim() : ''\n if (!code || !redirectUri) throw await fail('code and redirect_uri are required')\n const now = this.ctx.now()\n const codeHash = await sha256Hex(code)\n const row = await this.ctx.db\n .prepare(`SELECT connection_id, redirect_uri, expires_at, used_at FROM connect_codes WHERE code_hash = ?`)\n .bind(codeHash)\n .first<{ connection_id: string; redirect_uri: string; expires_at: string; used_at: string | null }>()\n if (!row || row.used_at || row.expires_at <= now) throw await fail('The code is unknown, used or expired')\n if (row.redirect_uri !== redirectUri) throw await fail('redirect_uri does not match the one the code was issued for')\n const existing = await this.byId(row.connection_id)\n if (!existing || existing.revokedAt || existing.keyPrefix) throw await fail('The code is unknown, used or expired')\n\n const apiKey = `${API_KEY_PREFIX}${randomBase64Url(32)}`\n await this.ctx.db.batch([\n this.ctx.db.prepare(`UPDATE connect_codes SET used_at = ? WHERE code_hash = ? AND used_at IS NULL`).bind(now, codeHash),\n this.ctx.db\n .prepare(`UPDATE connections SET key_hash = ?, key_prefix = ?, issued_at = ? WHERE id = ? AND key_hash IS NULL`)\n .bind(await sha256Hex(apiKey), apiKey.slice(0, API_KEY_PREFIX.length + 8), now, row.connection_id),\n this.limiter.clearStatement(limitKey),\n ])\n const connection = (await this.byId(row.connection_id))!\n if (!connection.keyPrefix) throw await fail('The code is unknown, used or expired')\n return { apiKey, connection }\n }\n\n /** The live connection behind a bearer key, or null. Bumps last_used_at at most once a minute. */\n async resolveKey(bearer: string): Promise<Connection | null> {\n if (!bearer.startsWith(API_KEY_PREFIX)) return null\n const row = await this.ctx.db.prepare(`SELECT * FROM connections WHERE key_hash = ?`).bind(await sha256Hex(bearer)).first<ConnectionRow>()\n if (!row || row.revoked_at) return null\n const now = this.ctx.now()\n if (!row.last_used_at || new Date(now).getTime() - new Date(row.last_used_at).getTime() > 60_000) {\n await this.ctx.db.prepare(`UPDATE connections SET last_used_at = ? WHERE id = ?`).bind(now, row.id).run()\n row.last_used_at = now\n }\n return toConnection(row)\n }\n\n async byId(id: string): Promise<Connection | null> {\n const row = await this.ctx.db.prepare(`SELECT * FROM connections WHERE id = ?`).bind(id).first<ConnectionRow>()\n return row ? toConnection(row) : null\n }\n\n /** Every connection that was issued a key, newest first. Pending (never exchanged) ones are not listed. */\n async list(): Promise<Connection[]> {\n const { results } = await this.ctx.db.prepare(`SELECT * FROM connections WHERE key_hash IS NOT NULL ORDER BY created_at DESC`).all<ConnectionRow>()\n return results.map(toConnection)\n }\n\n async revoke(id: string): Promise<boolean> {\n const result = await this.ctx.db.prepare(`UPDATE connections SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL`).bind(this.ctx.now(), id).run()\n return (result.meta?.changes ?? 0) > 0\n }\n\n /** Housekeeping: expired codes, connections whose code was never exchanged, stale limiter rows. */\n async prune(): Promise<void> {\n const now = this.ctx.now()\n await this.ctx.db.batch([\n this.ctx.db.prepare(`DELETE FROM connect_codes WHERE expires_at <= ? OR used_at IS NOT NULL`).bind(now),\n this.ctx.db.prepare(`DELETE FROM connections WHERE key_hash IS NULL AND created_at <= ?`).bind(new Date(new Date(now).getTime() - CONNECT_CODE_MINUTES * 60_000).toISOString()),\n this.limiter.pruneStatement(),\n ])\n }\n}\n\nfunction withQuery(base: string, params: Record<string, string>): string {\n const url = new URL(base)\n for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)\n return url.href\n}\n"],"mappings":"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","names":[]}