@usethrottle/quotes 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/hooks.ts","../src/QuoteView.tsx","../src/QuoteRequestForm.tsx"],"sourcesContent":["import { ThrottleError } from '@usethrottle/errors';\n\n/**\n * Every failure from the quote-link routes surfaces as this. The `code` is the\n * API's own error code, so callers can branch on the ones that matter to a\n * buyer flow rather than parsing messages:\n *\n * revision_superseded — a newer revision was issued; `details.currentRevisionId`\n * tells you what to reload\n * quote_expired — the offer lapsed (HTTP 410)\n * invalid_item_selection — locked-line edit, non-optional deselect, or unknown item\n * accept_in_progress — a concurrent accept won; retry shortly\n * not_found — unknown/rotated token, or a draft/archived quote\n */\nexport class ThrottleQuotesError extends ThrottleError {\n constructor(args: {\n code: string;\n message: string;\n statusCode: number;\n details?: unknown;\n }) {\n super(args);\n this.name = 'ThrottleQuotesError';\n }\n\n /** The offer moved on — reload and show the buyer the current revision. */\n get isSuperseded(): boolean {\n return this.code === 'revision_superseded';\n }\n\n get isExpired(): boolean {\n return this.code === 'quote_expired';\n }\n\n /** Revision the server considers current, when it told us (spec §8.1). */\n get currentRevisionId(): string | undefined {\n const d = this.details as { currentRevisionId?: string } | undefined;\n return d?.currentRevisionId;\n }\n}\n","import { ThrottleQuotesError } from './errors';\nimport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n QuoteComment,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n\nconst DEFAULT_BASE = 'https://api.usethrottle.dev';\n\n/**\n * Headless client for the public quote surfaces. Everything is authorized by\n * the capability token in the URL — there is no API key here, and there must\n * not be: this runs in the buyer's browser.\n */\nexport class QuotesClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: QuotesClientOptions = {}) {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('QuotesClient: fetch is required');\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/+$/, '');\n this.fetchImpl = fetchImpl;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n try {\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers: body === undefined ? {} : { 'content-type': 'application/json' },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n signal: ctrl.signal,\n });\n const json = (await res.json().catch(() => ({}))) as ThrottleEnvelope<T>;\n if (!res.ok) {\n throw new ThrottleQuotesError({\n code: json.error?.code ?? 'unknown_error',\n message: json.error?.message ?? `HTTP ${res.status}`,\n statusCode: res.status,\n details: json.error?.details,\n });\n }\n return json.data as T;\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */\n getQuote(token: string): Promise<BuyerQuote> {\n return this.request<BuyerQuote>('GET', `/api/v1/quote-links/${encodeURIComponent(token)}`);\n }\n\n /**\n * Tell the merchant the buyer opened the quote. Fire-and-forget: the server\n * throttles the resulting webhook to one per revision per 6h, and a failed\n * beacon must never break the page.\n */\n async recordView(token: string): Promise<void> {\n try {\n await this.request('POST', `/api/v1/quote-links/${encodeURIComponent(token)}/view`, {});\n } catch {\n /* telemetry only */\n }\n }\n\n addComment(token: string, input: { body: string; authorName?: string; authorEmail?: string }) {\n return this.request<QuoteComment>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/comments`,\n input,\n );\n }\n\n requestRevision(token: string, input: { message?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/request-revision`,\n input,\n );\n }\n\n decline(token: string, input: { reason?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/decline`,\n input,\n );\n }\n\n /**\n * Accept and get the checkout URL to send the buyer to.\n *\n * Re-accepting the same revision is idempotent and replays the same URL.\n * Accepting a superseded revision throws `revision_superseded` carrying\n * `currentRevisionId`; an expired offer throws `quote_expired`.\n */\n accept(token: string, input: AcceptQuoteInput): Promise<AcceptQuoteResult> {\n return this.request<AcceptQuoteResult>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/accept`,\n input,\n );\n }\n\n /** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */\n pdfUrl(token: string): string {\n return `${this.baseUrl}/api/v1/quote-links/${encodeURIComponent(token)}/pdf`;\n }\n\n // ── RFQ intake ─────────────────────────────────────────────────────────────\n\n getRequestForm(formToken: string): Promise<RequestFormConfig> {\n return this.request<RequestFormConfig>(\n 'GET',\n `/api/v1/quote-request-forms/${encodeURIComponent(formToken)}/public`,\n );\n }\n\n /**\n * Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —\n * anonymous callers learn no internal identifiers. A submission caught by\n * the honeypot returns the same shape without creating anything, so a bot\n * cannot tell it was detected; treat success as \"accepted\", not \"created\".\n */\n submitRequest(formToken: string, input: SubmitQuoteRequestInput): Promise<{ submitted: boolean }> {\n return this.request<{ submitted: boolean }>('POST', '/api/v1/quote-requests', {\n formToken,\n ...input,\n });\n }\n}\n\nexport function createQuotesClient(opts: QuotesClientOptions = {}): QuotesClient {\n return new QuotesClient(opts);\n}\n\n// ── money formatting ─────────────────────────────────────────────────────────\n\n/** Minor units → a display string, e.g. (104000, 'USD') → \"$1,040.00\". */\nexport function formatMoney(minorUnits: number, currency: string, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minorUnits / 100);\n}\n\n/**\n * Scale a line's quoted tax/discount to an edited quantity.\n *\n * MUST match `scaleLineCharges` in @commerce/quotes — the server recomputes\n * the accepted total with this rule, and any divergence shows the buyer one\n * total and charges another.\n */\nexport function scaleLineCharges(\n item: { quantity: number; taxAmount: number; discountAmount: number },\n quantity: number,\n): { taxAmount: number; discountAmount: number } {\n const base = item.quantity;\n if (!base || base <= 0 || quantity === base) {\n return { taxAmount: item.taxAmount, discountAmount: item.discountAmount };\n }\n const ratio = quantity / base;\n return {\n taxAmount: Math.round(item.taxAmount * ratio),\n discountAmount: Math.round(item.discountAmount * ratio),\n };\n}\n\n/** Effective total for a selection, using the same math the server will apply. */\nexport function computeSelectionTotal(\n items: Array<{\n id: string;\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n optional: boolean;\n }>,\n selections: Record<string, { selected: boolean; quantity: number }>,\n shippingTotal = 0,\n): number {\n let total = shippingTotal;\n for (const item of items) {\n const state = selections[item.id];\n if (item.optional && state && !state.selected) continue;\n const qty = state?.quantity ?? item.quantity;\n const { taxAmount, discountAmount } = scaleLineCharges(item, qty);\n total += item.unitPrice * qty + taxAmount - discountAmount;\n }\n return total;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { QuotesClient, computeSelectionTotal, createQuotesClient } from './client';\nimport { ThrottleQuotesError } from './errors';\nimport type {\n AcceptQuoteInput,\n BuyerQuote,\n ItemSelection,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n} from './types';\n\nexport interface UseQuoteResult {\n quote: BuyerQuote | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n /** Re-fetch — call after a `revision_superseded` to show the current offer. */\n refresh: () => Promise<void>;\n /** Buyer's current line choices, keyed by item id. */\n selections: Record<string, { selected: boolean; quantity: number }>;\n setSelected: (itemId: string, selected: boolean) => void;\n setQuantity: (itemId: string, quantity: number) => void;\n /** Live total for the current selections, using the server's own math. */\n total: number;\n accept: (\n input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>,\n ) => Promise<{ checkoutUrl: string }>;\n requestRevision: (message: string, authorName?: string) => Promise<void>;\n decline: (reason?: string, authorName?: string) => Promise<void>;\n addComment: (body: string, authorName?: string) => Promise<void>;\n pdfUrl: string;\n submitting: boolean;\n}\n\n/**\n * Everything the buyer quote page needs.\n *\n * Fires the view beacon once per mount, keeps line selections in local state,\n * and recomputes the total with the same rule the server applies at\n * acceptance — so the number the buyer sees is the number they are charged.\n */\nexport function useQuote(token: string, options: QuotesClientOptions = {}): UseQuoteResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [quote, setQuote] = useState<BuyerQuote | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const [selections, setSelections] = useState<Record<string, { selected: boolean; quantity: number }>>({});\n const beaconSent = useRef(false);\n\n const load = useCallback(async () => {\n setLoading(true);\n try {\n const q = await client.getQuote(token);\n setQuote(q);\n setError(null);\n // Seed selections from the revision's own defaults so a re-issue that\n // adds or locks lines resets the buyer's choices to the new offer.\n const next: Record<string, { selected: boolean; quantity: number }> = {};\n for (const item of q.currentRevision?.items ?? []) {\n next[item.id] = { selected: item.optional ? item.optionalSelected : true, quantity: item.quantity };\n }\n setSelections(next);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n } finally {\n setLoading(false);\n }\n }, [client, token]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (!quote || beaconSent.current) return;\n beaconSent.current = true;\n void client.recordView(token);\n }, [client, quote, token]);\n\n const setSelected = useCallback((itemId: string, selected: boolean) => {\n setSelections((prev) => ({ ...prev, [itemId]: { ...(prev[itemId] ?? { quantity: 1 }), selected } }));\n }, []);\n\n const setQuantity = useCallback((itemId: string, quantity: number) => {\n setSelections((prev) => ({\n ...prev,\n [itemId]: { ...(prev[itemId] ?? { selected: true }), quantity: Math.max(1, Math.floor(quantity) || 1) },\n }));\n }, []);\n\n const total = useMemo(\n () =>\n computeSelectionTotal(\n quote?.currentRevision?.items ?? [],\n selections,\n quote?.currentRevision?.shippingTotal ?? 0,\n ),\n [quote, selections],\n );\n\n /** Only send selections the rep actually allows the buyer to change. */\n const buildSelections = useCallback((): ItemSelection[] => {\n const items = quote?.currentRevision?.items ?? [];\n const out: ItemSelection[] = [];\n for (const item of items) {\n const state = selections[item.id];\n if (!state) continue;\n const sel: ItemSelection = { itemId: item.id };\n let include = false;\n if (item.optional && state.selected !== item.optionalSelected) {\n sel.selected = state.selected;\n include = true;\n }\n if (item.qtyEditable && state.quantity !== item.quantity) {\n sel.quantity = state.quantity;\n include = true;\n }\n if (include) out.push(sel);\n }\n return out;\n }, [quote, selections]);\n\n const guard = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T> => {\n setSubmitting(true);\n try {\n const result = await fn();\n setError(null);\n return result;\n } catch (e) {\n const err = e as ThrottleQuotesError;\n setError(err);\n // A superseded offer means our copy is stale — pull the current one so\n // the buyer sees what they can actually accept (spec §8.1).\n if (err.isSuperseded) void load();\n throw err;\n } finally {\n setSubmitting(false);\n }\n },\n [load],\n );\n\n const accept = useCallback(\n async (input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>) => {\n const revisionId = quote?.currentRevision?.id;\n if (!revisionId) throw new Error('useQuote: no current revision to accept');\n const itemSelections = buildSelections();\n const res = await guard(() =>\n client.accept(token, {\n ...input,\n revisionId,\n ...(itemSelections.length ? { itemSelections } : {}),\n }),\n );\n return { checkoutUrl: res.checkoutUrl };\n },\n [buildSelections, client, guard, quote, token],\n );\n\n const requestRevision = useCallback(\n async (message: string, authorName?: string) => {\n const q = await guard(() => client.requestRevision(token, { message, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const decline = useCallback(\n async (reason?: string, authorName?: string) => {\n const q = await guard(() => client.decline(token, { reason, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const addComment = useCallback(\n async (body: string, authorName?: string) => {\n await guard(() => client.addComment(token, { body, authorName }));\n await load();\n },\n [client, guard, load, token],\n );\n\n return {\n quote,\n loading,\n error,\n refresh: load,\n selections,\n setSelected,\n setQuantity,\n total,\n accept,\n requestRevision,\n decline,\n addComment,\n pdfUrl: client.pdfUrl(token),\n submitting,\n };\n}\n\nexport interface UseQuoteRequestFormResult {\n config: RequestFormConfig | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n submitting: boolean;\n submitted: boolean;\n submit: (input: SubmitQuoteRequestInput) => Promise<void>;\n /** Mount time; pass through so the API's minimum-fill-time check passes. */\n formLoadedAt: number;\n}\n\n/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */\nexport function useQuoteRequestForm(\n formToken: string,\n options: QuotesClientOptions = {},\n): UseQuoteRequestFormResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [config, setConfig] = useState<RequestFormConfig | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [submitted, setSubmitted] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const formLoadedAt = useRef(Date.now()).current;\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const cfg = await client.getRequestForm(formToken);\n if (!cancelled) setConfig(cfg);\n } catch (e) {\n if (!cancelled) setError(e as ThrottleQuotesError);\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, formToken]);\n\n const submit = useCallback(\n async (input: SubmitQuoteRequestInput) => {\n setSubmitting(true);\n try {\n await client.submitRequest(formToken, { formLoadedAt, ...input });\n setSubmitted(true);\n setError(null);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n throw e;\n } finally {\n setSubmitting(false);\n }\n },\n [client, formLoadedAt, formToken],\n );\n\n return { config, loading, error, submitting, submitted, submit, formLoadedAt };\n}\n\nexport { createQuotesClient };\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { formatMoney } from './client';\nimport { useQuote } from './hooks';\nimport type { QuotesClientOptions } from './types';\n\nexport interface QuoteViewProps {\n /** The `qtl_…` token from the buyer's link. */\n token: string;\n options?: QuotesClientOptions;\n /**\n * Where to send the buyer after acceptance. Defaults to assigning\n * `window.location.href`, which is what the hosted page does.\n */\n onCheckout?: (checkoutUrl: string) => void;\n /** Rendered instead of the built-in markup while loading. */\n loadingFallback?: ReactNode;\n className?: string;\n}\n\nconst row: CSSProperties = { display: 'flex', justifyContent: 'space-between', gap: 16, padding: '8px 0' };\nconst muted: CSSProperties = { color: '#666', fontSize: 13 };\n\n/**\n * Drop-in buyer quote page: line items with optional toggles and quantity\n * editing, live totals, revision history, comment thread, and the\n * accept / request-changes / decline actions.\n *\n * Intentionally unstyled beyond a readable default — it is meant to be\n * restyled or replaced. `useQuote` carries all the behaviour if you want to\n * build your own presentation.\n */\nexport function QuoteView({ token, options, onCheckout, loadingFallback, className }: QuoteViewProps) {\n const q = useQuote(token, options ?? {});\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [showAccept, setShowAccept] = useState(false);\n const [comment, setComment] = useState('');\n\n if (q.loading) return <>{loadingFallback ?? <p>Loading quote…</p>}</>;\n if (!q.quote) {\n return <p>This quote link is no longer available.</p>;\n }\n\n const quote = q.quote;\n const rev = quote.currentRevision;\n const currency = quote.currency;\n const ordered = quote.status === 'ordered';\n const canAct = quote.status === 'ready';\n\n const handleAccept = async () => {\n const { checkoutUrl } = await q.accept({ acceptedByName: name, acceptedByEmail: email });\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n return (\n <div className={className}>\n <header>\n {quote.branding?.displayName ? <p style={muted}>{quote.branding.displayName}</p> : null}\n <h1>Quote {quote.quoteNumber}</h1>\n <p style={muted}>{statusLabel(quote.status)}</p>\n {quote.buyerReference ? <p style={muted}>Your reference: {quote.buyerReference}</p> : null}\n {rev ? (\n <p style={muted}>\n Revision {rev.revisionNumber}\n {rev.expiresAt ? ` · valid until ${new Date(rev.expiresAt).toLocaleDateString()}` : null}\n {' · '}\n <a href={q.pdfUrl}>Download PDF</a>\n </p>\n ) : null}\n </header>\n\n {q.error ? (\n <p role=\"alert\">\n {q.error.isSuperseded\n ? 'This quote was updated — showing the latest version.'\n : q.error.message}\n </p>\n ) : null}\n\n {rev ? (\n <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n <thead>\n <tr>\n <th align=\"left\">Item</th>\n <th align=\"right\">Unit price</th>\n <th align=\"right\">Qty</th>\n <th align=\"right\">Total</th>\n </tr>\n </thead>\n <tbody>\n {rev.items.map((item) => {\n const state = q.selections[item.id] ?? { selected: true, quantity: item.quantity };\n const included = item.optional ? state.selected : true;\n return (\n <tr key={item.id}>\n <td>\n {item.optional ? (\n <label>\n <input\n type=\"checkbox\"\n checked={state.selected}\n disabled={!canAct}\n onChange={(e) => q.setSelected(item.id, e.target.checked)}\n />{' '}\n {item.name} <span style={muted}>optional</span>\n </label>\n ) : (\n item.name\n )}\n {item.description ? <div style={muted}>{item.description}</div> : null}\n </td>\n <td align=\"right\">{formatMoney(item.unitPrice, currency)}</td>\n <td align=\"right\">\n {item.qtyEditable && canAct ? (\n <input\n type=\"number\"\n min={1}\n value={state.quantity}\n disabled={!included}\n aria-label={`Quantity for ${item.name}`}\n onChange={(e) => q.setQuantity(item.id, Number(e.target.value))}\n style={{ width: 64 }}\n />\n ) : (\n state.quantity\n )}\n </td>\n <td align=\"right\">\n {included\n ? formatMoney(\n item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,\n currency,\n )\n : formatMoney(0, currency)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n ) : null}\n\n {rev ? (\n <section>\n <div style={row}>\n <strong>Total</strong>\n <strong>{formatMoney(q.total, currency)}</strong>\n </div>\n <p style={muted}>{termsLabel(rev.paymentTerms, rev.depositAmount, currency)}</p>\n {rev.leadTime ? <p style={muted}>Lead time: {rev.leadTime}</p> : null}\n {rev.buyerNote ? <blockquote>{rev.buyerNote}</blockquote> : null}\n </section>\n ) : null}\n\n {ordered ? (\n <p>\n Ordered{quote.orderId ? ` — order ${quote.orderId}` : ''}. Thank you.\n </p>\n ) : canAct ? (\n <section>\n {showAccept ? (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleAccept();\n }}\n >\n <label>\n Your name\n <input value={name} onChange={(e) => setName(e.target.value)} required />\n </label>\n <label>\n Email\n <input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <button type=\"submit\" disabled={q.submitting || !name || !email}>\n Confirm &amp; continue to payment\n </button>\n </form>\n ) : (\n <button type=\"button\" onClick={() => setShowAccept(true)}>\n Accept &amp; checkout\n </button>\n )}\n <button\n type=\"button\"\n disabled={q.submitting}\n onClick={() => void q.requestRevision('Please revise this quote.', name || undefined)}\n >\n Request changes\n </button>\n <button type=\"button\" disabled={q.submitting} onClick={() => void q.decline(undefined, name || undefined)}>\n Decline\n </button>\n </section>\n ) : null}\n\n {quote.revisionHistory.length > 0 ? (\n <details>\n <summary>Previous versions ({quote.revisionHistory.length})</summary>\n <ul>\n {quote.revisionHistory.map((r) => (\n <li key={r.id}>\n Revision {r.revisionNumber} — {formatMoney(r.total, currency)}\n {r.changeSummary ? ` · ${r.changeSummary}` : ''}\n </li>\n ))}\n </ul>\n </details>\n ) : null}\n\n <section>\n <h2>Questions</h2>\n {quote.comments.map((c) => (\n <p key={c.id}>\n <strong>{c.authorName ?? (c.authorType === 'merchant' ? 'Seller' : 'You')}:</strong> {c.body}\n </p>\n ))}\n <form\n onSubmit={(e) => {\n e.preventDefault();\n const body = comment.trim();\n if (!body) return;\n setComment('');\n void q.addComment(body, name || undefined);\n }}\n >\n <input\n value={comment}\n onChange={(e) => setComment(e.target.value)}\n placeholder=\"Ask a question…\"\n aria-label=\"Ask a question\"\n />\n <button type=\"submit\" disabled={q.submitting || !comment.trim()}>\n Send\n </button>\n </form>\n </section>\n </div>\n );\n}\n\nfunction statusLabel(status: string): string {\n switch (status) {\n case 'in_progress':\n return 'We are preparing your quote';\n case 'ready':\n return 'Ready for your review';\n case 'being_revised':\n return 'We are revising this quote';\n case 'accepted':\n return 'Accepted — completing payment';\n case 'ordered':\n return 'Ordered';\n case 'declined':\n return 'Declined';\n case 'expired':\n return 'This quote has expired — request updated pricing';\n default:\n return status;\n }\n}\n\nfunction termsLabel(\n terms: { mode: string; netN?: number; balanceNetN?: number },\n depositAmount: number | null,\n currency: string,\n): string {\n if (terms.mode === 'net_terms') return `Payment terms: Net ${terms.netN}`;\n if (terms.mode === 'deposit_balance') {\n const dep = depositAmount != null ? formatMoney(depositAmount, currency) : 'a deposit';\n return `${dep} due today, balance Net ${terms.balanceNetN}`;\n }\n return 'Payment in full';\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { useQuoteRequestForm } from './hooks';\nimport type { QuotesClientOptions } from './types';\n\nexport interface QuoteRequestFormProps {\n /** The `qrf_…` token from the merchant's hosted form URL. */\n formToken: string;\n options?: QuotesClientOptions;\n /**\n * Turnstile token, when the form requires it. Render the widget yourself and\n * pass its token through — this package deliberately pulls in no Cloudflare\n * script. If `config.fields.requireTurnstile` is set and this is empty, the\n * API fails the submission closed.\n */\n turnstileToken?: string;\n onSubmitted?: () => void;\n loadingFallback?: ReactNode;\n className?: string;\n}\n\n/** Off-screen but still focusable-free; bots fill it, humans never see it. */\nconst honeypotStyle: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Hosted RFQ intake form.\n *\n * Bot protection is wired in for you: the honeypot field and the mount-time\n * anchor that feeds the API's minimum-fill-time check. A honeypot hit returns\n * a success-shaped response and creates nothing, so `onSubmitted` fires either\n * way — by design, a bot must not learn it was caught.\n */\nexport function QuoteRequestForm({\n formToken,\n options,\n turnstileToken,\n onSubmitted,\n loadingFallback,\n className,\n}: QuoteRequestFormProps) {\n const f = useQuoteRequestForm(formToken, options ?? {});\n const [values, setValues] = useState({\n email: '',\n name: '',\n companyName: '',\n phone: '',\n buyerReference: '',\n message: '',\n website: '', // honeypot\n });\n\n const set = (k: keyof typeof values) => (e: { target: { value: string } }) =>\n setValues((prev) => ({ ...prev, [k]: e.target.value }));\n\n if (f.loading) return <>{loadingFallback ?? <p>Loading…</p>}</>;\n if (!f.config) return <p>This request form is not available.</p>;\n\n if (f.submitted) {\n const redirect = f.config.redirectUrl;\n // Re-check the scheme client-side even though the API rejects non-http(s)\n // on write — a stale row must not be able to script the host page.\n if (redirect && /^https?:\\/\\//i.test(redirect) && typeof window !== 'undefined') {\n window.location.href = redirect;\n }\n return <p>{f.config.successMessage ?? 'Thanks — we will be in touch shortly.'}</p>;\n }\n\n const fields = f.config.fields ?? {};\n\n return (\n <form\n className={className}\n onSubmit={(e) => {\n e.preventDefault();\n void f\n .submit({\n email: values.email,\n message: values.message,\n name: values.name || null,\n companyName: fields.companyName ? values.companyName || null : null,\n phone: fields.phone ? values.phone || null : null,\n buyerReference: fields.buyerReference ? values.buyerReference || null : null,\n website: values.website || null,\n ...(turnstileToken ? { turnstileToken } : {}),\n })\n .then(() => onSubmitted?.())\n .catch(() => {\n /* surfaced via f.error */\n });\n }}\n >\n {f.config.title ? <h1>{f.config.title}</h1> : null}\n {f.config.introText ? <p>{f.config.introText}</p> : null}\n {f.error ? <p role=\"alert\">{f.error.message}</p> : null}\n\n <label>\n Email\n <input type=\"email\" value={values.email} onChange={set('email')} required />\n </label>\n <label>\n Name\n <input value={values.name} onChange={set('name')} />\n </label>\n {fields.companyName ? (\n <label>\n Company\n <input value={values.companyName} onChange={set('companyName')} />\n </label>\n ) : null}\n {fields.phone ? (\n <label>\n Phone\n <input value={values.phone} onChange={set('phone')} />\n </label>\n ) : null}\n {fields.buyerReference ? (\n <label>\n Your reference #\n <input value={values.buyerReference} onChange={set('buyerReference')} />\n </label>\n ) : null}\n <label>\n What do you need?\n <textarea value={values.message} onChange={set('message')} required />\n </label>\n\n <div style={honeypotStyle} aria-hidden=\"true\">\n <label>\n Website\n <input\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={values.website}\n onChange={set('website')}\n />\n </label>\n </div>\n\n <button type=\"submit\" disabled={f.submitting || !values.email || !values.message}>\n {f.submitting ? 'Sending…' : 'Request a quote'}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["useState","useState","useState","Fragment","jsx","jsxs","useState"]}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ createQuoteProxyHandler: () => createQuoteProxyHandler
24
+ });
25
+ module.exports = __toCommonJS(server_exports);
26
+ var TOKEN = "[A-Za-z0-9_-]+";
27
+ var ALLOWED = [
28
+ { method: "GET", re: new RegExp(`^/api/v1/quote-links/${TOKEN}$`) },
29
+ { method: "GET", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/pdf$`) },
30
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/view$`) },
31
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/comments$`) },
32
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/request-revision$`) },
33
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/decline$`) },
34
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/accept$`) },
35
+ { method: "GET", re: new RegExp(`^/api/v1/quote-request-forms/${TOKEN}/public$`) },
36
+ { method: "POST", re: /^\/api\/v1\/quote-requests$/ }
37
+ ];
38
+ function createQuoteProxyHandler(options = {}) {
39
+ return async function handler(request, context = {}) {
40
+ const method = request.method.toUpperCase();
41
+ if (method !== "GET" && method !== "POST") {
42
+ return json(405, "method_not_allowed", "Method not allowed");
43
+ }
44
+ const path = normalize(await pathFrom(context, request));
45
+ if (!ALLOWED.some((a) => a.method === method && a.re.test(path))) {
46
+ return json(404, "not_found", "Not found");
47
+ }
48
+ const baseUrl = (options.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
49
+ const fetchImpl = options.fetch ?? globalThis.fetch;
50
+ if (!fetchImpl) throw new Error("createQuoteProxyHandler: fetch is required");
51
+ const search = new URL(request.url).search;
52
+ const body = method === "POST" ? await request.text() : void 0;
53
+ const upstream = await fetchImpl(`${baseUrl}${path}${search}`, {
54
+ method,
55
+ headers: {
56
+ "content-type": "application/json",
57
+ // Forwarded so the API can attribute acceptance evidence and per-IP
58
+ // limits to the real buyer rather than to this proxy.
59
+ ...forwardedFor(request)
60
+ },
61
+ ...body ? { body } : {},
62
+ // The PDF route answers 302 to a short-lived signed URL; hand that
63
+ // straight back so the browser follows it instead of us buffering the file.
64
+ redirect: "manual"
65
+ });
66
+ if (upstream.status === 302 || upstream.status === 303) {
67
+ const location = upstream.headers.get("location");
68
+ if (location) return new Response(null, { status: 302, headers: { location } });
69
+ }
70
+ const headers = new Headers(upstream.headers);
71
+ headers.delete("content-encoding");
72
+ headers.delete("content-length");
73
+ return new Response(await upstream.text(), { status: upstream.status, headers });
74
+ };
75
+ }
76
+ function forwardedFor(request) {
77
+ const existing = request.headers.get("x-forwarded-for");
78
+ return existing ? { "x-forwarded-for": existing } : {};
79
+ }
80
+ async function pathFrom(context, request) {
81
+ const raw = context.params?.path ?? context.path;
82
+ const segments = raw ? Array.isArray(await raw) ? await raw : [String(raw)] : null;
83
+ if (segments) return `/${segments.join("/")}`;
84
+ return new URL(request.url).pathname;
85
+ }
86
+ function normalize(path) {
87
+ const clean = `/${path.replace(/^\/+/, "")}`.replace(/\/{2,}/g, "/");
88
+ if (clean.includes("..")) return "/__invalid__";
89
+ return clean.startsWith("/api/v1/") ? clean : `/api/v1${clean}`;
90
+ }
91
+ function json(status, code, message) {
92
+ return new Response(JSON.stringify({ error: { code, message } }), {
93
+ status,
94
+ headers: { "content-type": "application/json" }
95
+ });
96
+ }
97
+ // Annotate the CommonJS export names for ESM import in node:
98
+ 0 && (module.exports = {
99
+ createQuoteProxyHandler
100
+ });
101
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Same-origin proxy for the public quote surfaces.\n *\n * Quote links are authorized by the capability token in the URL, so the buyer\n * page CAN call the Throttle API directly. Merchants usually don't want that:\n * proxying keeps the buyer on their own origin (no third-party host in the\n * address bar or CSP), and lets them add their own logging or rate limiting.\n *\n * No API key is involved — this forwards the token exactly as the browser sent\n * it. Deliberately allow-lists the public paths so it can never be pointed at\n * a merchant endpoint, and refuses anything but GET/POST.\n */\n\nexport interface QuoteProxyOptions {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n}\n\nexport interface QuoteProxyContext {\n params?: { path?: string[] | Promise<string[]> };\n path?: string[] | string;\n}\n\n// Anchored so a crafted token cannot walk out of the public surface.\nconst TOKEN = '[A-Za-z0-9_-]+';\nconst ALLOWED: Array<{ method: 'GET' | 'POST'; re: RegExp }> = [\n { method: 'GET', re: new RegExp(`^/api/v1/quote-links/${TOKEN}$`) },\n { method: 'GET', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/pdf$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/view$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/comments$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/request-revision$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/decline$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/accept$`) },\n { method: 'GET', re: new RegExp(`^/api/v1/quote-request-forms/${TOKEN}/public$`) },\n { method: 'POST', re: /^\\/api\\/v1\\/quote-requests$/ },\n];\n\nexport function createQuoteProxyHandler(options: QuoteProxyOptions = {}) {\n return async function handler(request: Request, context: QuoteProxyContext = {}): Promise<Response> {\n const method = request.method.toUpperCase();\n if (method !== 'GET' && method !== 'POST') {\n return json(405, 'method_not_allowed', 'Method not allowed');\n }\n\n const path = normalize(await pathFrom(context, request));\n if (!ALLOWED.some((a) => a.method === method && a.re.test(path))) {\n return json(404, 'not_found', 'Not found');\n }\n\n const baseUrl = (options.baseUrl ?? 'https://api.usethrottle.dev').replace(/\\/+$/, '');\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('createQuoteProxyHandler: fetch is required');\n\n const search = new URL(request.url).search;\n const body = method === 'POST' ? await request.text() : undefined;\n\n const upstream = await fetchImpl(`${baseUrl}${path}${search}`, {\n method,\n headers: {\n 'content-type': 'application/json',\n // Forwarded so the API can attribute acceptance evidence and per-IP\n // limits to the real buyer rather than to this proxy.\n ...forwardedFor(request),\n },\n ...(body ? { body } : {}),\n // The PDF route answers 302 to a short-lived signed URL; hand that\n // straight back so the browser follows it instead of us buffering the file.\n redirect: 'manual',\n });\n\n if (upstream.status === 302 || upstream.status === 303) {\n const location = upstream.headers.get('location');\n if (location) return new Response(null, { status: 302, headers: { location } });\n }\n\n const headers = new Headers(upstream.headers);\n headers.delete('content-encoding');\n headers.delete('content-length');\n return new Response(await upstream.text(), { status: upstream.status, headers });\n };\n}\n\nfunction forwardedFor(request: Request): Record<string, string> {\n const existing = request.headers.get('x-forwarded-for');\n return existing ? { 'x-forwarded-for': existing } : {};\n}\n\nasync function pathFrom(context: QuoteProxyContext, request: Request): Promise<string> {\n const raw = context.params?.path ?? context.path;\n const segments = raw ? (Array.isArray(await raw) ? ((await raw) as string[]) : [String(raw)]) : null;\n if (segments) return `/${segments.join('/')}`;\n return new URL(request.url).pathname;\n}\n\n/** Force the upstream path into the /api/v1/… namespace, without traversal. */\nfunction normalize(path: string): string {\n const clean = `/${path.replace(/^\\/+/, '')}`.replace(/\\/{2,}/g, '/');\n if (clean.includes('..')) return '/__invalid__';\n return clean.startsWith('/api/v1/') ? clean : `/api/v1${clean}`;\n}\n\nfunction json(status: number, code: string, message: string): Response {\n return new Response(JSON.stringify({ error: { code, message } }), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Same-origin proxy for the public quote surfaces.
3
+ *
4
+ * Quote links are authorized by the capability token in the URL, so the buyer
5
+ * page CAN call the Throttle API directly. Merchants usually don't want that:
6
+ * proxying keeps the buyer on their own origin (no third-party host in the
7
+ * address bar or CSP), and lets them add their own logging or rate limiting.
8
+ *
9
+ * No API key is involved — this forwards the token exactly as the browser sent
10
+ * it. Deliberately allow-lists the public paths so it can never be pointed at
11
+ * a merchant endpoint, and refuses anything but GET/POST.
12
+ */
13
+ interface QuoteProxyOptions {
14
+ baseUrl?: string;
15
+ fetch?: typeof globalThis.fetch;
16
+ }
17
+ interface QuoteProxyContext {
18
+ params?: {
19
+ path?: string[] | Promise<string[]>;
20
+ };
21
+ path?: string[] | string;
22
+ }
23
+ declare function createQuoteProxyHandler(options?: QuoteProxyOptions): (request: Request, context?: QuoteProxyContext) => Promise<Response>;
24
+
25
+ export { type QuoteProxyContext, type QuoteProxyOptions, createQuoteProxyHandler };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Same-origin proxy for the public quote surfaces.
3
+ *
4
+ * Quote links are authorized by the capability token in the URL, so the buyer
5
+ * page CAN call the Throttle API directly. Merchants usually don't want that:
6
+ * proxying keeps the buyer on their own origin (no third-party host in the
7
+ * address bar or CSP), and lets them add their own logging or rate limiting.
8
+ *
9
+ * No API key is involved — this forwards the token exactly as the browser sent
10
+ * it. Deliberately allow-lists the public paths so it can never be pointed at
11
+ * a merchant endpoint, and refuses anything but GET/POST.
12
+ */
13
+ interface QuoteProxyOptions {
14
+ baseUrl?: string;
15
+ fetch?: typeof globalThis.fetch;
16
+ }
17
+ interface QuoteProxyContext {
18
+ params?: {
19
+ path?: string[] | Promise<string[]>;
20
+ };
21
+ path?: string[] | string;
22
+ }
23
+ declare function createQuoteProxyHandler(options?: QuoteProxyOptions): (request: Request, context?: QuoteProxyContext) => Promise<Response>;
24
+
25
+ export { type QuoteProxyContext, type QuoteProxyOptions, createQuoteProxyHandler };
package/dist/server.js ADDED
@@ -0,0 +1,76 @@
1
+ // src/server.ts
2
+ var TOKEN = "[A-Za-z0-9_-]+";
3
+ var ALLOWED = [
4
+ { method: "GET", re: new RegExp(`^/api/v1/quote-links/${TOKEN}$`) },
5
+ { method: "GET", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/pdf$`) },
6
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/view$`) },
7
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/comments$`) },
8
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/request-revision$`) },
9
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/decline$`) },
10
+ { method: "POST", re: new RegExp(`^/api/v1/quote-links/${TOKEN}/accept$`) },
11
+ { method: "GET", re: new RegExp(`^/api/v1/quote-request-forms/${TOKEN}/public$`) },
12
+ { method: "POST", re: /^\/api\/v1\/quote-requests$/ }
13
+ ];
14
+ function createQuoteProxyHandler(options = {}) {
15
+ return async function handler(request, context = {}) {
16
+ const method = request.method.toUpperCase();
17
+ if (method !== "GET" && method !== "POST") {
18
+ return json(405, "method_not_allowed", "Method not allowed");
19
+ }
20
+ const path = normalize(await pathFrom(context, request));
21
+ if (!ALLOWED.some((a) => a.method === method && a.re.test(path))) {
22
+ return json(404, "not_found", "Not found");
23
+ }
24
+ const baseUrl = (options.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
25
+ const fetchImpl = options.fetch ?? globalThis.fetch;
26
+ if (!fetchImpl) throw new Error("createQuoteProxyHandler: fetch is required");
27
+ const search = new URL(request.url).search;
28
+ const body = method === "POST" ? await request.text() : void 0;
29
+ const upstream = await fetchImpl(`${baseUrl}${path}${search}`, {
30
+ method,
31
+ headers: {
32
+ "content-type": "application/json",
33
+ // Forwarded so the API can attribute acceptance evidence and per-IP
34
+ // limits to the real buyer rather than to this proxy.
35
+ ...forwardedFor(request)
36
+ },
37
+ ...body ? { body } : {},
38
+ // The PDF route answers 302 to a short-lived signed URL; hand that
39
+ // straight back so the browser follows it instead of us buffering the file.
40
+ redirect: "manual"
41
+ });
42
+ if (upstream.status === 302 || upstream.status === 303) {
43
+ const location = upstream.headers.get("location");
44
+ if (location) return new Response(null, { status: 302, headers: { location } });
45
+ }
46
+ const headers = new Headers(upstream.headers);
47
+ headers.delete("content-encoding");
48
+ headers.delete("content-length");
49
+ return new Response(await upstream.text(), { status: upstream.status, headers });
50
+ };
51
+ }
52
+ function forwardedFor(request) {
53
+ const existing = request.headers.get("x-forwarded-for");
54
+ return existing ? { "x-forwarded-for": existing } : {};
55
+ }
56
+ async function pathFrom(context, request) {
57
+ const raw = context.params?.path ?? context.path;
58
+ const segments = raw ? Array.isArray(await raw) ? await raw : [String(raw)] : null;
59
+ if (segments) return `/${segments.join("/")}`;
60
+ return new URL(request.url).pathname;
61
+ }
62
+ function normalize(path) {
63
+ const clean = `/${path.replace(/^\/+/, "")}`.replace(/\/{2,}/g, "/");
64
+ if (clean.includes("..")) return "/__invalid__";
65
+ return clean.startsWith("/api/v1/") ? clean : `/api/v1${clean}`;
66
+ }
67
+ function json(status, code, message) {
68
+ return new Response(JSON.stringify({ error: { code, message } }), {
69
+ status,
70
+ headers: { "content-type": "application/json" }
71
+ });
72
+ }
73
+ export {
74
+ createQuoteProxyHandler
75
+ };
76
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Same-origin proxy for the public quote surfaces.\n *\n * Quote links are authorized by the capability token in the URL, so the buyer\n * page CAN call the Throttle API directly. Merchants usually don't want that:\n * proxying keeps the buyer on their own origin (no third-party host in the\n * address bar or CSP), and lets them add their own logging or rate limiting.\n *\n * No API key is involved — this forwards the token exactly as the browser sent\n * it. Deliberately allow-lists the public paths so it can never be pointed at\n * a merchant endpoint, and refuses anything but GET/POST.\n */\n\nexport interface QuoteProxyOptions {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n}\n\nexport interface QuoteProxyContext {\n params?: { path?: string[] | Promise<string[]> };\n path?: string[] | string;\n}\n\n// Anchored so a crafted token cannot walk out of the public surface.\nconst TOKEN = '[A-Za-z0-9_-]+';\nconst ALLOWED: Array<{ method: 'GET' | 'POST'; re: RegExp }> = [\n { method: 'GET', re: new RegExp(`^/api/v1/quote-links/${TOKEN}$`) },\n { method: 'GET', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/pdf$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/view$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/comments$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/request-revision$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/decline$`) },\n { method: 'POST', re: new RegExp(`^/api/v1/quote-links/${TOKEN}/accept$`) },\n { method: 'GET', re: new RegExp(`^/api/v1/quote-request-forms/${TOKEN}/public$`) },\n { method: 'POST', re: /^\\/api\\/v1\\/quote-requests$/ },\n];\n\nexport function createQuoteProxyHandler(options: QuoteProxyOptions = {}) {\n return async function handler(request: Request, context: QuoteProxyContext = {}): Promise<Response> {\n const method = request.method.toUpperCase();\n if (method !== 'GET' && method !== 'POST') {\n return json(405, 'method_not_allowed', 'Method not allowed');\n }\n\n const path = normalize(await pathFrom(context, request));\n if (!ALLOWED.some((a) => a.method === method && a.re.test(path))) {\n return json(404, 'not_found', 'Not found');\n }\n\n const baseUrl = (options.baseUrl ?? 'https://api.usethrottle.dev').replace(/\\/+$/, '');\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('createQuoteProxyHandler: fetch is required');\n\n const search = new URL(request.url).search;\n const body = method === 'POST' ? await request.text() : undefined;\n\n const upstream = await fetchImpl(`${baseUrl}${path}${search}`, {\n method,\n headers: {\n 'content-type': 'application/json',\n // Forwarded so the API can attribute acceptance evidence and per-IP\n // limits to the real buyer rather than to this proxy.\n ...forwardedFor(request),\n },\n ...(body ? { body } : {}),\n // The PDF route answers 302 to a short-lived signed URL; hand that\n // straight back so the browser follows it instead of us buffering the file.\n redirect: 'manual',\n });\n\n if (upstream.status === 302 || upstream.status === 303) {\n const location = upstream.headers.get('location');\n if (location) return new Response(null, { status: 302, headers: { location } });\n }\n\n const headers = new Headers(upstream.headers);\n headers.delete('content-encoding');\n headers.delete('content-length');\n return new Response(await upstream.text(), { status: upstream.status, headers });\n };\n}\n\nfunction forwardedFor(request: Request): Record<string, string> {\n const existing = request.headers.get('x-forwarded-for');\n return existing ? { 'x-forwarded-for': existing } : {};\n}\n\nasync function pathFrom(context: QuoteProxyContext, request: Request): Promise<string> {\n const raw = context.params?.path ?? context.path;\n const segments = raw ? (Array.isArray(await raw) ? ((await raw) as string[]) : [String(raw)]) : null;\n if (segments) return `/${segments.join('/')}`;\n return new URL(request.url).pathname;\n}\n\n/** Force the upstream path into the /api/v1/… namespace, without traversal. */\nfunction normalize(path: string): string {\n const clean = `/${path.replace(/^\\/+/, '')}`.replace(/\\/{2,}/g, '/');\n if (clean.includes('..')) return '/__invalid__';\n return clean.startsWith('/api/v1/') ? clean : `/api/v1${clean}`;\n}\n\nfunction json(status: number, code: string, message: string): Response {\n return new Response(JSON.stringify({ error: { code, message } }), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@usethrottle/quotes",
3
+ "version": "0.1.0",
4
+ "description": "Buyer-facing React components, hooks and server proxy for Throttle B2B quotes — hosted quote links and RFQ request forms.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./server": {
16
+ "types": "./dist/server.d.ts",
17
+ "import": "./dist/server.js",
18
+ "require": "./dist/server.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "keywords": [
26
+ "throttle",
27
+ "quotes",
28
+ "b2b",
29
+ "rfq",
30
+ "checkout",
31
+ "react"
32
+ ],
33
+ "dependencies": {
34
+ "@usethrottle/errors": "^1.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "react": ">=18"
38
+ },
39
+ "devDependencies": {
40
+ "@testing-library/react": "^16.1.0",
41
+ "@types/react": "^19.1.0",
42
+ "@vitejs/plugin-react": "^4.3.4",
43
+ "jsdom": "^25.0.1",
44
+ "react": "^19.1.0",
45
+ "react-dom": "^19.1.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.7.0",
48
+ "vitest": "^2.1.9"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "dev": "tsup --watch",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "typecheck": "tsc --noEmit"
62
+ }
63
+ }