@skuio/storefront 0.6.0 → 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/README.md +2 -2
- package/db/migrations/0008_quotes.sql +59 -0
- package/dist/api.d.ts +48 -4
- package/dist/api.js +112 -2
- package/dist/api.js.map +1 -1
- package/dist/{chunk-UCREHLZH.js → chunk-BD274TXU.js} +84 -2
- package/dist/chunk-BD274TXU.js.map +1 -0
- package/dist/{chunk-56ODALDY.js → chunk-NM5YP764.js} +1 -1
- package/dist/chunk-NM5YP764.js.map +1 -0
- package/dist/{chunk-X7VXO4QU.js → chunk-RUUUZ2EL.js} +4 -3
- package/dist/chunk-RUUUZ2EL.js.map +1 -0
- package/dist/{config-C4YCi7m4.d.ts → config-CCxTka_z.d.ts} +78 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/mail.d.ts +10 -2
- package/dist/mail.js +9 -1
- package/dist/payments/bitcoin.d.ts +1 -1
- package/dist/payments/btcpay.d.ts +1 -1
- package/dist/payments/manual.d.ts +1 -1
- package/dist/payments/manual.js +1 -1
- package/dist/payments/paypal.d.ts +1 -1
- package/dist/payments/stripe.d.ts +1 -1
- package/dist/payments.d.ts +1 -1
- package/dist/store.d.ts +163 -8
- package/dist/store.js +516 -50
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-56ODALDY.js.map +0 -1
- package/dist/chunk-UCREHLZH.js.map +0 -1
- package/dist/chunk-X7VXO4QU.js.map +0 -1
|
@@ -189,6 +189,84 @@ function merchantNewOrderMail(order, ctx) {
|
|
|
189
189
|
);
|
|
190
190
|
return { subject: `New order ${number} \u2014 ${formatMoney(order.total, config.locale)}`, html, text };
|
|
191
191
|
}
|
|
192
|
+
function quoteUrl(quote, siteUrl) {
|
|
193
|
+
return `${siteUrl}/quotes/${encodeURIComponent(quote.id)}/?key=${encodeURIComponent(quote.accessKey)}`;
|
|
194
|
+
}
|
|
195
|
+
function quoteNumber(quote, config) {
|
|
196
|
+
return `${config.quotes.numberPrefix}${quote.number}`;
|
|
197
|
+
}
|
|
198
|
+
function quoteLinesTable(quote, locale, priced) {
|
|
199
|
+
const rows = quote.lines.map(
|
|
200
|
+
(l) => `<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)} \xD7 ${l.quantity}${priced && l.unitAmount ? ` at ${formatMoney(l.unitAmount, locale)}` : ""}</span></td><td align="right" style="padding:8px 0;border-bottom:1px solid #eee;white-space:nowrap">${priced && l.totalAmount ? formatMoney(l.totalAmount, locale) : ""}</td></tr>`
|
|
201
|
+
).join("");
|
|
202
|
+
const totals = priced && quote.total ? [
|
|
203
|
+
["Subtotal", quote.subtotal ? formatMoney(quote.subtotal, locale) : ""],
|
|
204
|
+
["Shipping", quote.shipping?.amount ? formatMoney(quote.shipping, locale) : "Included"],
|
|
205
|
+
...quote.tax?.amount ? [["Tax", formatMoney(quote.tax, locale)]] : [],
|
|
206
|
+
["Total", formatMoney(quote.total, locale)]
|
|
207
|
+
].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>`).join("") : "";
|
|
208
|
+
return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="font-size:14px">${rows}${totals}</table>`;
|
|
209
|
+
}
|
|
210
|
+
function quoteLinesText(quote, locale, priced) {
|
|
211
|
+
return [
|
|
212
|
+
...quote.lines.map((l) => `${l.quantity} \xD7 ${l.name} (${l.sku})${priced && l.totalAmount ? ` \u2014 ${formatMoney(l.totalAmount, locale)}` : ""}`),
|
|
213
|
+
...priced && quote.total ? [
|
|
214
|
+
"",
|
|
215
|
+
...quote.subtotal ? [`Subtotal: ${formatMoney(quote.subtotal, locale)}`] : [],
|
|
216
|
+
`Shipping: ${quote.shipping?.amount ? formatMoney(quote.shipping, locale) : "Included"}`,
|
|
217
|
+
...quote.tax?.amount ? [`Tax: ${formatMoney(quote.tax, locale)}`] : [],
|
|
218
|
+
`Total: ${formatMoney(quote.total, locale)}`
|
|
219
|
+
] : []
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
var button = (url, label) => `<p style="margin:24px 0 0"><a href="${escapeHtml(url)}" style="display:inline-block;background:#0f6fff;color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:700;font-size:14px">${escapeHtml(label)}</a></p>`;
|
|
223
|
+
function quoteReceivedMail(quote, ctx) {
|
|
224
|
+
const { config, siteUrl } = ctx;
|
|
225
|
+
const number = quoteNumber(quote, config);
|
|
226
|
+
const url = quoteUrl(quote, siteUrl);
|
|
227
|
+
const intro = `Thanks \u2014 we have your request for a quote and will email you when it is priced. Keep this email: the link below shows the quote's status at any time.`;
|
|
228
|
+
const html = layout(config, `Quote request ${number} received`, `<p style="margin:0 0 16px;font-size:14px">${intro}</p>${quoteLinesTable(quote, config.locale, false)}${button(url, "View your quote request")}`, contactFooter(config));
|
|
229
|
+
const text = [`${config.name} \u2014 quote request ${number} received`, "", intro, "", ...quoteLinesText(quote, config.locale, false), "", `View your quote request: ${url}`].join("\n");
|
|
230
|
+
return { subject: `Quote request ${number} received \u2014 ${config.name}`, html, text };
|
|
231
|
+
}
|
|
232
|
+
function quoteReadyMail(quote, ctx) {
|
|
233
|
+
const { config, siteUrl } = ctx;
|
|
234
|
+
const number = quoteNumber(quote, config);
|
|
235
|
+
const url = quoteUrl(quote, siteUrl);
|
|
236
|
+
const expiry = quote.expiresAt ? ` It is valid until ${new Date(quote.expiresAt).toLocaleDateString(config.locale, { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" })}.` : "";
|
|
237
|
+
const intro = `Your quote is ready.${expiry} Accept it to place the order at these prices, or decline it \u2014 either takes one click.`;
|
|
238
|
+
const html = layout(
|
|
239
|
+
config,
|
|
240
|
+
`Quote ${number} is ready`,
|
|
241
|
+
`<p style="margin:0 0 16px;font-size:14px">${escapeHtml(intro)}</p>${quoteLinesTable(quote, config.locale, true)}${quote.offerNote ? `<p style="margin:16px 0 0;font-size:14px;color:#444;white-space:pre-wrap">${escapeHtml(quote.offerNote)}</p>` : ""}${button(url, "Review and accept")}`,
|
|
242
|
+
contactFooter(config)
|
|
243
|
+
);
|
|
244
|
+
const text = [`${config.name} \u2014 quote ${number} is ready`, "", intro, "", ...quoteLinesText(quote, config.locale, true), ...quote.offerNote ? ["", quote.offerNote] : [], "", `Review and accept: ${url}`].join("\n");
|
|
245
|
+
return { subject: `Quote ${number} is ready \u2014 ${config.name}`, html, text };
|
|
246
|
+
}
|
|
247
|
+
function merchantQuoteMail(quote, ctx, event, orderNumber) {
|
|
248
|
+
const { config } = ctx;
|
|
249
|
+
const number = quoteNumber(quote, config);
|
|
250
|
+
const headline = event === "requested" ? `New quote request ${number}` : event === "accepted" ? `Quote ${number} accepted${orderNumber ? ` \u2014 order ${config.orders.numberPrefix}${orderNumber}` : ""}` : `Quote ${number} declined`;
|
|
251
|
+
const ship = addressLines(quote.shippingAddress);
|
|
252
|
+
const text = [
|
|
253
|
+
headline,
|
|
254
|
+
"",
|
|
255
|
+
...quoteLinesText(quote, config.locale, event !== "requested"),
|
|
256
|
+
"",
|
|
257
|
+
`Customer: ${quote.customer.name ?? "(not given)"}`,
|
|
258
|
+
...quote.company ? [`Company: ${quote.company}`] : [],
|
|
259
|
+
`Email: ${quote.customer.email}`,
|
|
260
|
+
`Phone: ${quote.customer.phone ?? "(not given)"}`,
|
|
261
|
+
...ship.length ? ["", "Ship to:", ...ship] : [],
|
|
262
|
+
...quote.message ? ["", `Message: ${quote.message}`] : [],
|
|
263
|
+
...event === "declined" && quote.declineReason ? ["", `Reason: ${quote.declineReason}`] : [],
|
|
264
|
+
"",
|
|
265
|
+
`Quote id: ${quote.id}`
|
|
266
|
+
].join("\n");
|
|
267
|
+
const html = layout(config, headline, `<pre style="font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:pre-wrap;margin:0">${escapeHtml(text)}</pre>`, escapeHtml(config.name));
|
|
268
|
+
return { subject: `${headline} \u2014 ${config.name}`, html, text };
|
|
269
|
+
}
|
|
192
270
|
|
|
193
271
|
export {
|
|
194
272
|
escapeHtml,
|
|
@@ -197,6 +275,10 @@ export {
|
|
|
197
275
|
partnerSignInMail,
|
|
198
276
|
magicLinkMail,
|
|
199
277
|
ownerPasswordResetMail,
|
|
200
|
-
merchantNewOrderMail
|
|
278
|
+
merchantNewOrderMail,
|
|
279
|
+
quoteUrl,
|
|
280
|
+
quoteReceivedMail,
|
|
281
|
+
quoteReadyMail,
|
|
282
|
+
merchantQuoteMail
|
|
201
283
|
};
|
|
202
|
-
//# sourceMappingURL=chunk-
|
|
284
|
+
//# sourceMappingURL=chunk-BD274TXU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mail/templates.ts"],"sourcesContent":["import type { StorefrontConfig } from '../config'\nimport { formatMoney } from '../money'\nimport type { Address, Fulfillment, Order, StoreQuote } 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) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[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\n/** Where a buyer opens a quote without signing in: the access key is the credential. */\nexport function quoteUrl(quote: StoreQuote, siteUrl: string): string {\n return `${siteUrl}/quotes/${encodeURIComponent(quote.id)}/?key=${encodeURIComponent(quote.accessKey)}`\n}\n\nfunction quoteNumber(quote: StoreQuote, config: StorefrontConfig): string {\n return `${config.quotes.numberPrefix}${quote.number}`\n}\n\nfunction quoteLinesTable(quote: StoreQuote, locale: string, priced: boolean): string {\n const rows = quote.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}${priced && l.unitAmount ? ` at ${formatMoney(l.unitAmount, locale)}` : ''}</span></td><td align=\"right\" style=\"padding:8px 0;border-bottom:1px solid #eee;white-space:nowrap\">${priced && l.totalAmount ? formatMoney(l.totalAmount, locale) : ''}</td></tr>`\n )\n .join('')\n const totals =\n priced && quote.total\n ? [\n ['Subtotal', quote.subtotal ? formatMoney(quote.subtotal, locale) : ''],\n ['Shipping', quote.shipping?.amount ? formatMoney(quote.shipping, locale) : 'Included'],\n ...(quote.tax?.amount ? [['Tax', formatMoney(quote.tax, locale)]] : []),\n ['Total', formatMoney(quote.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 : ''\n return `<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-size:14px\">${rows}${totals}</table>`\n}\n\nfunction quoteLinesText(quote: StoreQuote, locale: string, priced: boolean): string[] {\n return [\n ...quote.lines.map((l) => `${l.quantity} × ${l.name} (${l.sku})${priced && l.totalAmount ? ` — ${formatMoney(l.totalAmount, locale)}` : ''}`),\n ...(priced && quote.total\n ? [\n '',\n ...(quote.subtotal ? [`Subtotal: ${formatMoney(quote.subtotal, locale)}`] : []),\n `Shipping: ${quote.shipping?.amount ? formatMoney(quote.shipping, locale) : 'Included'}`,\n ...(quote.tax?.amount ? [`Tax: ${formatMoney(quote.tax, locale)}`] : []),\n `Total: ${formatMoney(quote.total, locale)}`,\n ]\n : []),\n ]\n}\n\nconst button = (url: string, label: string) =>\n `<p style=\"margin:24px 0 0\"><a href=\"${escapeHtml(url)}\" style=\"display:inline-block;background:#0f6fff;color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:700;font-size:14px\">${escapeHtml(label)}</a></p>`\n\n/** To the buyer: we have your request. Carries the link that is their way back in — no account needed. */\nexport function quoteReceivedMail(quote: StoreQuote, ctx: MailContext): RenderedMail {\n const { config, siteUrl } = ctx\n const number = quoteNumber(quote, config)\n const url = quoteUrl(quote, siteUrl)\n const intro = `Thanks — we have your request for a quote and will email you when it is priced. Keep this email: the link below shows the quote's status at any time.`\n const html = layout(config, `Quote request ${number} received`, `<p style=\"margin:0 0 16px;font-size:14px\">${intro}</p>${quoteLinesTable(quote, config.locale, false)}${button(url, 'View your quote request')}`, contactFooter(config))\n const text = [`${config.name} — quote request ${number} received`, '', intro, '', ...quoteLinesText(quote, config.locale, false), '', `View your quote request: ${url}`].join('\\n')\n return { subject: `Quote request ${number} received — ${config.name}`, html, text }\n}\n\n/** To the buyer: the quote is priced and waiting for their answer. */\nexport function quoteReadyMail(quote: StoreQuote, ctx: MailContext): RenderedMail {\n const { config, siteUrl } = ctx\n const number = quoteNumber(quote, config)\n const url = quoteUrl(quote, siteUrl)\n const expiry = quote.expiresAt ? ` It is valid until ${new Date(quote.expiresAt).toLocaleDateString(config.locale, { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' })}.` : ''\n const intro = `Your quote is ready.${expiry} Accept it to place the order at these prices, or decline it — either takes one click.`\n const html = layout(\n config,\n `Quote ${number} is ready`,\n `<p style=\"margin:0 0 16px;font-size:14px\">${escapeHtml(intro)}</p>${quoteLinesTable(quote, config.locale, true)}${quote.offerNote ? `<p style=\"margin:16px 0 0;font-size:14px;color:#444;white-space:pre-wrap\">${escapeHtml(quote.offerNote)}</p>` : ''}${button(url, 'Review and accept')}`,\n contactFooter(config)\n )\n const text = [`${config.name} — quote ${number} is ready`, '', intro, '', ...quoteLinesText(quote, config.locale, true), ...(quote.offerNote ? ['', quote.offerNote] : []), '', `Review and accept: ${url}`].join('\\n')\n return { subject: `Quote ${number} is ready — ${config.name}`, html, text }\n}\n\n/** To the merchant: a new request, or the buyer's answer to an offer. */\nexport function merchantQuoteMail(quote: StoreQuote, ctx: MailContext, event: 'requested' | 'accepted' | 'declined', orderNumber?: string | null): RenderedMail {\n const { config } = ctx\n const number = quoteNumber(quote, config)\n const headline =\n event === 'requested'\n ? `New quote request ${number}`\n : event === 'accepted'\n ? `Quote ${number} accepted${orderNumber ? ` — order ${config.orders.numberPrefix}${orderNumber}` : ''}`\n : `Quote ${number} declined`\n const ship = addressLines(quote.shippingAddress)\n const text = [\n headline,\n '',\n ...quoteLinesText(quote, config.locale, event !== 'requested'),\n '',\n `Customer: ${quote.customer.name ?? '(not given)'}`,\n ...(quote.company ? [`Company: ${quote.company}`] : []),\n `Email: ${quote.customer.email}`,\n `Phone: ${quote.customer.phone ?? '(not given)'}`,\n ...(ship.length ? ['', 'Ship to:', ...ship] : []),\n ...(quote.message ? ['', `Message: ${quote.message}`] : []),\n ...(event === 'declined' && quote.declineReason ? ['', `Reason: ${quote.declineReason}`] : []),\n '',\n `Quote id: ${quote.id}`,\n ].join('\\n')\n const html = layout(config, headline, `<pre style=\"font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:pre-wrap;margin:0\">${escapeHtml(text)}</pre>`, escapeHtml(config.name))\n return { subject: `${headline} — ${config.name}`, html, text }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
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 catalog 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 * catalog. 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":"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","names":[]}
|
|
@@ -134,13 +134,14 @@ var CLIENT_ID = /^[a-z0-9][a-z0-9._-]{1,62}$/;
|
|
|
134
134
|
function connectClientName(clientId) {
|
|
135
135
|
return KNOWN_CONNECT_CLIENTS[clientId] ?? clientId;
|
|
136
136
|
}
|
|
137
|
-
var CONNECT_SCOPES = ["orders", "payments", "fulfillments", "inventory", "catalog"];
|
|
137
|
+
var CONNECT_SCOPES = ["orders", "payments", "fulfillments", "inventory", "catalog", "quotes"];
|
|
138
138
|
var SCOPE_DESCRIPTIONS = {
|
|
139
139
|
orders: "Read orders, stamp its own order number on them and cancel them",
|
|
140
140
|
payments: "Mark orders paid, refunded or failed (for payments taken outside this store)",
|
|
141
141
|
fulfillments: "Push shipments and tracking to orders",
|
|
142
142
|
inventory: "Update available stock",
|
|
143
|
-
catalog: "Read and update products and categories"
|
|
143
|
+
catalog: "Read and update products and categories",
|
|
144
|
+
quotes: "Read quote requests and send buyers priced quotes they can accept as orders"
|
|
144
145
|
};
|
|
145
146
|
var toConnection = (r) => ({
|
|
146
147
|
id: r.id,
|
|
@@ -313,4 +314,4 @@ export {
|
|
|
313
314
|
isLocalHost,
|
|
314
315
|
ConnectionsService
|
|
315
316
|
};
|
|
316
|
-
//# sourceMappingURL=chunk-
|
|
317
|
+
//# sourceMappingURL=chunk-RUUUZ2EL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 /** Merchant address that hears about quote requests and answers; falls back to ORDER_NOTIFICATION_EMAIL. */\n QUOTE_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', 'quotes'] 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 quotes: 'Read quote requests and send buyers priced quotes they can accept as orders',\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":[]}
|
|
@@ -171,9 +171,66 @@ interface OrderEvent {
|
|
|
171
171
|
data: Record<string, unknown> | null;
|
|
172
172
|
createdAt: string;
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* requested → quoted → accepted (an order exists) | declined | expired → (re-quoted).
|
|
176
|
+
* `cancelled` is the merchant or the operations system withdrawing it.
|
|
177
|
+
*/
|
|
178
|
+
type QuoteStatus = 'requested' | 'quoted' | 'accepted' | 'declined' | 'expired' | 'cancelled';
|
|
179
|
+
interface QuoteLine {
|
|
180
|
+
id: string;
|
|
181
|
+
sku: string;
|
|
182
|
+
name: string;
|
|
183
|
+
quantity: number;
|
|
184
|
+
/** The catalog price when the buyer asked, for reference; null for a line the offer added off-catalog. */
|
|
185
|
+
listAmount: Money | null;
|
|
186
|
+
/** The offered price; null until the quote is priced. */
|
|
187
|
+
unitAmount: Money | null;
|
|
188
|
+
totalAmount: Money | null;
|
|
189
|
+
image?: string;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A request for quote and, once priced, the offer. The buyer needs no account: a guest's quote is reached
|
|
193
|
+
* through the access-key link emailed to them, a signed-in buyer's also through /account.
|
|
194
|
+
*/
|
|
195
|
+
interface StoreQuote {
|
|
196
|
+
id: string;
|
|
197
|
+
/** Human quote number without the prefix, e.g. "1001" (shown as Q1001). */
|
|
198
|
+
number: string;
|
|
199
|
+
status: QuoteStatus;
|
|
200
|
+
currency: string;
|
|
201
|
+
customer: OrderCustomer;
|
|
202
|
+
company: string | null;
|
|
203
|
+
/** What the buyer wrote with the request: lead time, use, delivery constraints. */
|
|
204
|
+
message: string | null;
|
|
205
|
+
shippingAddress: Address | null;
|
|
206
|
+
lines: QuoteLine[];
|
|
207
|
+
/** Null until priced. */
|
|
208
|
+
subtotal: Money | null;
|
|
209
|
+
shipping: Money | null;
|
|
210
|
+
tax: Money | null;
|
|
211
|
+
total: Money | null;
|
|
212
|
+
/** The merchant's note with the offer (terms, lead time). */
|
|
213
|
+
offerNote: string | null;
|
|
214
|
+
/** When the offer lapses; null for an open-ended offer or an unpriced request. */
|
|
215
|
+
expiresAt: string | null;
|
|
216
|
+
/** The operations system's own quote number, stamped back through the Store API. */
|
|
217
|
+
externalReference: string | null;
|
|
218
|
+
/** The order placed when the buyer accepted. */
|
|
219
|
+
orderId: string | null;
|
|
220
|
+
declineReason: string | null;
|
|
221
|
+
metadata: Record<string, string>;
|
|
222
|
+
/** Secret in the buyer's emailed link; never exposed through the Store API. */
|
|
223
|
+
accessKey: string;
|
|
224
|
+
createdAt: string;
|
|
225
|
+
updatedAt: string;
|
|
226
|
+
quotedAt: string | null;
|
|
227
|
+
acceptedAt: string | null;
|
|
228
|
+
declinedAt: string | null;
|
|
229
|
+
cancelledAt: string | null;
|
|
230
|
+
}
|
|
174
231
|
|
|
175
232
|
/**
|
|
176
|
-
* An in-memory snapshot of the
|
|
233
|
+
* An in-memory snapshot of the catalog with the tree and the lookups the
|
|
177
234
|
* pages need. Built from plain data — the store service builds one from the
|
|
178
235
|
* database per request (cached briefly per isolate); tests build one from
|
|
179
236
|
* fixtures.
|
|
@@ -219,7 +276,7 @@ declare class Catalog {
|
|
|
219
276
|
interface RoutePatterns {
|
|
220
277
|
product: string;
|
|
221
278
|
category: string;
|
|
222
|
-
/** Where the full
|
|
279
|
+
/** Where the full catalog lives. */
|
|
223
280
|
shop: string;
|
|
224
281
|
}
|
|
225
282
|
declare const DEFAULT_ROUTES: RoutePatterns;
|
|
@@ -406,6 +463,17 @@ interface StorefrontInput {
|
|
|
406
463
|
*/
|
|
407
464
|
numberStart?: number;
|
|
408
465
|
};
|
|
466
|
+
/** Request-for-quote (see store/quotes.ts). */
|
|
467
|
+
quotes?: {
|
|
468
|
+
/** Prefix for quote numbers: 'Q' → Q1001 (default 'Q'). */
|
|
469
|
+
numberPrefix?: string;
|
|
470
|
+
/** The lowest number a new quote may take (default 1001). */
|
|
471
|
+
numberStart?: number;
|
|
472
|
+
/** Per line. Quotes are for volume, so this is far above checkout's limit (default 100000). */
|
|
473
|
+
maxQuantity?: number;
|
|
474
|
+
/** How many quote requests one email address may send per hour before it is told to wait (default 5). */
|
|
475
|
+
maxRequestsPerHour?: number;
|
|
476
|
+
};
|
|
409
477
|
/**
|
|
410
478
|
* Discount codes the store prices itself (see discounts.ts). A code with a
|
|
411
479
|
* `partner` attributes the order to that partner and snapshots the
|
|
@@ -420,7 +488,7 @@ interface StorefrontInput {
|
|
|
420
488
|
/** `?code=` links that apply a code for a while. Defaults: param "code", 30 days. */
|
|
421
489
|
referral?: Partial<ReferralSettings>;
|
|
422
490
|
}
|
|
423
|
-
interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | 'checkout' | 'inventory' | 'orders' | 'discounts' | 'quantityDiscounts' | 'partners' | 'referral'> {
|
|
491
|
+
interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | 'checkout' | 'inventory' | 'orders' | 'quotes' | 'discounts' | 'quantityDiscounts' | 'partners' | 'referral'> {
|
|
424
492
|
locale: string;
|
|
425
493
|
routes: Routes;
|
|
426
494
|
checkout: {
|
|
@@ -435,6 +503,12 @@ interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | '
|
|
|
435
503
|
numberPrefix: string;
|
|
436
504
|
numberStart: number;
|
|
437
505
|
};
|
|
506
|
+
quotes: {
|
|
507
|
+
numberPrefix: string;
|
|
508
|
+
numberStart: number;
|
|
509
|
+
maxQuantity: number;
|
|
510
|
+
maxRequestsPerHour: number;
|
|
511
|
+
};
|
|
438
512
|
discounts: DiscountCode[];
|
|
439
513
|
quantityDiscounts: QuantityDiscount[];
|
|
440
514
|
partners: PartnerProfile[];
|
|
@@ -446,4 +520,4 @@ interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | '
|
|
|
446
520
|
*/
|
|
447
521
|
declare function defineStorefront(input: StorefrontInput): StorefrontConfig;
|
|
448
522
|
|
|
449
|
-
export { type Address as A,
|
|
523
|
+
export { type Address as A, type StorefrontInput as B, Catalog as C, DEFAULT_ROUTES as D, commissionAmount as E, type Fulfillment as F, defineRoutes as G, defineStorefront as H, discountAmount as I, findDiscount as J, flatRate as K, freeShipping as L, type Money as M, hasInternationalRegion as N, type Order as O, type PartnerProfile as P, type QuoteStatus as Q, type ReferralSettings as R, type StoreQuote as S, normalizeDiscountCode as T, publicDiscount as U, quantityLabel as V, quantityTier as W, regionForCountry as X, shippingForCountry as Y, shippingForRegion as Z, type Availability as a, type AvailabilityStatus as b, type Category as c, type CategoryNode as d, type Customer as e, DISCOUNT_COOKIE as f, type DiscountCode as g, type FulfillmentLine as h, type OrderCustomer as i, type OrderEvent as j, type OrderLine as k, type OrderPayment as l, type OrderStatus as m, type PaymentStatus as n, type Product as o, type ProductImage as p, type PublicDiscount as q, type QuantityDiscount as r, type QuoteLine as s, type ResolvedShipping as t, type RoutePatterns as u, type Routes as v, type ShippingMethod as w, type ShippingOption as x, type ShippingRegion as y, type StorefrontConfig as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as Money } from './config-
|
|
2
|
-
export { A as Address, a as Availability, b as AvailabilityStatus, C as Catalog, c as Category, d as CategoryNode, e as Customer, D as DEFAULT_ROUTES, f as DISCOUNT_COOKIE, g as DiscountCode, F as Fulfillment, h as FulfillmentLine, O as Order, i as OrderCustomer, j as OrderEvent, k as OrderLine, l as OrderPayment, m as OrderStatus, P as PartnerProfile, n as PaymentStatus, o as Product, p as ProductImage, q as PublicDiscount,
|
|
1
|
+
import { M as Money } from './config-CCxTka_z.js';
|
|
2
|
+
export { A as Address, a as Availability, b as AvailabilityStatus, C as Catalog, c as Category, d as CategoryNode, e as Customer, D as DEFAULT_ROUTES, f as DISCOUNT_COOKIE, g as DiscountCode, F as Fulfillment, h as FulfillmentLine, O as Order, i as OrderCustomer, j as OrderEvent, k as OrderLine, l as OrderPayment, m as OrderStatus, P as PartnerProfile, n as PaymentStatus, o as Product, p as ProductImage, q as PublicDiscount, r as QuantityDiscount, s as QuoteLine, Q as QuoteStatus, R as ReferralSettings, t as ResolvedShipping, u as RoutePatterns, v as Routes, w as ShippingMethod, x as ShippingOption, y as ShippingRegion, S as StoreQuote, z as StorefrontConfig, B as StorefrontInput, E as commissionAmount, G as defineRoutes, H as defineStorefront, I as discountAmount, J as findDiscount, K as flatRate, L as freeShipping, N as hasInternationalRegion, T as normalizeDiscountCode, U as publicDiscount, V as quantityLabel, W as quantityTier, X as regionForCountry, Y as shippingForCountry, Z as shippingForRegion } from './config-CCxTka_z.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Format minor units for display. Whole amounts drop the cents ($1,099), which
|
|
@@ -16,7 +16,7 @@ declare function slugify(input: string): string;
|
|
|
16
16
|
/**
|
|
17
17
|
* A cart is client-side state — lines the customer has chosen, with a snapshot
|
|
18
18
|
* of what they saw (name, price) so the cart page can render without the
|
|
19
|
-
*
|
|
19
|
+
* catalog. It is never trusted for money: checkout re-prices every SKU on the
|
|
20
20
|
* server. These are pure functions; the browser store wraps them.
|
|
21
21
|
*/
|
|
22
22
|
interface CartLine {
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
slugify,
|
|
31
31
|
validateDiscounts,
|
|
32
32
|
validateQuantityDiscounts
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-NM5YP764.js";
|
|
34
34
|
import {
|
|
35
35
|
addMoney,
|
|
36
36
|
formatMoney,
|
|
@@ -103,6 +103,12 @@ function defineStorefront(input) {
|
|
|
103
103
|
numberPrefix: input.orders?.numberPrefix ?? "",
|
|
104
104
|
numberStart: Math.max(1, Math.trunc(input.orders?.numberStart ?? 1001))
|
|
105
105
|
},
|
|
106
|
+
quotes: {
|
|
107
|
+
numberPrefix: input.quotes?.numberPrefix ?? "Q",
|
|
108
|
+
numberStart: Math.max(1, Math.trunc(input.quotes?.numberStart ?? 1001)),
|
|
109
|
+
maxQuantity: Math.max(1, Math.trunc(input.quotes?.maxQuantity ?? 1e5)),
|
|
110
|
+
maxRequestsPerHour: Math.max(1, Math.trunc(input.quotes?.maxRequestsPerHour ?? 5))
|
|
111
|
+
},
|
|
106
112
|
discounts: input.discounts ?? [],
|
|
107
113
|
quantityDiscounts: input.quantityDiscounts ?? [],
|
|
108
114
|
partners: input.partners ?? [],
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/routes.ts","../src/config.ts","../src/http.ts"],"sourcesContent":["import type { CategoryNode, Product } from './types.js'\nimport type { Catalog } from './catalog.js'\n\n/**\n * URL patterns. Tokens:\n * :slug product slug\n * :sku product SKU\n * :path category slug path joined with '/', e.g. toyota/tacoma-95-04\n *\n * Defaults are conventional; a migrated store sets them to match its legacy\n * URLs so nothing indexed ever moves. Patterns should start and end with '/'.\n */\nexport interface RoutePatterns {\n product: string\n category: string\n /** Where the full catalogue lives. */\n shop: string\n}\n\nexport const DEFAULT_ROUTES: RoutePatterns = {\n product: '/products/:slug/',\n category: '/collections/:path/',\n shop: '/shop/',\n}\n\nexport interface Routes {\n product(product: Product, catalog: Catalog): string\n category(node: CategoryNode): string\n shop(): string\n patterns: RoutePatterns\n}\n\nexport function defineRoutes(partial: Partial<RoutePatterns> = {}): Routes {\n const patterns: RoutePatterns = { ...DEFAULT_ROUTES, ...partial }\n\n const fill = (pattern: string, vars: Record<string, string>) =>\n pattern.replace(/:(\\w+)/g, (_, key: string) => {\n const v = vars[key]\n if (v === undefined) throw new Error(`Route pattern \"${pattern}\" needs :${key}`)\n return v\n })\n\n return {\n patterns,\n product(product, catalog) {\n const crumbs = catalog.breadcrumbs(product)\n return fill(patterns.product, {\n slug: product.slug,\n sku: encodeURIComponent(product.sku),\n path: crumbs.map((c) => c.slug).join('/'),\n })\n },\n category(node) {\n return fill(patterns.category, { path: node.path.join('/') })\n },\n shop() {\n return patterns.shop\n },\n }\n}\n","import { defineRoutes, type RoutePatterns, type Routes } from './routes'\nimport type { ShippingOption } from './shipping'\nimport { validateDiscounts, validateQuantityDiscounts, type DiscountCode, type PartnerProfile, type QuantityDiscount, type ReferralSettings } from './discounts'\n\nexport interface StorefrontInput {\n /** Store name, used in titles, emails and the Store API health check. */\n name: string\n /** Canonical public origin, e.g. https://www.example.com (no trailing slash). */\n url: string\n /** ISO 4217, lower case: 'usd'. */\n currency: string\n /** BCP 47 for formatting: 'en-US'. */\n locale?: string\n routes?: Partial<RoutePatterns>\n /** At least one option. Options that name `countries` are only offered to those countries. */\n shipping: ShippingOption[]\n /** ISO 3166-1 alpha-2 codes the store ships to. */\n shipToCountries: string[]\n contact?: {\n phone?: string\n phoneE164?: string\n email?: string\n }\n checkout?: {\n /** Per line. */\n maxQuantity?: number\n allowPromotionCodes?: boolean\n collectPhone?: boolean\n }\n inventory?: {\n /** `available <= lowStockThreshold` shows as low stock. */\n lowStockThreshold?: number\n }\n orders?: {\n /** Prefix for human order numbers: 'VLD-' → VLD-1001. Shown on pages, emails and receipts, and sent to SKU.io. */\n numberPrefix?: string\n /**\n * The lowest number a new order may take (default 1001). New orders take the next number above the highest\n * order on file, or this, whichever is larger: set it above numbers an earlier platform used (4200 after\n * WooCommerce's VLD4154) so a new order never repeats an old one.\n */\n numberStart?: number\n }\n /**\n * Discount codes the store prices itself (see discounts.ts). A code with a\n * `partner` attributes the order to that partner and snapshots the\n * commission owed. Prefer these over processor promotion codes: they work\n * under every payment adapter and land on the order.\n */\n discounts?: DiscountCode[]\n /** Percent off by units in the order (see discounts.ts). Never stacks with a code: the larger saving wins. */\n quantityDiscounts?: QuantityDiscount[]\n /** Profiles for the partners codes name: welcome banner + portal sign-in. */\n partners?: PartnerProfile[]\n /** `?code=` links that apply a code for a while. Defaults: param \"code\", 30 days. */\n referral?: Partial<ReferralSettings>\n}\n\nexport interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | 'checkout' | 'inventory' | 'orders' | 'discounts' | 'quantityDiscounts' | 'partners' | 'referral'> {\n locale: string\n routes: Routes\n checkout: {\n maxQuantity: number\n allowPromotionCodes: boolean\n collectPhone: boolean\n }\n inventory: {\n lowStockThreshold: number\n }\n orders: {\n numberPrefix: string\n numberStart: number\n }\n discounts: DiscountCode[]\n quantityDiscounts: QuantityDiscount[]\n partners: PartnerProfile[]\n referral: ReferralSettings\n}\n\n/**\n * Validate and fill in a store's configuration. Every store has exactly one\n * of these, exported from storefront.config.ts.\n */\nexport function defineStorefront(input: StorefrontInput): StorefrontConfig {\n if (!/^https?:\\/\\//.test(input.url)) throw new Error(`storefront.url must be absolute, got ${input.url}`)\n if (input.url.endsWith('/')) throw new Error('storefront.url must not end with a slash')\n if (!input.shipping.length) throw new Error('storefront.shipping needs at least one option')\n if (!input.shipToCountries.length) throw new Error('storefront.shipToCountries needs at least one country')\n validateDiscounts(input.discounts ?? [], input.currency)\n validateQuantityDiscounts(input.quantityDiscounts ?? [])\n const partnerIds = new Set<string>()\n for (const p of input.partners ?? []) {\n if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(p.id)) throw new Error(`partner id \"${p.id}\" must be lower-case letters, digits and dashes`)\n if (partnerIds.has(p.id)) throw new Error(`partner ${p.id} is defined twice`)\n if (p.email && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(p.email)) throw new Error(`partner ${p.id}: email is not an address`)\n partnerIds.add(p.id)\n }\n const referralParam = input.referral?.param ?? 'code'\n if (!/^[a-z][a-z0-9_]{0,31}$/.test(referralParam)) throw new Error(`storefront.referral.param \"${referralParam}\" must be a short lower-case query parameter`)\n\n return {\n ...input,\n locale: input.locale ?? 'en-US',\n routes: defineRoutes(input.routes),\n checkout: {\n maxQuantity: input.checkout?.maxQuantity ?? 10,\n allowPromotionCodes: input.checkout?.allowPromotionCodes ?? true,\n collectPhone: input.checkout?.collectPhone ?? true,\n },\n inventory: {\n lowStockThreshold: input.inventory?.lowStockThreshold ?? 3,\n },\n orders: {\n numberPrefix: input.orders?.numberPrefix ?? '',\n numberStart: Math.max(1, Math.trunc(input.orders?.numberStart ?? 1001)),\n },\n discounts: input.discounts ?? [],\n quantityDiscounts: input.quantityDiscounts ?? [],\n partners: input.partners ?? [],\n referral: {\n param: referralParam,\n cookieDays: input.referral?.cookieDays ?? 30,\n },\n }\n}\n","/**\n * Framework-agnostic request helpers for browser form endpoints. The store\n * disables Astro's global origin check so the bearer-authenticated Store API\n * (machine traffic, no Origin header) is never rejected; these keep the\n * browser form POSTs same-origin instead.\n */\n\n/**\n * True when the request is a same-origin browser navigation/form post, or a\n * non-browser call (no Origin, no Sec-Fetch-Site — e.g. the Store API). False\n * only when a cross-site browser context is explicit. Use it to guard mutating\n * form handlers: `if (!sameOrigin(request, url)) return reject`.\n */\nexport function sameOrigin(request: Request, url: URL): boolean {\n const site = request.headers.get('sec-fetch-site')\n if (site && site !== 'same-origin' && site !== 'none') return false\n const origin = request.headers.get('origin')\n if (origin && origin !== url.origin) return false\n return true\n}\n\n/** The caller's address, from Cloudflare's header first, then a proxy's. */\nexport function clientAddress(request: Request): string {\n return request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/routes.ts","../src/config.ts","../src/http.ts"],"sourcesContent":["import type { CategoryNode, Product } from './types.js'\nimport type { Catalog } from './catalog.js'\n\n/**\n * URL patterns. Tokens:\n * :slug product slug\n * :sku product SKU\n * :path category slug path joined with '/', e.g. toyota/tacoma-95-04\n *\n * Defaults are conventional; a migrated store sets them to match its legacy\n * URLs so nothing indexed ever moves. Patterns should start and end with '/'.\n */\nexport interface RoutePatterns {\n product: string\n category: string\n /** Where the full catalog lives. */\n shop: string\n}\n\nexport const DEFAULT_ROUTES: RoutePatterns = {\n product: '/products/:slug/',\n category: '/collections/:path/',\n shop: '/shop/',\n}\n\nexport interface Routes {\n product(product: Product, catalog: Catalog): string\n category(node: CategoryNode): string\n shop(): string\n patterns: RoutePatterns\n}\n\nexport function defineRoutes(partial: Partial<RoutePatterns> = {}): Routes {\n const patterns: RoutePatterns = { ...DEFAULT_ROUTES, ...partial }\n\n const fill = (pattern: string, vars: Record<string, string>) =>\n pattern.replace(/:(\\w+)/g, (_, key: string) => {\n const v = vars[key]\n if (v === undefined) throw new Error(`Route pattern \"${pattern}\" needs :${key}`)\n return v\n })\n\n return {\n patterns,\n product(product, catalog) {\n const crumbs = catalog.breadcrumbs(product)\n return fill(patterns.product, {\n slug: product.slug,\n sku: encodeURIComponent(product.sku),\n path: crumbs.map((c) => c.slug).join('/'),\n })\n },\n category(node) {\n return fill(patterns.category, { path: node.path.join('/') })\n },\n shop() {\n return patterns.shop\n },\n }\n}\n","import { defineRoutes, type RoutePatterns, type Routes } from './routes'\nimport type { ShippingOption } from './shipping'\nimport { validateDiscounts, validateQuantityDiscounts, type DiscountCode, type PartnerProfile, type QuantityDiscount, type ReferralSettings } from './discounts'\n\nexport interface StorefrontInput {\n /** Store name, used in titles, emails and the Store API health check. */\n name: string\n /** Canonical public origin, e.g. https://www.example.com (no trailing slash). */\n url: string\n /** ISO 4217, lower case: 'usd'. */\n currency: string\n /** BCP 47 for formatting: 'en-US'. */\n locale?: string\n routes?: Partial<RoutePatterns>\n /** At least one option. Options that name `countries` are only offered to those countries. */\n shipping: ShippingOption[]\n /** ISO 3166-1 alpha-2 codes the store ships to. */\n shipToCountries: string[]\n contact?: {\n phone?: string\n phoneE164?: string\n email?: string\n }\n checkout?: {\n /** Per line. */\n maxQuantity?: number\n allowPromotionCodes?: boolean\n collectPhone?: boolean\n }\n inventory?: {\n /** `available <= lowStockThreshold` shows as low stock. */\n lowStockThreshold?: number\n }\n orders?: {\n /** Prefix for human order numbers: 'VLD-' → VLD-1001. Shown on pages, emails and receipts, and sent to SKU.io. */\n numberPrefix?: string\n /**\n * The lowest number a new order may take (default 1001). New orders take the next number above the highest\n * order on file, or this, whichever is larger: set it above numbers an earlier platform used (4200 after\n * WooCommerce's VLD4154) so a new order never repeats an old one.\n */\n numberStart?: number\n }\n /** Request-for-quote (see store/quotes.ts). */\n quotes?: {\n /** Prefix for quote numbers: 'Q' → Q1001 (default 'Q'). */\n numberPrefix?: string\n /** The lowest number a new quote may take (default 1001). */\n numberStart?: number\n /** Per line. Quotes are for volume, so this is far above checkout's limit (default 100000). */\n maxQuantity?: number\n /** How many quote requests one email address may send per hour before it is told to wait (default 5). */\n maxRequestsPerHour?: number\n }\n /**\n * Discount codes the store prices itself (see discounts.ts). A code with a\n * `partner` attributes the order to that partner and snapshots the\n * commission owed. Prefer these over processor promotion codes: they work\n * under every payment adapter and land on the order.\n */\n discounts?: DiscountCode[]\n /** Percent off by units in the order (see discounts.ts). Never stacks with a code: the larger saving wins. */\n quantityDiscounts?: QuantityDiscount[]\n /** Profiles for the partners codes name: welcome banner + portal sign-in. */\n partners?: PartnerProfile[]\n /** `?code=` links that apply a code for a while. Defaults: param \"code\", 30 days. */\n referral?: Partial<ReferralSettings>\n}\n\nexport interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | 'checkout' | 'inventory' | 'orders' | 'quotes' | 'discounts' | 'quantityDiscounts' | 'partners' | 'referral'> {\n locale: string\n routes: Routes\n checkout: {\n maxQuantity: number\n allowPromotionCodes: boolean\n collectPhone: boolean\n }\n inventory: {\n lowStockThreshold: number\n }\n orders: {\n numberPrefix: string\n numberStart: number\n }\n quotes: {\n numberPrefix: string\n numberStart: number\n maxQuantity: number\n maxRequestsPerHour: number\n }\n discounts: DiscountCode[]\n quantityDiscounts: QuantityDiscount[]\n partners: PartnerProfile[]\n referral: ReferralSettings\n}\n\n/**\n * Validate and fill in a store's configuration. Every store has exactly one\n * of these, exported from storefront.config.ts.\n */\nexport function defineStorefront(input: StorefrontInput): StorefrontConfig {\n if (!/^https?:\\/\\//.test(input.url)) throw new Error(`storefront.url must be absolute, got ${input.url}`)\n if (input.url.endsWith('/')) throw new Error('storefront.url must not end with a slash')\n if (!input.shipping.length) throw new Error('storefront.shipping needs at least one option')\n if (!input.shipToCountries.length) throw new Error('storefront.shipToCountries needs at least one country')\n validateDiscounts(input.discounts ?? [], input.currency)\n validateQuantityDiscounts(input.quantityDiscounts ?? [])\n const partnerIds = new Set<string>()\n for (const p of input.partners ?? []) {\n if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(p.id)) throw new Error(`partner id \"${p.id}\" must be lower-case letters, digits and dashes`)\n if (partnerIds.has(p.id)) throw new Error(`partner ${p.id} is defined twice`)\n if (p.email && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(p.email)) throw new Error(`partner ${p.id}: email is not an address`)\n partnerIds.add(p.id)\n }\n const referralParam = input.referral?.param ?? 'code'\n if (!/^[a-z][a-z0-9_]{0,31}$/.test(referralParam)) throw new Error(`storefront.referral.param \"${referralParam}\" must be a short lower-case query parameter`)\n\n return {\n ...input,\n locale: input.locale ?? 'en-US',\n routes: defineRoutes(input.routes),\n checkout: {\n maxQuantity: input.checkout?.maxQuantity ?? 10,\n allowPromotionCodes: input.checkout?.allowPromotionCodes ?? true,\n collectPhone: input.checkout?.collectPhone ?? true,\n },\n inventory: {\n lowStockThreshold: input.inventory?.lowStockThreshold ?? 3,\n },\n orders: {\n numberPrefix: input.orders?.numberPrefix ?? '',\n numberStart: Math.max(1, Math.trunc(input.orders?.numberStart ?? 1001)),\n },\n quotes: {\n numberPrefix: input.quotes?.numberPrefix ?? 'Q',\n numberStart: Math.max(1, Math.trunc(input.quotes?.numberStart ?? 1001)),\n maxQuantity: Math.max(1, Math.trunc(input.quotes?.maxQuantity ?? 100_000)),\n maxRequestsPerHour: Math.max(1, Math.trunc(input.quotes?.maxRequestsPerHour ?? 5)),\n },\n discounts: input.discounts ?? [],\n quantityDiscounts: input.quantityDiscounts ?? [],\n partners: input.partners ?? [],\n referral: {\n param: referralParam,\n cookieDays: input.referral?.cookieDays ?? 30,\n },\n }\n}\n","/**\n * Framework-agnostic request helpers for browser form endpoints. The store\n * disables Astro's global origin check so the bearer-authenticated Store API\n * (machine traffic, no Origin header) is never rejected; these keep the\n * browser form POSTs same-origin instead.\n */\n\n/**\n * True when the request is a same-origin browser navigation/form post, or a\n * non-browser call (no Origin, no Sec-Fetch-Site — e.g. the Store API). False\n * only when a cross-site browser context is explicit. Use it to guard mutating\n * form handlers: `if (!sameOrigin(request, url)) return reject`.\n */\nexport function sameOrigin(request: Request, url: URL): boolean {\n const site = request.headers.get('sec-fetch-site')\n if (site && site !== 'same-origin' && site !== 'none') return false\n const origin = request.headers.get('origin')\n if (origin && origin !== url.origin) return false\n return true\n}\n\n/** The caller's address, from Cloudflare's header first, then a proxy's. */\nexport function clientAddress(request: Request): string {\n return request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'\n}\n"],"mappings":"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","names":[]}
|