@skuio/storefront 0.5.0 → 0.6.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/db/migrations/0007_payments_scope.sql +8 -0
- package/dist/api.d.ts +17 -6
- package/dist/api.js +25 -8
- package/dist/api.js.map +1 -1
- package/dist/{chunk-QWQA4NBG.js → chunk-X7VXO4QU.js} +13 -7
- package/dist/chunk-X7VXO4QU.js.map +1 -0
- package/dist/{config-HNzNP1O_.d.ts → config-C4YCi7m4.d.ts} +8 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mail.d.ts +1 -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/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 +22 -2
- package/dist/store.js +35 -2
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-QWQA4NBG.js.map +0 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Recording a payment (PATCH /orders/{id} with `payment`) became its own scope, `payments`.
|
|
2
|
+
-- Until now the `orders` scope covered it, and the approval screen said so ("Read orders,
|
|
3
|
+
-- record payments and cancellations"), so every connection already approved for `orders`
|
|
4
|
+
-- keeps that power. New connections must ask for `payments` explicitly.
|
|
5
|
+
UPDATE connections
|
|
6
|
+
SET scopes_json = json_insert(scopes_json, '$[#]', 'payments')
|
|
7
|
+
WHERE EXISTS (SELECT 1 FROM json_each(connections.scopes_json) WHERE value = 'orders')
|
|
8
|
+
AND NOT EXISTS (SELECT 1 FROM json_each(connections.scopes_json) WHERE value = 'payments');
|
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CategoryRecord, ProductRecord, Connection, Store } from './store.js';
|
|
2
|
-
import { O as Order, F as Fulfillment } from './config-
|
|
2
|
+
import { O as Order, F as Fulfillment } from './config-C4YCi7m4.js';
|
|
3
3
|
import './driver-Cr4l6GgG.js';
|
|
4
4
|
import './types-CWreCyxc.js';
|
|
5
5
|
|
|
@@ -121,8 +121,11 @@ interface ApiCategory {
|
|
|
121
121
|
updated_at: string;
|
|
122
122
|
}
|
|
123
123
|
declare function serializeFulfillment(f: Fulfillment): ApiFulfillment;
|
|
124
|
-
/**
|
|
125
|
-
|
|
124
|
+
/**
|
|
125
|
+
* The Store API's view of an order. `access_key` never leaves the store. `number` is the human number the buyer
|
|
126
|
+
* sees, with the store's prefix (VLD4200), so the consumer shows the same number.
|
|
127
|
+
*/
|
|
128
|
+
declare function serializeOrder(o: Order, numberPrefix?: string): ApiOrder;
|
|
126
129
|
/** The Store API's view of a catalogue product — the stored row, not the shopper projection. */
|
|
127
130
|
declare function serializeProduct(p: ProductRecord): ApiProduct;
|
|
128
131
|
declare function serializeCategory(c: CategoryRecord): ApiCategory;
|
|
@@ -150,17 +153,25 @@ type ApiCaller = {
|
|
|
150
153
|
kind: 'connection';
|
|
151
154
|
connection: Connection;
|
|
152
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* Where the Store API is served. `/api/store/v1` is the neutral path for any
|
|
158
|
+
* operations system; `/api/skuio/v1` is the original path SKU.io's Storefront
|
|
159
|
+
* integration calls, kept for good. Same handler, same keys, same responses.
|
|
160
|
+
*/
|
|
161
|
+
declare const STORE_API_BASES: readonly ["/api/store/v1", "/api/skuio/v1"];
|
|
162
|
+
/** @deprecated The SKU.io path; use STORE_API_BASES. */
|
|
153
163
|
declare const STORE_API_BASE = "/api/skuio/v1";
|
|
154
164
|
/**
|
|
155
165
|
* The Store API (`docs/store-api.md`) as one fetch-style handler. Mount it on
|
|
156
|
-
* a catch-all route under `/api/
|
|
166
|
+
* a catch-all route under `/api/store/v1` (and `/api/skuio/v1`, the path SKU.io
|
|
167
|
+
* calls) and hand it every method.
|
|
157
168
|
*/
|
|
158
169
|
declare function handleStoreApi(request: Request, options: StoreApiOptions): Promise<Response>;
|
|
159
170
|
declare function serializeConnection(c: Connection): {
|
|
160
171
|
id: string;
|
|
161
172
|
name: string;
|
|
162
173
|
tenant: string | null;
|
|
163
|
-
scopes: ("orders" | "fulfillments" | "inventory" | "catalog")[];
|
|
174
|
+
scopes: ("orders" | "payments" | "fulfillments" | "inventory" | "catalog")[];
|
|
164
175
|
key_prefix: string | null;
|
|
165
176
|
created_at: string;
|
|
166
177
|
issued_at: string | null;
|
|
@@ -168,4 +179,4 @@ declare function serializeConnection(c: Connection): {
|
|
|
168
179
|
revoked_at: string | null;
|
|
169
180
|
};
|
|
170
181
|
|
|
171
|
-
export { type ApiCaller, type ApiCategory, type ApiFulfillment, type ApiOrder, type ApiProduct, STORE_API_BASE, type StoreApiOptions, ValidationError, handleStoreApi, serializeCategory, serializeConnection, serializeFulfillment, serializeOrder, serializeProduct };
|
|
182
|
+
export { type ApiCaller, type ApiCategory, type ApiFulfillment, type ApiOrder, type ApiProduct, STORE_API_BASE, STORE_API_BASES, type StoreApiOptions, ValidationError, handleStoreApi, serializeCategory, serializeConnection, serializeFulfillment, serializeOrder, serializeProduct };
|
package/dist/api.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
API_KEY_PREFIX,
|
|
3
3
|
StoreError,
|
|
4
4
|
secretsEqual
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-X7VXO4QU.js";
|
|
6
6
|
|
|
7
7
|
// src/api/serialize.ts
|
|
8
8
|
var money = (m) => ({ amount: m.amount, currency: m.currency.toUpperCase() });
|
|
@@ -28,10 +28,10 @@ function serializeFulfillment(f) {
|
|
|
28
28
|
created_at: f.createdAt
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
function serializeOrder(o) {
|
|
31
|
+
function serializeOrder(o, numberPrefix = "") {
|
|
32
32
|
return {
|
|
33
33
|
id: o.id,
|
|
34
|
-
number: o.number
|
|
34
|
+
number: `${numberPrefix}${o.number}`,
|
|
35
35
|
status: o.status,
|
|
36
36
|
payment: { status: o.payment.status, provider: o.payment.provider, reference: o.payment.reference, method_label: o.payment.methodLabel, paid_at: o.payment.paidAt },
|
|
37
37
|
currency: o.currency.toUpperCase(),
|
|
@@ -372,14 +372,16 @@ function validateOrderPatch(body) {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
// src/api/index.ts
|
|
375
|
+
var STORE_API_BASES = ["/api/store/v1", "/api/skuio/v1"];
|
|
375
376
|
var STORE_API_BASE = "/api/skuio/v1";
|
|
376
377
|
var json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...headers } });
|
|
377
378
|
var error = (status, code, message, details = null) => json({ error: { code, message, details } }, status);
|
|
378
379
|
async function handleStoreApi(request, options) {
|
|
379
|
-
const base = (options.basePath ?? STORE_API_BASE).replace(/\/+$/, "");
|
|
380
380
|
const url = new URL(request.url);
|
|
381
|
+
const bases = options.basePath ? [options.basePath] : [...STORE_API_BASES];
|
|
382
|
+
const base = bases.map((b) => b.replace(/\/+$/, "")).find((b) => url.pathname.includes(b));
|
|
383
|
+
if (!base) return error(404, "not_found", "No such endpoint");
|
|
381
384
|
const at = url.pathname.indexOf(base);
|
|
382
|
-
if (at < 0) return error(404, "not_found", "No such endpoint");
|
|
383
385
|
const path = url.pathname.slice(at + base.length).replace(/\/+$/, "") || "/";
|
|
384
386
|
const method = request.method.toUpperCase();
|
|
385
387
|
try {
|
|
@@ -389,6 +391,10 @@ async function handleStoreApi(request, options) {
|
|
|
389
391
|
}
|
|
390
392
|
const caller = await authorized(request, options.apiKey, options.store);
|
|
391
393
|
if (!caller) return error(401, "unauthorized", "A valid bearer key is required: a connection key issued through /admin/connect, or the STORE_API_KEY secret");
|
|
394
|
+
const scope = scopeFor(path);
|
|
395
|
+
if (scope && caller.kind === "connection" && !caller.connection.scopes.includes(scope)) {
|
|
396
|
+
return error(403, "forbidden", `This connection was not approved for "${scope}"; reconnect and approve it at /admin/connect`);
|
|
397
|
+
}
|
|
392
398
|
return await route(method, path, url, request, options.store, caller);
|
|
393
399
|
} catch (e) {
|
|
394
400
|
if (e instanceof ValidationError) return error(422, "validation_failed", e.message, e.details);
|
|
@@ -437,6 +443,13 @@ async function readJson(request) {
|
|
|
437
443
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ValidationError("Request body must be a JSON object", { body: "is not an object" });
|
|
438
444
|
return parsed;
|
|
439
445
|
}
|
|
446
|
+
function scopeFor(path) {
|
|
447
|
+
const first = path.split("/").filter(Boolean)[0];
|
|
448
|
+
if (first === "orders") return /^\/orders\/[^/]+\/fulfillments$/.test(path) ? "fulfillments" : "orders";
|
|
449
|
+
if (first === "inventory") return "inventory";
|
|
450
|
+
if (first === "products" || first === "categories") return "catalog";
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
440
453
|
async function route(method, path, url, request, store, caller) {
|
|
441
454
|
const segments = path.split("/").filter(Boolean);
|
|
442
455
|
if (path === "/health" && method === "GET") {
|
|
@@ -462,18 +475,21 @@ async function route(method, path, url, request, store, caller) {
|
|
|
462
475
|
if (updatedSince && Number.isNaN(Date.parse(updatedSince))) throw new ValidationError("Invalid query", { updated_since: "must be an ISO-8601 timestamp" });
|
|
463
476
|
await store.orders.expireAbandoned();
|
|
464
477
|
const page = await store.orders.changedSince(updatedSince ? new Date(updatedSince).toISOString() : null, cursor, limit);
|
|
465
|
-
return json({ data: page.orders.map(serializeOrder), next_cursor: page.nextCursor });
|
|
478
|
+
return json({ data: page.orders.map((o) => serializeOrder(o, store.config.orders.numberPrefix)), next_cursor: page.nextCursor });
|
|
466
479
|
}
|
|
467
480
|
const id = segments[1] ? decodeURIComponent(segments[1]) : "";
|
|
468
481
|
if (segments.length === 2 && id) {
|
|
469
|
-
if (method === "GET") return json({ data: serializeOrder(await store.orders.require(id)) });
|
|
482
|
+
if (method === "GET") return json({ data: serializeOrder(await store.orders.require(id), store.config.orders.numberPrefix) });
|
|
470
483
|
if (method === "PATCH") {
|
|
471
484
|
const patch = validateOrderPatch(await readJson(request));
|
|
485
|
+
if (patch.payment && caller.kind === "connection" && !caller.connection.scopes.includes("payments")) {
|
|
486
|
+
return error(403, "forbidden", 'This connection was not approved for "payments"; reconnect and approve it at /admin/connect');
|
|
487
|
+
}
|
|
472
488
|
let order = await store.orders.require(id);
|
|
473
489
|
if (patch.external_reference !== void 0) order = await store.orders.setExternalReference(id, patch.external_reference);
|
|
474
490
|
if (patch.payment) order = await store.orders.setPaymentStatus(id, patch.payment);
|
|
475
491
|
if (patch.status === "cancelled") order = await store.orders.cancel(id, "Cancelled by the operations system");
|
|
476
|
-
return json({ data: serializeOrder(order) });
|
|
492
|
+
return json({ data: serializeOrder(order, store.config.orders.numberPrefix) });
|
|
477
493
|
}
|
|
478
494
|
}
|
|
479
495
|
if (segments.length === 3 && id && segments[2] === "fulfillments" && method === "POST") {
|
|
@@ -517,6 +533,7 @@ async function route(method, path, url, request, store, caller) {
|
|
|
517
533
|
}
|
|
518
534
|
export {
|
|
519
535
|
STORE_API_BASE,
|
|
536
|
+
STORE_API_BASES,
|
|
520
537
|
ValidationError,
|
|
521
538
|
handleStoreApi,
|
|
522
539
|
serializeCategory,
|
package/dist/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/serialize.ts","../src/api/validate.ts","../src/api/index.ts"],"sourcesContent":["import type { CategoryRecord, ProductRecord } from '../store/catalog'\nimport type { Address, Fulfillment, Money, Order } from '../types'\n\nexport interface ApiMoney {\n amount: number\n currency: string\n}\n\nexport interface ApiAddress {\n name: string | null\n line1: string | null\n line2: string | null\n city: string | null\n state: string | null\n postal_code: string | null\n country: string | null\n}\n\nexport interface ApiFulfillment {\n id: string\n external_id: string\n carrier: string | null\n service: string | null\n tracking_number: string | null\n tracking_url: string | null\n shipped_at: string | null\n lines: { sku: string; quantity: number }[]\n created_at: string\n}\n\nexport interface ApiOrder {\n id: string\n number: string\n status: Order['status']\n payment: { status: Order['payment']['status']; provider: string | null; reference: string | null; method_label: string | null; paid_at: string | null }\n currency: string\n subtotal: ApiMoney\n shipping: ApiMoney\n tax: ApiMoney\n discount: ApiMoney\n /** The store discount code the buyer used, upper case. Partner and commission stay in the store. */\n discount_code: string | null\n total: ApiMoney\n customer: { id: string; email: string; name: string | null; phone: string | null }\n shipping_address: ApiAddress\n billing_address: ApiAddress | null\n shipping_method: { code: string; name: string } | null\n lines: { id: string; sku: string; name: string; quantity: number; unit_amount: ApiMoney; total_amount: ApiMoney; fulfilled_quantity: number }[]\n fulfillments: ApiFulfillment[]\n external_reference: string | null\n notes: string | null\n metadata: Record<string, string>\n created_at: string\n updated_at: string\n paid_at: string | null\n cancelled_at: string | null\n}\n\nexport interface ApiProduct {\n sku: string\n name: string\n slug: string\n description_html: string | null\n short_description_html: string | null\n price: ApiMoney\n compare_at: ApiMoney | null\n attributes: Record<string, string>\n active: boolean\n position: number\n legacy_path: string | null\n categories: { id: string; slug: string | null; name: string | null }[]\n images: { url: string; alt: string | null; position: number }[]\n inventory: { available: number; allow_backorder: boolean; updated_at: string } | null\n created_at: string\n updated_at: string\n}\n\nexport interface ApiCategory {\n id: string\n parent_id: string | null\n slug: string\n name: string\n description_html: string | null\n position: number\n product_count: number\n created_at: string\n updated_at: string\n}\n\nconst money = (m: Money): ApiMoney => ({ amount: m.amount, currency: m.currency.toUpperCase() })\n\nconst address = (a: Address | null): ApiAddress | null =>\n a\n ? {\n name: a.name ?? null,\n line1: a.line1 ?? null,\n line2: a.line2 ?? null,\n city: a.city ?? null,\n state: a.state ?? null,\n postal_code: a.postalCode ?? null,\n country: a.country ?? null,\n }\n : null\n\nexport function serializeFulfillment(f: Fulfillment): ApiFulfillment {\n return {\n id: f.id,\n external_id: f.externalId,\n carrier: f.carrier,\n service: f.service,\n tracking_number: f.trackingNumber,\n tracking_url: f.trackingUrl,\n shipped_at: f.shippedAt,\n lines: f.lines.map((l) => ({ sku: l.sku, quantity: l.quantity })),\n created_at: f.createdAt,\n }\n}\n\n/** The Store API's view of an order. `access_key` never leaves the store. */\nexport function serializeOrder(o: Order): ApiOrder {\n return {\n id: o.id,\n number: o.number,\n status: o.status,\n payment: { status: o.payment.status, provider: o.payment.provider, reference: o.payment.reference, method_label: o.payment.methodLabel, paid_at: o.payment.paidAt },\n currency: o.currency.toUpperCase(),\n subtotal: money(o.subtotal),\n shipping: money(o.shipping),\n tax: money(o.tax),\n discount: money(o.discount),\n discount_code: o.discountCode,\n total: money(o.total),\n customer: { id: o.customer.id, email: o.customer.email, name: o.customer.name, phone: o.customer.phone },\n shipping_address: address(o.shippingAddress) ?? { name: null, line1: null, line2: null, city: null, state: null, postal_code: null, country: null },\n billing_address: address(o.billingAddress),\n shipping_method: o.shippingMethod,\n lines: o.lines.map((l) => ({\n id: l.id,\n sku: l.sku,\n name: l.name,\n quantity: l.quantity,\n unit_amount: money(l.unitAmount),\n total_amount: money(l.totalAmount),\n fulfilled_quantity: l.fulfilledQuantity,\n })),\n fulfillments: o.fulfillments.map(serializeFulfillment),\n external_reference: o.externalReference,\n notes: o.notes,\n metadata: o.metadata,\n created_at: o.createdAt,\n updated_at: o.updatedAt,\n paid_at: o.paidAt,\n cancelled_at: o.cancelledAt,\n }\n}\n\n/** The Store API's view of a catalogue product — the stored row, not the shopper projection. */\nexport function serializeProduct(p: ProductRecord): ApiProduct {\n return {\n sku: p.sku,\n name: p.name,\n slug: p.slug,\n description_html: p.descriptionHtml,\n short_description_html: p.shortDescriptionHtml,\n price: money(p.price),\n compare_at: p.compareAtPrice ? money(p.compareAtPrice) : null,\n attributes: p.attributes,\n active: p.active,\n position: p.position,\n legacy_path: p.legacyPath,\n categories: p.categories.map((c) => ({ id: c.id, slug: c.slug, name: c.name })),\n images: p.images.map((i) => ({ url: i.url, alt: i.alt, position: i.position })),\n inventory: p.inventory ? { available: p.inventory.available, allow_backorder: p.inventory.allowBackorder, updated_at: p.inventory.updatedAt } : null,\n created_at: p.createdAt,\n updated_at: p.updatedAt,\n }\n}\n\nexport function serializeCategory(c: CategoryRecord): ApiCategory {\n return {\n id: c.id,\n parent_id: c.parentId,\n slug: c.slug,\n name: c.name,\n description_html: c.descriptionHtml,\n position: c.position,\n product_count: c.productCount,\n created_at: c.createdAt,\n updated_at: c.updatedAt,\n }\n}\n","import type { CategoryInput, ProductInput } from '../store/catalog'\nimport type { InventoryItemInput } from '../store/inventory'\nimport type { FulfillmentInput } from '../store/orders'\nimport type { Money } from '../types'\n\nexport class ValidationError extends Error {\n readonly details: Record<string, string>\n constructor(message: string, details: Record<string, string>) {\n super(message)\n this.name = 'ValidationError'\n this.details = details\n }\n}\n\ntype Details = Record<string, string>\n\nconst isObject = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v)\nconst isString = (v: unknown): v is string => typeof v === 'string'\nconst isInt = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v)\n\nfunction str(details: Details, obj: Record<string, unknown>, key: string, path: string, opts: { required?: boolean; max?: number; nullable?: boolean } = {}): string | null | undefined {\n const v = obj[key]\n if (v === undefined) {\n if (opts.required) details[path] = 'is required'\n return undefined\n }\n if (v === null) {\n if (opts.required && !opts.nullable) details[path] = 'is required'\n return opts.nullable || !opts.required ? null : undefined\n }\n if (!isString(v)) {\n details[path] = 'must be a string'\n return undefined\n }\n if (opts.required && !v.trim()) details[path] = 'is required'\n if (opts.max && v.length > opts.max) details[path] = `must be at most ${opts.max} characters`\n return v\n}\n\nfunction moneyField(details: Details, v: unknown, path: string, opts: { required?: boolean; nullable?: boolean } = {}): Money | null | undefined {\n if (v === undefined) {\n if (opts.required) details[path] = 'is required'\n return undefined\n }\n if (v === null) {\n if (!opts.nullable) details[path] = 'is required'\n return null\n }\n if (!isObject(v) || !isInt(v.amount) || !isString(v.currency) || v.currency.length !== 3) {\n details[path] = 'must be { amount: integer minor units, currency: ISO 4217 }'\n return undefined\n }\n if (v.amount < 0) details[path] = 'must not be negative'\n return { amount: v.amount, currency: v.currency }\n}\n\nfunction isoDate(details: Details, v: unknown, path: string): string | null | undefined {\n if (v === undefined) return undefined\n if (v === null) return null\n if (!isString(v) || Number.isNaN(Date.parse(v))) {\n details[path] = 'must be an ISO-8601 timestamp'\n return undefined\n }\n return new Date(v).toISOString()\n}\n\nfunction finish(details: Details, message: string): void {\n if (Object.keys(details).length) throw new ValidationError(message, details)\n}\n\nexport interface ListQuery {\n updatedSince: string | null\n cursor: string | null\n limit: number\n active: boolean | null\n}\n\n/**\n * Query string for the catalogue list endpoints. Out-of-range values are a 422\n * rather than a silent clamp, so a caller paging with a bad limit is told.\n */\nexport function validateListQuery(\n params: URLSearchParams,\n opts: { defaultLimit: number; maxLimit: number; allowUpdatedSince?: boolean; allowActive?: boolean }\n): ListQuery {\n const details: Details = {}\n const limitRaw = params.get('limit')\n const limit = limitRaw ? Number(limitRaw) : opts.defaultLimit\n if (limitRaw && (!Number.isInteger(limit) || limit < 1 || limit > opts.maxLimit)) {\n details.limit = `must be an integer from 1 to ${opts.maxLimit}`\n }\n\n const updatedSinceRaw = opts.allowUpdatedSince ? params.get('updated_since') : null\n if (updatedSinceRaw && Number.isNaN(Date.parse(updatedSinceRaw))) details.updated_since = 'must be an ISO-8601 timestamp'\n\n const activeRaw = opts.allowActive ? params.get('active') : null\n let active: boolean | null = null\n if (activeRaw !== null) {\n if (activeRaw === 'true') active = true\n else if (activeRaw === 'false') active = false\n else details.active = 'must be true or false'\n }\n\n finish(details, 'Invalid query')\n return {\n updatedSince: updatedSinceRaw ? new Date(updatedSinceRaw).toISOString() : null,\n cursor: params.get('cursor'),\n limit: Number.isInteger(limit) ? limit : opts.defaultLimit,\n active,\n }\n}\n\nexport function validateProducts(body: Record<string, unknown>): ProductInput[] {\n const details: Details = {}\n const list = body.products\n if (!Array.isArray(list)) {\n details.products = 'must be an array'\n finish(details, 'Invalid products payload')\n }\n const items = list as unknown[]\n if (items.length > 200) details.products = 'must have at most 200 items'\n const out: ProductInput[] = []\n const seen = new Set<string>()\n items.forEach((raw, i) => {\n const p = `products.${i}`\n if (!isObject(raw)) {\n details[p] = 'must be an object'\n return\n }\n const sku = str(details, raw, 'sku', `${p}.sku`, { required: true, max: 64 })\n const name = str(details, raw, 'name', `${p}.name`, { required: true, max: 255 })\n const slug = str(details, raw, 'slug', `${p}.slug`, { max: 255, nullable: true })\n const description = str(details, raw, 'description_html', `${p}.description_html`, { nullable: true })\n const short = str(details, raw, 'short_description_html', `${p}.short_description_html`, { nullable: true })\n const price = moneyField(details, raw.price, `${p}.price`, { required: true })\n const compare = moneyField(details, raw.compare_at_price, `${p}.compare_at_price`, { nullable: true })\n if (sku && seen.has(sku)) details[`${p}.sku`] = 'appears more than once'\n if (sku) seen.add(sku)\n if (slug && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) details[`${p}.slug`] = 'must be lower-case letters, digits and hyphens'\n\n const images: { url: string; alt?: string | null }[] = []\n if (raw.images !== undefined) {\n if (!Array.isArray(raw.images)) details[`${p}.images`] = 'must be an array'\n else\n raw.images.forEach((img, j) => {\n if (!isObject(img) || !isString(img.url) || !img.url.trim()) details[`${p}.images.${j}.url`] = 'is required'\n else images.push({ url: img.url, alt: isString(img.alt) ? img.alt : null })\n })\n }\n const categoryIds: string[] = []\n if (raw.category_ids !== undefined) {\n if (!Array.isArray(raw.category_ids) || !raw.category_ids.every(isString)) details[`${p}.category_ids`] = 'must be an array of strings'\n else categoryIds.push(...(raw.category_ids as string[]))\n }\n let attributes: Record<string, string> = {}\n if (raw.attributes !== undefined) {\n if (!isObject(raw.attributes) || !Object.values(raw.attributes).every(isString)) details[`${p}.attributes`] = 'must be an object of strings'\n else attributes = raw.attributes as Record<string, string>\n }\n if (raw.active !== undefined && typeof raw.active !== 'boolean') details[`${p}.active`] = 'must be a boolean'\n if (raw.position !== undefined && !isInt(raw.position)) details[`${p}.position`] = 'must be an integer'\n\n if (sku && name && price) {\n out.push({\n sku: sku.trim(),\n name: name.trim(),\n slug: slug ?? null,\n description_html: description ?? null,\n short_description_html: short ?? null,\n price,\n compare_at_price: compare ?? null,\n images,\n category_ids: categoryIds,\n attributes,\n active: raw.active === undefined ? true : (raw.active as boolean),\n ...(isInt(raw.position) ? { position: raw.position } : {}),\n })\n }\n })\n finish(details, 'Invalid products payload')\n return out\n}\n\nexport function validateCategories(body: Record<string, unknown>): CategoryInput[] {\n const details: Details = {}\n const list = body.categories\n if (!Array.isArray(list)) {\n details.categories = 'must be an array'\n finish(details, 'Invalid categories payload')\n }\n const items = list as unknown[]\n if (items.length > 500) details.categories = 'must have at most 500 items'\n const out: CategoryInput[] = []\n const seen = new Set<string>()\n items.forEach((raw, i) => {\n const p = `categories.${i}`\n if (!isObject(raw)) {\n details[p] = 'must be an object'\n return\n }\n const id = str(details, raw, 'id', `${p}.id`, { required: true, max: 64 })\n const name = str(details, raw, 'name', `${p}.name`, { required: true, max: 255 })\n const slug = str(details, raw, 'slug', `${p}.slug`, { max: 255, nullable: true })\n const parent = str(details, raw, 'parent_id', `${p}.parent_id`, { max: 64, nullable: true })\n const description = str(details, raw, 'description_html', `${p}.description_html`, { nullable: true })\n if (id && seen.has(id)) details[`${p}.id`] = 'appears more than once'\n if (id) seen.add(id)\n if (id && parent && parent === id) details[`${p}.parent_id`] = 'cannot be the category itself'\n if (slug && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) details[`${p}.slug`] = 'must be lower-case letters, digits and hyphens'\n let image: { url: string; alt?: string | null } | null = null\n if (raw.image !== undefined && raw.image !== null) {\n if (!isObject(raw.image) || !isString(raw.image.url)) details[`${p}.image`] = 'must be { url, alt? } or null'\n else image = { url: raw.image.url, alt: isString(raw.image.alt) ? raw.image.alt : null }\n }\n if (raw.position !== undefined && !isInt(raw.position)) details[`${p}.position`] = 'must be an integer'\n if (id && name) {\n out.push({ id: id.trim(), name: name.trim(), slug: slug ?? null, parent_id: parent ?? null, description_html: description ?? null, image, ...(isInt(raw.position) ? { position: raw.position } : {}) })\n }\n })\n finish(details, 'Invalid categories payload')\n return out\n}\n\nexport function validateInventory(body: Record<string, unknown>): InventoryItemInput[] {\n const details: Details = {}\n const list = body.items\n if (!Array.isArray(list)) {\n details.items = 'must be an array'\n finish(details, 'Invalid inventory payload')\n }\n const items = list as unknown[]\n if (items.length > 500) details.items = 'must have at most 500 items'\n const out: InventoryItemInput[] = []\n items.forEach((raw, i) => {\n const p = `items.${i}`\n if (!isObject(raw)) {\n details[p] = 'must be an object'\n return\n }\n const sku = str(details, raw, 'sku', `${p}.sku`, { required: true, max: 64 })\n if (!isInt(raw.available)) details[`${p}.available`] = 'must be an integer'\n else if (raw.available < 0) details[`${p}.available`] = 'must be a non-negative integer'\n if (raw.allow_backorder !== undefined && typeof raw.allow_backorder !== 'boolean') details[`${p}.allow_backorder`] = 'must be a boolean'\n if (sku && isInt(raw.available) && raw.available >= 0) out.push({ sku: sku.trim(), available: raw.available, allow_backorder: raw.allow_backorder === true })\n })\n finish(details, 'Invalid inventory payload')\n return out\n}\n\nexport function validateFulfillment(body: Record<string, unknown>): FulfillmentInput {\n const details: Details = {}\n const externalId = str(details, body, 'external_id', 'external_id', { required: true, max: 64 })\n const carrier = str(details, body, 'carrier', 'carrier', { nullable: true, max: 100 })\n const service = str(details, body, 'service', 'service', { nullable: true, max: 100 })\n const trackingNumber = str(details, body, 'tracking_number', 'tracking_number', { nullable: true, max: 100 })\n const trackingUrl = str(details, body, 'tracking_url', 'tracking_url', { nullable: true, max: 2000 })\n if (trackingUrl && !/^https?:\\/\\//.test(trackingUrl)) details.tracking_url = 'must be an absolute http(s) URL'\n const shippedAt = isoDate(details, body.shipped_at, 'shipped_at')\n if (body.notify_customer !== undefined && typeof body.notify_customer !== 'boolean') details.notify_customer = 'must be a boolean'\n const lines: { sku: string; quantity: number }[] = []\n if (!Array.isArray(body.lines) || body.lines.length === 0) details.lines = 'must be a non-empty array'\n else\n body.lines.forEach((raw, i) => {\n if (!isObject(raw) || !isString(raw.sku) || !raw.sku.trim()) details[`lines.${i}.sku`] = 'is required'\n if (!isObject(raw) || !isInt(raw.quantity) || raw.quantity < 1) details[`lines.${i}.quantity`] = 'must be an integer of at least 1'\n if (isObject(raw) && isString(raw.sku) && isInt(raw.quantity) && raw.quantity >= 1) lines.push({ sku: raw.sku.trim(), quantity: raw.quantity })\n })\n finish(details, 'Invalid fulfillment payload')\n return {\n external_id: externalId!.trim(),\n carrier: carrier ?? null,\n service: service ?? null,\n tracking_number: trackingNumber ?? null,\n tracking_url: trackingUrl ?? null,\n shipped_at: shippedAt ?? null,\n lines,\n notify_customer: body.notify_customer === undefined ? true : (body.notify_customer as boolean),\n }\n}\n\nexport interface OrderPatch {\n external_reference?: string | null\n status?: 'cancelled'\n payment?: { status: 'paid' | 'refunded' | 'failed'; provider?: string | null; reference?: string | null; paid_at?: string | null }\n}\n\nexport function validateOrderPatch(body: Record<string, unknown>): OrderPatch {\n const details: Details = {}\n const out: OrderPatch = {}\n if ('external_reference' in body) {\n const ref = str(details, body, 'external_reference', 'external_reference', { nullable: true, max: 64 })\n if (ref !== undefined) out.external_reference = ref\n }\n if (body.status !== undefined) {\n if (body.status !== 'cancelled') details.status = 'can only be set to \"cancelled\"'\n else out.status = 'cancelled'\n }\n if (body.payment !== undefined) {\n if (!isObject(body.payment)) details.payment = 'must be an object'\n else {\n const p = body.payment\n if (p.status !== 'paid' && p.status !== 'refunded' && p.status !== 'failed') details['payment.status'] = 'must be paid, refunded or failed'\n const provider = str(details, p, 'provider', 'payment.provider', { nullable: true, max: 64 })\n const reference = str(details, p, 'reference', 'payment.reference', { nullable: true, max: 255 })\n const paidAt = isoDate(details, p.paid_at, 'payment.paid_at')\n if (p.status === 'paid' || p.status === 'refunded' || p.status === 'failed') {\n out.payment = { status: p.status, provider: provider ?? null, reference: reference ?? null, paid_at: paidAt ?? null }\n }\n }\n }\n if (!('external_reference' in body) && body.status === undefined && body.payment === undefined) details.body = 'must include external_reference, status or payment'\n finish(details, 'Invalid order patch')\n return out\n}\n","import type { Connection, Store } from '../store'\nimport { API_KEY_PREFIX } from '../store/connections'\nimport { secretsEqual } from '../store/crypto'\nimport { StoreError } from '../store/errors'\nimport { serializeCategory, serializeFulfillment, serializeOrder, serializeProduct } from './serialize'\nimport {\n validateCategories, validateFulfillment, validateInventory, validateListQuery, validateOrderPatch, validateProducts, ValidationError,\n} from './validate'\n\nexport interface StoreApiOptions {\n store: Store\n /**\n * The STORE_API_KEY secret — the break-glass key that works without an owner\n * or a connection. Optional: a store whose consumers all connect through the\n * owner-approved flow can leave it unset.\n */\n apiKey: string | undefined\n /** Where the API is mounted in the request URL. */\n basePath?: string\n}\n\n/** Who a request authenticated as: the environment key, or an approved connection. */\nexport type ApiCaller = { kind: 'env' } | { kind: 'connection'; connection: Connection }\n\nexport const STORE_API_BASE = '/api/skuio/v1'\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', ...headers } })\n\nconst error = (status: number, code: string, message: string, details: Record<string, string> | null = null) =>\n json({ error: { code, message, details } }, status)\n\n/**\n * The Store API (`docs/store-api.md`) as one fetch-style handler. Mount it on\n * a catch-all route under `/api/skuio/v1` and hand it every method.\n */\nexport async function handleStoreApi(request: Request, options: StoreApiOptions): Promise<Response> {\n const base = (options.basePath ?? STORE_API_BASE).replace(/\\/+$/, '')\n const url = new URL(request.url)\n const at = url.pathname.indexOf(base)\n if (at < 0) return error(404, 'not_found', 'No such endpoint')\n const path = url.pathname.slice(at + base.length).replace(/\\/+$/, '') || '/'\n const method = request.method.toUpperCase()\n\n try {\n // The token exchange is the one unauthenticated endpoint: the code is the credential.\n if (path === '/connect/token' && method === 'POST') {\n const issued = await options.store.connections.exchange(await readJson(request), clientAddress(request))\n return json({ data: { api_key: issued.apiKey, connection: serializeConnection(issued.connection) } }, 201)\n }\n\n const caller = await authorized(request, options.apiKey, options.store)\n if (!caller) return error(401, 'unauthorized', 'A valid bearer key is required: a connection key issued through /admin/connect, or the STORE_API_KEY secret')\n\n return await route(method, path, url, request, options.store, caller)\n } catch (e) {\n if (e instanceof ValidationError) return error(422, 'validation_failed', e.message, e.details)\n if (e instanceof StoreError) return error(e.status, e.code, e.message, e.details)\n console.error('store api: unhandled error', e)\n return error(500, 'internal_error', 'The store could not complete the request')\n }\n}\n\nasync function authorized(request: Request, apiKey: string | undefined, store: Store): Promise<ApiCaller | null> {\n const header = request.headers.get('authorization') ?? ''\n const match = /^Bearer\\s+(.+)$/i.exec(header.trim())\n if (!match) return null\n const bearer = match[1]!.trim()\n if (bearer.startsWith(API_KEY_PREFIX)) {\n const connection = await store.connections.resolveKey(bearer)\n return connection ? { kind: 'connection', connection } : null\n }\n if (apiKey && (await secretsEqual(bearer, apiKey))) return { kind: 'env' }\n return null\n}\n\n/** The caller's address for failure counting; Cloudflare sets the first header, proxies the second. */\nfunction clientAddress(request: Request): string {\n return request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'\n}\n\nexport function serializeConnection(c: Connection) {\n return {\n id: c.id,\n name: c.name,\n tenant: c.tenant,\n scopes: c.scopes,\n key_prefix: c.keyPrefix,\n created_at: c.createdAt,\n issued_at: c.issuedAt,\n last_used_at: c.lastUsedAt,\n revoked_at: c.revokedAt,\n }\n}\n\nasync function readJson(request: Request): Promise<Record<string, unknown>> {\n const text = await request.text()\n if (!text.trim()) throw new ValidationError('Request body must be JSON', { body: 'is empty' })\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n throw new ValidationError('Request body must be JSON', { body: 'is not valid JSON' })\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new ValidationError('Request body must be a JSON object', { body: 'is not an object' })\n return parsed as Record<string, unknown>\n}\n\nasync function route(method: string, path: string, url: URL, request: Request, store: Store, caller: ApiCaller): Promise<Response> {\n const segments = path.split('/').filter(Boolean)\n\n if (path === '/health' && method === 'GET') {\n const health = await store.health()\n const connection = caller.kind === 'connection' ? { id: caller.connection.id, name: caller.connection.name, tenant: caller.connection.tenant } : null\n return json({ data: { ...health, connection } })\n }\n\n if (path === '/connect') {\n if (method === 'GET') return json({ data: caller.kind === 'connection' ? serializeConnection(caller.connection) : null })\n if (method === 'DELETE') {\n if (caller.kind !== 'connection') return error(409, 'conflict', 'The STORE_API_KEY secret is not a connection; rotate it on the Worker instead')\n await store.connections.revoke(caller.connection.id)\n return new Response(null, { status: 204 })\n }\n }\n\n if (segments[0] === 'orders') {\n if (segments.length === 1 && method === 'GET') {\n const updatedSince = url.searchParams.get('updated_since')\n const cursor = url.searchParams.get('cursor')\n const limitRaw = url.searchParams.get('limit')\n const limit = limitRaw ? Number(limitRaw) : 50\n if (limitRaw && (!Number.isInteger(limit) || limit < 1 || limit > 200)) throw new ValidationError('Invalid query', { limit: 'must be an integer from 1 to 200' })\n if (updatedSince && Number.isNaN(Date.parse(updatedSince))) throw new ValidationError('Invalid query', { updated_since: 'must be an ISO-8601 timestamp' })\n // Lazy housekeeping: abandoned processor checkouts become cancelled before the consumer sees the list.\n await store.orders.expireAbandoned()\n const page = await store.orders.changedSince(updatedSince ? new Date(updatedSince).toISOString() : null, cursor, limit)\n return json({ data: page.orders.map(serializeOrder), next_cursor: page.nextCursor })\n }\n const id = segments[1] ? decodeURIComponent(segments[1]) : ''\n if (segments.length === 2 && id) {\n if (method === 'GET') return json({ data: serializeOrder(await store.orders.require(id)) })\n if (method === 'PATCH') {\n const patch = validateOrderPatch(await readJson(request))\n let order = await store.orders.require(id)\n if (patch.external_reference !== undefined) order = await store.orders.setExternalReference(id, patch.external_reference)\n if (patch.payment) order = await store.orders.setPaymentStatus(id, patch.payment)\n if (patch.status === 'cancelled') order = await store.orders.cancel(id, 'Cancelled by the operations system')\n return json({ data: serializeOrder(order) })\n }\n }\n if (segments.length === 3 && id && segments[2] === 'fulfillments' && method === 'POST') {\n const input = validateFulfillment(await readJson(request))\n const result = await store.orders.addFulfillment(id, input)\n return json({ data: serializeFulfillment(result.fulfillment) }, result.created ? 201 : 200)\n }\n }\n\n if (path === '/inventory' && method === 'PUT') {\n const items = validateInventory(await readJson(request))\n return json({ data: await store.inventory.set(items) })\n }\n\n if (path === '/products' && method === 'PUT') {\n const products = validateProducts(await readJson(request))\n return json({ data: { upserted: await store.catalog.upsertProducts(products) } })\n }\n\n if (path === '/products' && method === 'GET') {\n const query = validateListQuery(url.searchParams, { defaultLimit: 50, maxLimit: 200, allowUpdatedSince: true, allowActive: true })\n const page = await store.catalog.listProducts(query)\n return json({ data: page.products.map(serializeProduct), next_cursor: page.nextCursor })\n }\n\n if (segments[0] === 'products' && segments.length === 2) {\n const sku = decodeURIComponent(segments[1]!)\n if (method === 'GET') return json({ data: serializeProduct(await store.catalog.requireProduct(sku)) })\n if (method === 'DELETE') {\n const done = await store.catalog.deactivateProduct(sku)\n if (!done) return error(404, 'not_found', `Product ${sku} not found`)\n return new Response(null, { status: 204 })\n }\n }\n\n if (path === '/categories' && method === 'PUT') {\n const categories = validateCategories(await readJson(request))\n return json({ data: { upserted: await store.catalog.upsertCategories(categories) } })\n }\n\n if (path === '/categories' && method === 'GET') {\n const query = validateListQuery(url.searchParams, { defaultLimit: 500, maxLimit: 1000 })\n const page = await store.catalog.listCategories(query)\n return json({ data: page.categories.map(serializeCategory), next_cursor: page.nextCursor })\n }\n\n return error(404, 'not_found', `No such endpoint: ${method} ${path}`)\n}\n\nexport {\n serializeOrder, serializeFulfillment, serializeProduct, serializeCategory,\n type ApiOrder, type ApiFulfillment, type ApiProduct, type ApiCategory,\n} from './serialize'\nexport { ValidationError } from './validate'\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/api/serialize.ts","../src/api/validate.ts","../src/api/index.ts"],"sourcesContent":["import type { CategoryRecord, ProductRecord } from '../store/catalog'\nimport type { Address, Fulfillment, Money, Order } from '../types'\n\nexport interface ApiMoney {\n amount: number\n currency: string\n}\n\nexport interface ApiAddress {\n name: string | null\n line1: string | null\n line2: string | null\n city: string | null\n state: string | null\n postal_code: string | null\n country: string | null\n}\n\nexport interface ApiFulfillment {\n id: string\n external_id: string\n carrier: string | null\n service: string | null\n tracking_number: string | null\n tracking_url: string | null\n shipped_at: string | null\n lines: { sku: string; quantity: number }[]\n created_at: string\n}\n\nexport interface ApiOrder {\n id: string\n number: string\n status: Order['status']\n payment: { status: Order['payment']['status']; provider: string | null; reference: string | null; method_label: string | null; paid_at: string | null }\n currency: string\n subtotal: ApiMoney\n shipping: ApiMoney\n tax: ApiMoney\n discount: ApiMoney\n /** The store discount code the buyer used, upper case. Partner and commission stay in the store. */\n discount_code: string | null\n total: ApiMoney\n customer: { id: string; email: string; name: string | null; phone: string | null }\n shipping_address: ApiAddress\n billing_address: ApiAddress | null\n shipping_method: { code: string; name: string } | null\n lines: { id: string; sku: string; name: string; quantity: number; unit_amount: ApiMoney; total_amount: ApiMoney; fulfilled_quantity: number }[]\n fulfillments: ApiFulfillment[]\n external_reference: string | null\n notes: string | null\n metadata: Record<string, string>\n created_at: string\n updated_at: string\n paid_at: string | null\n cancelled_at: string | null\n}\n\nexport interface ApiProduct {\n sku: string\n name: string\n slug: string\n description_html: string | null\n short_description_html: string | null\n price: ApiMoney\n compare_at: ApiMoney | null\n attributes: Record<string, string>\n active: boolean\n position: number\n legacy_path: string | null\n categories: { id: string; slug: string | null; name: string | null }[]\n images: { url: string; alt: string | null; position: number }[]\n inventory: { available: number; allow_backorder: boolean; updated_at: string } | null\n created_at: string\n updated_at: string\n}\n\nexport interface ApiCategory {\n id: string\n parent_id: string | null\n slug: string\n name: string\n description_html: string | null\n position: number\n product_count: number\n created_at: string\n updated_at: string\n}\n\nconst money = (m: Money): ApiMoney => ({ amount: m.amount, currency: m.currency.toUpperCase() })\n\nconst address = (a: Address | null): ApiAddress | null =>\n a\n ? {\n name: a.name ?? null,\n line1: a.line1 ?? null,\n line2: a.line2 ?? null,\n city: a.city ?? null,\n state: a.state ?? null,\n postal_code: a.postalCode ?? null,\n country: a.country ?? null,\n }\n : null\n\nexport function serializeFulfillment(f: Fulfillment): ApiFulfillment {\n return {\n id: f.id,\n external_id: f.externalId,\n carrier: f.carrier,\n service: f.service,\n tracking_number: f.trackingNumber,\n tracking_url: f.trackingUrl,\n shipped_at: f.shippedAt,\n lines: f.lines.map((l) => ({ sku: l.sku, quantity: l.quantity })),\n created_at: f.createdAt,\n }\n}\n\n/**\n * The Store API's view of an order. `access_key` never leaves the store. `number` is the human number the buyer\n * sees, with the store's prefix (VLD4200), so the consumer shows the same number.\n */\nexport function serializeOrder(o: Order, numberPrefix = ''): ApiOrder {\n return {\n id: o.id,\n number: `${numberPrefix}${o.number}`,\n status: o.status,\n payment: { status: o.payment.status, provider: o.payment.provider, reference: o.payment.reference, method_label: o.payment.methodLabel, paid_at: o.payment.paidAt },\n currency: o.currency.toUpperCase(),\n subtotal: money(o.subtotal),\n shipping: money(o.shipping),\n tax: money(o.tax),\n discount: money(o.discount),\n discount_code: o.discountCode,\n total: money(o.total),\n customer: { id: o.customer.id, email: o.customer.email, name: o.customer.name, phone: o.customer.phone },\n shipping_address: address(o.shippingAddress) ?? { name: null, line1: null, line2: null, city: null, state: null, postal_code: null, country: null },\n billing_address: address(o.billingAddress),\n shipping_method: o.shippingMethod,\n lines: o.lines.map((l) => ({\n id: l.id,\n sku: l.sku,\n name: l.name,\n quantity: l.quantity,\n unit_amount: money(l.unitAmount),\n total_amount: money(l.totalAmount),\n fulfilled_quantity: l.fulfilledQuantity,\n })),\n fulfillments: o.fulfillments.map(serializeFulfillment),\n external_reference: o.externalReference,\n notes: o.notes,\n metadata: o.metadata,\n created_at: o.createdAt,\n updated_at: o.updatedAt,\n paid_at: o.paidAt,\n cancelled_at: o.cancelledAt,\n }\n}\n\n/** The Store API's view of a catalogue product — the stored row, not the shopper projection. */\nexport function serializeProduct(p: ProductRecord): ApiProduct {\n return {\n sku: p.sku,\n name: p.name,\n slug: p.slug,\n description_html: p.descriptionHtml,\n short_description_html: p.shortDescriptionHtml,\n price: money(p.price),\n compare_at: p.compareAtPrice ? money(p.compareAtPrice) : null,\n attributes: p.attributes,\n active: p.active,\n position: p.position,\n legacy_path: p.legacyPath,\n categories: p.categories.map((c) => ({ id: c.id, slug: c.slug, name: c.name })),\n images: p.images.map((i) => ({ url: i.url, alt: i.alt, position: i.position })),\n inventory: p.inventory ? { available: p.inventory.available, allow_backorder: p.inventory.allowBackorder, updated_at: p.inventory.updatedAt } : null,\n created_at: p.createdAt,\n updated_at: p.updatedAt,\n }\n}\n\nexport function serializeCategory(c: CategoryRecord): ApiCategory {\n return {\n id: c.id,\n parent_id: c.parentId,\n slug: c.slug,\n name: c.name,\n description_html: c.descriptionHtml,\n position: c.position,\n product_count: c.productCount,\n created_at: c.createdAt,\n updated_at: c.updatedAt,\n }\n}\n","import type { CategoryInput, ProductInput } from '../store/catalog'\nimport type { InventoryItemInput } from '../store/inventory'\nimport type { FulfillmentInput } from '../store/orders'\nimport type { Money } from '../types'\n\nexport class ValidationError extends Error {\n readonly details: Record<string, string>\n constructor(message: string, details: Record<string, string>) {\n super(message)\n this.name = 'ValidationError'\n this.details = details\n }\n}\n\ntype Details = Record<string, string>\n\nconst isObject = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v)\nconst isString = (v: unknown): v is string => typeof v === 'string'\nconst isInt = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v)\n\nfunction str(details: Details, obj: Record<string, unknown>, key: string, path: string, opts: { required?: boolean; max?: number; nullable?: boolean } = {}): string | null | undefined {\n const v = obj[key]\n if (v === undefined) {\n if (opts.required) details[path] = 'is required'\n return undefined\n }\n if (v === null) {\n if (opts.required && !opts.nullable) details[path] = 'is required'\n return opts.nullable || !opts.required ? null : undefined\n }\n if (!isString(v)) {\n details[path] = 'must be a string'\n return undefined\n }\n if (opts.required && !v.trim()) details[path] = 'is required'\n if (opts.max && v.length > opts.max) details[path] = `must be at most ${opts.max} characters`\n return v\n}\n\nfunction moneyField(details: Details, v: unknown, path: string, opts: { required?: boolean; nullable?: boolean } = {}): Money | null | undefined {\n if (v === undefined) {\n if (opts.required) details[path] = 'is required'\n return undefined\n }\n if (v === null) {\n if (!opts.nullable) details[path] = 'is required'\n return null\n }\n if (!isObject(v) || !isInt(v.amount) || !isString(v.currency) || v.currency.length !== 3) {\n details[path] = 'must be { amount: integer minor units, currency: ISO 4217 }'\n return undefined\n }\n if (v.amount < 0) details[path] = 'must not be negative'\n return { amount: v.amount, currency: v.currency }\n}\n\nfunction isoDate(details: Details, v: unknown, path: string): string | null | undefined {\n if (v === undefined) return undefined\n if (v === null) return null\n if (!isString(v) || Number.isNaN(Date.parse(v))) {\n details[path] = 'must be an ISO-8601 timestamp'\n return undefined\n }\n return new Date(v).toISOString()\n}\n\nfunction finish(details: Details, message: string): void {\n if (Object.keys(details).length) throw new ValidationError(message, details)\n}\n\nexport interface ListQuery {\n updatedSince: string | null\n cursor: string | null\n limit: number\n active: boolean | null\n}\n\n/**\n * Query string for the catalogue list endpoints. Out-of-range values are a 422\n * rather than a silent clamp, so a caller paging with a bad limit is told.\n */\nexport function validateListQuery(\n params: URLSearchParams,\n opts: { defaultLimit: number; maxLimit: number; allowUpdatedSince?: boolean; allowActive?: boolean }\n): ListQuery {\n const details: Details = {}\n const limitRaw = params.get('limit')\n const limit = limitRaw ? Number(limitRaw) : opts.defaultLimit\n if (limitRaw && (!Number.isInteger(limit) || limit < 1 || limit > opts.maxLimit)) {\n details.limit = `must be an integer from 1 to ${opts.maxLimit}`\n }\n\n const updatedSinceRaw = opts.allowUpdatedSince ? params.get('updated_since') : null\n if (updatedSinceRaw && Number.isNaN(Date.parse(updatedSinceRaw))) details.updated_since = 'must be an ISO-8601 timestamp'\n\n const activeRaw = opts.allowActive ? params.get('active') : null\n let active: boolean | null = null\n if (activeRaw !== null) {\n if (activeRaw === 'true') active = true\n else if (activeRaw === 'false') active = false\n else details.active = 'must be true or false'\n }\n\n finish(details, 'Invalid query')\n return {\n updatedSince: updatedSinceRaw ? new Date(updatedSinceRaw).toISOString() : null,\n cursor: params.get('cursor'),\n limit: Number.isInteger(limit) ? limit : opts.defaultLimit,\n active,\n }\n}\n\nexport function validateProducts(body: Record<string, unknown>): ProductInput[] {\n const details: Details = {}\n const list = body.products\n if (!Array.isArray(list)) {\n details.products = 'must be an array'\n finish(details, 'Invalid products payload')\n }\n const items = list as unknown[]\n if (items.length > 200) details.products = 'must have at most 200 items'\n const out: ProductInput[] = []\n const seen = new Set<string>()\n items.forEach((raw, i) => {\n const p = `products.${i}`\n if (!isObject(raw)) {\n details[p] = 'must be an object'\n return\n }\n const sku = str(details, raw, 'sku', `${p}.sku`, { required: true, max: 64 })\n const name = str(details, raw, 'name', `${p}.name`, { required: true, max: 255 })\n const slug = str(details, raw, 'slug', `${p}.slug`, { max: 255, nullable: true })\n const description = str(details, raw, 'description_html', `${p}.description_html`, { nullable: true })\n const short = str(details, raw, 'short_description_html', `${p}.short_description_html`, { nullable: true })\n const price = moneyField(details, raw.price, `${p}.price`, { required: true })\n const compare = moneyField(details, raw.compare_at_price, `${p}.compare_at_price`, { nullable: true })\n if (sku && seen.has(sku)) details[`${p}.sku`] = 'appears more than once'\n if (sku) seen.add(sku)\n if (slug && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) details[`${p}.slug`] = 'must be lower-case letters, digits and hyphens'\n\n const images: { url: string; alt?: string | null }[] = []\n if (raw.images !== undefined) {\n if (!Array.isArray(raw.images)) details[`${p}.images`] = 'must be an array'\n else\n raw.images.forEach((img, j) => {\n if (!isObject(img) || !isString(img.url) || !img.url.trim()) details[`${p}.images.${j}.url`] = 'is required'\n else images.push({ url: img.url, alt: isString(img.alt) ? img.alt : null })\n })\n }\n const categoryIds: string[] = []\n if (raw.category_ids !== undefined) {\n if (!Array.isArray(raw.category_ids) || !raw.category_ids.every(isString)) details[`${p}.category_ids`] = 'must be an array of strings'\n else categoryIds.push(...(raw.category_ids as string[]))\n }\n let attributes: Record<string, string> = {}\n if (raw.attributes !== undefined) {\n if (!isObject(raw.attributes) || !Object.values(raw.attributes).every(isString)) details[`${p}.attributes`] = 'must be an object of strings'\n else attributes = raw.attributes as Record<string, string>\n }\n if (raw.active !== undefined && typeof raw.active !== 'boolean') details[`${p}.active`] = 'must be a boolean'\n if (raw.position !== undefined && !isInt(raw.position)) details[`${p}.position`] = 'must be an integer'\n\n if (sku && name && price) {\n out.push({\n sku: sku.trim(),\n name: name.trim(),\n slug: slug ?? null,\n description_html: description ?? null,\n short_description_html: short ?? null,\n price,\n compare_at_price: compare ?? null,\n images,\n category_ids: categoryIds,\n attributes,\n active: raw.active === undefined ? true : (raw.active as boolean),\n ...(isInt(raw.position) ? { position: raw.position } : {}),\n })\n }\n })\n finish(details, 'Invalid products payload')\n return out\n}\n\nexport function validateCategories(body: Record<string, unknown>): CategoryInput[] {\n const details: Details = {}\n const list = body.categories\n if (!Array.isArray(list)) {\n details.categories = 'must be an array'\n finish(details, 'Invalid categories payload')\n }\n const items = list as unknown[]\n if (items.length > 500) details.categories = 'must have at most 500 items'\n const out: CategoryInput[] = []\n const seen = new Set<string>()\n items.forEach((raw, i) => {\n const p = `categories.${i}`\n if (!isObject(raw)) {\n details[p] = 'must be an object'\n return\n }\n const id = str(details, raw, 'id', `${p}.id`, { required: true, max: 64 })\n const name = str(details, raw, 'name', `${p}.name`, { required: true, max: 255 })\n const slug = str(details, raw, 'slug', `${p}.slug`, { max: 255, nullable: true })\n const parent = str(details, raw, 'parent_id', `${p}.parent_id`, { max: 64, nullable: true })\n const description = str(details, raw, 'description_html', `${p}.description_html`, { nullable: true })\n if (id && seen.has(id)) details[`${p}.id`] = 'appears more than once'\n if (id) seen.add(id)\n if (id && parent && parent === id) details[`${p}.parent_id`] = 'cannot be the category itself'\n if (slug && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) details[`${p}.slug`] = 'must be lower-case letters, digits and hyphens'\n let image: { url: string; alt?: string | null } | null = null\n if (raw.image !== undefined && raw.image !== null) {\n if (!isObject(raw.image) || !isString(raw.image.url)) details[`${p}.image`] = 'must be { url, alt? } or null'\n else image = { url: raw.image.url, alt: isString(raw.image.alt) ? raw.image.alt : null }\n }\n if (raw.position !== undefined && !isInt(raw.position)) details[`${p}.position`] = 'must be an integer'\n if (id && name) {\n out.push({ id: id.trim(), name: name.trim(), slug: slug ?? null, parent_id: parent ?? null, description_html: description ?? null, image, ...(isInt(raw.position) ? { position: raw.position } : {}) })\n }\n })\n finish(details, 'Invalid categories payload')\n return out\n}\n\nexport function validateInventory(body: Record<string, unknown>): InventoryItemInput[] {\n const details: Details = {}\n const list = body.items\n if (!Array.isArray(list)) {\n details.items = 'must be an array'\n finish(details, 'Invalid inventory payload')\n }\n const items = list as unknown[]\n if (items.length > 500) details.items = 'must have at most 500 items'\n const out: InventoryItemInput[] = []\n items.forEach((raw, i) => {\n const p = `items.${i}`\n if (!isObject(raw)) {\n details[p] = 'must be an object'\n return\n }\n const sku = str(details, raw, 'sku', `${p}.sku`, { required: true, max: 64 })\n if (!isInt(raw.available)) details[`${p}.available`] = 'must be an integer'\n else if (raw.available < 0) details[`${p}.available`] = 'must be a non-negative integer'\n if (raw.allow_backorder !== undefined && typeof raw.allow_backorder !== 'boolean') details[`${p}.allow_backorder`] = 'must be a boolean'\n if (sku && isInt(raw.available) && raw.available >= 0) out.push({ sku: sku.trim(), available: raw.available, allow_backorder: raw.allow_backorder === true })\n })\n finish(details, 'Invalid inventory payload')\n return out\n}\n\nexport function validateFulfillment(body: Record<string, unknown>): FulfillmentInput {\n const details: Details = {}\n const externalId = str(details, body, 'external_id', 'external_id', { required: true, max: 64 })\n const carrier = str(details, body, 'carrier', 'carrier', { nullable: true, max: 100 })\n const service = str(details, body, 'service', 'service', { nullable: true, max: 100 })\n const trackingNumber = str(details, body, 'tracking_number', 'tracking_number', { nullable: true, max: 100 })\n const trackingUrl = str(details, body, 'tracking_url', 'tracking_url', { nullable: true, max: 2000 })\n if (trackingUrl && !/^https?:\\/\\//.test(trackingUrl)) details.tracking_url = 'must be an absolute http(s) URL'\n const shippedAt = isoDate(details, body.shipped_at, 'shipped_at')\n if (body.notify_customer !== undefined && typeof body.notify_customer !== 'boolean') details.notify_customer = 'must be a boolean'\n const lines: { sku: string; quantity: number }[] = []\n if (!Array.isArray(body.lines) || body.lines.length === 0) details.lines = 'must be a non-empty array'\n else\n body.lines.forEach((raw, i) => {\n if (!isObject(raw) || !isString(raw.sku) || !raw.sku.trim()) details[`lines.${i}.sku`] = 'is required'\n if (!isObject(raw) || !isInt(raw.quantity) || raw.quantity < 1) details[`lines.${i}.quantity`] = 'must be an integer of at least 1'\n if (isObject(raw) && isString(raw.sku) && isInt(raw.quantity) && raw.quantity >= 1) lines.push({ sku: raw.sku.trim(), quantity: raw.quantity })\n })\n finish(details, 'Invalid fulfillment payload')\n return {\n external_id: externalId!.trim(),\n carrier: carrier ?? null,\n service: service ?? null,\n tracking_number: trackingNumber ?? null,\n tracking_url: trackingUrl ?? null,\n shipped_at: shippedAt ?? null,\n lines,\n notify_customer: body.notify_customer === undefined ? true : (body.notify_customer as boolean),\n }\n}\n\nexport interface OrderPatch {\n external_reference?: string | null\n status?: 'cancelled'\n payment?: { status: 'paid' | 'refunded' | 'failed'; provider?: string | null; reference?: string | null; paid_at?: string | null }\n}\n\nexport function validateOrderPatch(body: Record<string, unknown>): OrderPatch {\n const details: Details = {}\n const out: OrderPatch = {}\n if ('external_reference' in body) {\n const ref = str(details, body, 'external_reference', 'external_reference', { nullable: true, max: 64 })\n if (ref !== undefined) out.external_reference = ref\n }\n if (body.status !== undefined) {\n if (body.status !== 'cancelled') details.status = 'can only be set to \"cancelled\"'\n else out.status = 'cancelled'\n }\n if (body.payment !== undefined) {\n if (!isObject(body.payment)) details.payment = 'must be an object'\n else {\n const p = body.payment\n if (p.status !== 'paid' && p.status !== 'refunded' && p.status !== 'failed') details['payment.status'] = 'must be paid, refunded or failed'\n const provider = str(details, p, 'provider', 'payment.provider', { nullable: true, max: 64 })\n const reference = str(details, p, 'reference', 'payment.reference', { nullable: true, max: 255 })\n const paidAt = isoDate(details, p.paid_at, 'payment.paid_at')\n if (p.status === 'paid' || p.status === 'refunded' || p.status === 'failed') {\n out.payment = { status: p.status, provider: provider ?? null, reference: reference ?? null, paid_at: paidAt ?? null }\n }\n }\n }\n if (!('external_reference' in body) && body.status === undefined && body.payment === undefined) details.body = 'must include external_reference, status or payment'\n finish(details, 'Invalid order patch')\n return out\n}\n","import type { Connection, Store } from '../store'\nimport { API_KEY_PREFIX, type ConnectScope } from '../store/connections'\nimport { secretsEqual } from '../store/crypto'\nimport { StoreError } from '../store/errors'\nimport { serializeCategory, serializeFulfillment, serializeOrder, serializeProduct } from './serialize'\nimport {\n validateCategories, validateFulfillment, validateInventory, validateListQuery, validateOrderPatch, validateProducts, ValidationError,\n} from './validate'\n\nexport interface StoreApiOptions {\n store: Store\n /**\n * The STORE_API_KEY secret — the break-glass key that works without an owner\n * or a connection. Optional: a store whose consumers all connect through the\n * owner-approved flow can leave it unset.\n */\n apiKey: string | undefined\n /** Where the API is mounted in the request URL. */\n basePath?: string\n}\n\n/** Who a request authenticated as: the environment key, or an approved connection. */\nexport type ApiCaller = { kind: 'env' } | { kind: 'connection'; connection: Connection }\n\n/**\n * Where the Store API is served. `/api/store/v1` is the neutral path for any\n * operations system; `/api/skuio/v1` is the original path SKU.io's Storefront\n * integration calls, kept for good. Same handler, same keys, same responses.\n */\nexport const STORE_API_BASES = ['/api/store/v1', '/api/skuio/v1'] as const\n/** @deprecated The SKU.io path; use STORE_API_BASES. */\nexport const STORE_API_BASE = '/api/skuio/v1'\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', ...headers } })\n\nconst error = (status: number, code: string, message: string, details: Record<string, string> | null = null) =>\n json({ error: { code, message, details } }, status)\n\n/**\n * The Store API (`docs/store-api.md`) as one fetch-style handler. Mount it on\n * a catch-all route under `/api/store/v1` (and `/api/skuio/v1`, the path SKU.io\n * calls) and hand it every method.\n */\nexport async function handleStoreApi(request: Request, options: StoreApiOptions): Promise<Response> {\n const url = new URL(request.url)\n const bases = options.basePath ? [options.basePath] : [...STORE_API_BASES]\n const base = bases.map((b) => b.replace(/\\/+$/, '')).find((b) => url.pathname.includes(b))\n if (!base) return error(404, 'not_found', 'No such endpoint')\n const at = url.pathname.indexOf(base)\n const path = url.pathname.slice(at + base.length).replace(/\\/+$/, '') || '/'\n const method = request.method.toUpperCase()\n\n try {\n // The token exchange is the one unauthenticated endpoint: the code is the credential.\n if (path === '/connect/token' && method === 'POST') {\n const issued = await options.store.connections.exchange(await readJson(request), clientAddress(request))\n return json({ data: { api_key: issued.apiKey, connection: serializeConnection(issued.connection) } }, 201)\n }\n\n const caller = await authorized(request, options.apiKey, options.store)\n if (!caller) return error(401, 'unauthorized', 'A valid bearer key is required: a connection key issued through /admin/connect, or the STORE_API_KEY secret')\n\n // A connection may use only what the owner approved; the STORE_API_KEY secret may use everything.\n const scope = scopeFor(path)\n if (scope && caller.kind === 'connection' && !caller.connection.scopes.includes(scope)) {\n return error(403, 'forbidden', `This connection was not approved for \"${scope}\"; reconnect and approve it at /admin/connect`)\n }\n\n return await route(method, path, url, request, options.store, caller)\n } catch (e) {\n if (e instanceof ValidationError) return error(422, 'validation_failed', e.message, e.details)\n if (e instanceof StoreError) return error(e.status, e.code, e.message, e.details)\n console.error('store api: unhandled error', e)\n return error(500, 'internal_error', 'The store could not complete the request')\n }\n}\n\nasync function authorized(request: Request, apiKey: string | undefined, store: Store): Promise<ApiCaller | null> {\n const header = request.headers.get('authorization') ?? ''\n const match = /^Bearer\\s+(.+)$/i.exec(header.trim())\n if (!match) return null\n const bearer = match[1]!.trim()\n if (bearer.startsWith(API_KEY_PREFIX)) {\n const connection = await store.connections.resolveKey(bearer)\n return connection ? { kind: 'connection', connection } : null\n }\n if (apiKey && (await secretsEqual(bearer, apiKey))) return { kind: 'env' }\n return null\n}\n\n/** The caller's address for failure counting; Cloudflare sets the first header, proxies the second. */\nfunction clientAddress(request: Request): string {\n return request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'\n}\n\nexport function serializeConnection(c: Connection) {\n return {\n id: c.id,\n name: c.name,\n tenant: c.tenant,\n scopes: c.scopes,\n key_prefix: c.keyPrefix,\n created_at: c.createdAt,\n issued_at: c.issuedAt,\n last_used_at: c.lastUsedAt,\n revoked_at: c.revokedAt,\n }\n}\n\nasync function readJson(request: Request): Promise<Record<string, unknown>> {\n const text = await request.text()\n if (!text.trim()) throw new ValidationError('Request body must be JSON', { body: 'is empty' })\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n throw new ValidationError('Request body must be JSON', { body: 'is not valid JSON' })\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new ValidationError('Request body must be a JSON object', { body: 'is not an object' })\n return parsed as Record<string, unknown>\n}\n\n/**\n * The scope an endpoint needs. There is deliberately no endpoint that creates\n * an order: orders come only from the store's own checkout, priced from its\n * database, so a leaked key cannot invent sales.\n */\nfunction scopeFor(path: string): ConnectScope | null {\n const first = path.split('/').filter(Boolean)[0]\n if (first === 'orders') return /^\\/orders\\/[^/]+\\/fulfillments$/.test(path) ? 'fulfillments' : 'orders'\n if (first === 'inventory') return 'inventory'\n if (first === 'products' || first === 'categories') return 'catalog'\n return null // /health, /connect\n}\n\nasync function route(method: string, path: string, url: URL, request: Request, store: Store, caller: ApiCaller): Promise<Response> {\n const segments = path.split('/').filter(Boolean)\n\n if (path === '/health' && method === 'GET') {\n const health = await store.health()\n const connection = caller.kind === 'connection' ? { id: caller.connection.id, name: caller.connection.name, tenant: caller.connection.tenant } : null\n return json({ data: { ...health, connection } })\n }\n\n if (path === '/connect') {\n if (method === 'GET') return json({ data: caller.kind === 'connection' ? serializeConnection(caller.connection) : null })\n if (method === 'DELETE') {\n if (caller.kind !== 'connection') return error(409, 'conflict', 'The STORE_API_KEY secret is not a connection; rotate it on the Worker instead')\n await store.connections.revoke(caller.connection.id)\n return new Response(null, { status: 204 })\n }\n }\n\n if (segments[0] === 'orders') {\n if (segments.length === 1 && method === 'GET') {\n const updatedSince = url.searchParams.get('updated_since')\n const cursor = url.searchParams.get('cursor')\n const limitRaw = url.searchParams.get('limit')\n const limit = limitRaw ? Number(limitRaw) : 50\n if (limitRaw && (!Number.isInteger(limit) || limit < 1 || limit > 200)) throw new ValidationError('Invalid query', { limit: 'must be an integer from 1 to 200' })\n if (updatedSince && Number.isNaN(Date.parse(updatedSince))) throw new ValidationError('Invalid query', { updated_since: 'must be an ISO-8601 timestamp' })\n // Lazy housekeeping: abandoned processor checkouts become cancelled before the consumer sees the list.\n await store.orders.expireAbandoned()\n const page = await store.orders.changedSince(updatedSince ? new Date(updatedSince).toISOString() : null, cursor, limit)\n return json({ data: page.orders.map((o) => serializeOrder(o, store.config.orders.numberPrefix)), next_cursor: page.nextCursor })\n }\n const id = segments[1] ? decodeURIComponent(segments[1]) : ''\n if (segments.length === 2 && id) {\n if (method === 'GET') return json({ data: serializeOrder(await store.orders.require(id), store.config.orders.numberPrefix) })\n if (method === 'PATCH') {\n const patch = validateOrderPatch(await readJson(request))\n // Recording a payment is its own scope: a key that may read and stamp orders cannot\n // turn an unpaid order into a paid one unless the owner approved that too.\n if (patch.payment && caller.kind === 'connection' && !caller.connection.scopes.includes('payments')) {\n return error(403, 'forbidden', 'This connection was not approved for \"payments\"; reconnect and approve it at /admin/connect')\n }\n let order = await store.orders.require(id)\n if (patch.external_reference !== undefined) order = await store.orders.setExternalReference(id, patch.external_reference)\n if (patch.payment) order = await store.orders.setPaymentStatus(id, patch.payment)\n if (patch.status === 'cancelled') order = await store.orders.cancel(id, 'Cancelled by the operations system')\n return json({ data: serializeOrder(order, store.config.orders.numberPrefix) })\n }\n }\n if (segments.length === 3 && id && segments[2] === 'fulfillments' && method === 'POST') {\n const input = validateFulfillment(await readJson(request))\n const result = await store.orders.addFulfillment(id, input)\n return json({ data: serializeFulfillment(result.fulfillment) }, result.created ? 201 : 200)\n }\n }\n\n if (path === '/inventory' && method === 'PUT') {\n const items = validateInventory(await readJson(request))\n return json({ data: await store.inventory.set(items) })\n }\n\n if (path === '/products' && method === 'PUT') {\n const products = validateProducts(await readJson(request))\n return json({ data: { upserted: await store.catalog.upsertProducts(products) } })\n }\n\n if (path === '/products' && method === 'GET') {\n const query = validateListQuery(url.searchParams, { defaultLimit: 50, maxLimit: 200, allowUpdatedSince: true, allowActive: true })\n const page = await store.catalog.listProducts(query)\n return json({ data: page.products.map(serializeProduct), next_cursor: page.nextCursor })\n }\n\n if (segments[0] === 'products' && segments.length === 2) {\n const sku = decodeURIComponent(segments[1]!)\n if (method === 'GET') return json({ data: serializeProduct(await store.catalog.requireProduct(sku)) })\n if (method === 'DELETE') {\n const done = await store.catalog.deactivateProduct(sku)\n if (!done) return error(404, 'not_found', `Product ${sku} not found`)\n return new Response(null, { status: 204 })\n }\n }\n\n if (path === '/categories' && method === 'PUT') {\n const categories = validateCategories(await readJson(request))\n return json({ data: { upserted: await store.catalog.upsertCategories(categories) } })\n }\n\n if (path === '/categories' && method === 'GET') {\n const query = validateListQuery(url.searchParams, { defaultLimit: 500, maxLimit: 1000 })\n const page = await store.catalog.listCategories(query)\n return json({ data: page.categories.map(serializeCategory), next_cursor: page.nextCursor })\n }\n\n return error(404, 'not_found', `No such endpoint: ${method} ${path}`)\n}\n\nexport {\n serializeOrder, serializeFulfillment, serializeProduct, serializeCategory,\n type ApiOrder, type ApiFulfillment, type ApiProduct, type ApiCategory,\n} from './serialize'\nexport { ValidationError } from './validate'\n"],"mappings":"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","names":[]}
|
|
@@ -128,10 +128,16 @@ function parseJson(raw, fallback) {
|
|
|
128
128
|
// src/store/connections.ts
|
|
129
129
|
var CONNECT_CODE_MINUTES = 10;
|
|
130
130
|
var API_KEY_PREFIX = "sk_store_";
|
|
131
|
-
var
|
|
132
|
-
var
|
|
131
|
+
var KNOWN_CONNECT_CLIENTS = { skuio: "SKU.io" };
|
|
132
|
+
var CONNECT_CLIENT_IDS = Object.keys(KNOWN_CONNECT_CLIENTS);
|
|
133
|
+
var CLIENT_ID = /^[a-z0-9][a-z0-9._-]{1,62}$/;
|
|
134
|
+
function connectClientName(clientId) {
|
|
135
|
+
return KNOWN_CONNECT_CLIENTS[clientId] ?? clientId;
|
|
136
|
+
}
|
|
137
|
+
var CONNECT_SCOPES = ["orders", "payments", "fulfillments", "inventory", "catalog"];
|
|
133
138
|
var SCOPE_DESCRIPTIONS = {
|
|
134
|
-
orders: "Read orders,
|
|
139
|
+
orders: "Read orders, stamp its own order number on them and cancel them",
|
|
140
|
+
payments: "Mark orders paid, refunded or failed (for payments taken outside this store)",
|
|
135
141
|
fulfillments: "Push shipments and tracking to orders",
|
|
136
142
|
inventory: "Update available stock",
|
|
137
143
|
catalog: "Read and update products and categories"
|
|
@@ -164,7 +170,7 @@ var ConnectionsService = class {
|
|
|
164
170
|
validateAuthorize(params) {
|
|
165
171
|
const details = {};
|
|
166
172
|
const clientId = (params.client_id ?? "").trim();
|
|
167
|
-
if (!
|
|
173
|
+
if (!CLIENT_ID.test(clientId)) details.client_id = "must be 2-63 lowercase letters, digits, dots, dashes or underscores";
|
|
168
174
|
const state = (params.state ?? "").trim();
|
|
169
175
|
if (!state) details.state = "is required";
|
|
170
176
|
else if (state.length > 1024) details.state = "must be at most 1024 characters";
|
|
@@ -192,7 +198,7 @@ var ConnectionsService = class {
|
|
|
192
198
|
if (!scopes.length) details.scope = "is required";
|
|
193
199
|
else if (unknown.length) details.scope = `unknown scope: ${unknown.join(", ")}`;
|
|
194
200
|
if (Object.keys(details).length) throw new StoreError("validation_failed", "Invalid connect request", details);
|
|
195
|
-
return { clientId, redirectUri: redirect.href, redirectHost: redirect.host, state, tenant, scopes };
|
|
201
|
+
return { clientId, clientName: connectClientName(clientId), redirectUri: redirect.href, redirectHost: redirect.host, state, tenant, scopes };
|
|
196
202
|
}
|
|
197
203
|
/** The owner said yes: create the connection and a one-time code. Returns the URL to send the browser to. */
|
|
198
204
|
async approve(request, ownerId) {
|
|
@@ -200,7 +206,7 @@ var ConnectionsService = class {
|
|
|
200
206
|
const id = newId("con");
|
|
201
207
|
const code = randomToken(32);
|
|
202
208
|
const expiresAt = new Date(new Date(now).getTime() + CONNECT_CODE_MINUTES * 6e4).toISOString();
|
|
203
|
-
const name = `${request.tenant} on
|
|
209
|
+
const name = `${request.tenant} on ${request.clientName}`;
|
|
204
210
|
await this.ctx.db.batch([
|
|
205
211
|
this.ctx.db.prepare(`INSERT INTO connections (id, name, tenant, redirect_host, scopes_json, created_by_owner_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`).bind(id, name, request.tenant, request.redirectHost, JSON.stringify(request.scopes), ownerId, now),
|
|
206
212
|
this.ctx.db.prepare(`INSERT INTO connect_codes (code_hash, connection_id, redirect_uri, expires_at, created_at) VALUES (?, ?, ?, ?, ?)`).bind(await sha256Hex(code), id, request.redirectUri, expiresAt, now)
|
|
@@ -307,4 +313,4 @@ export {
|
|
|
307
313
|
isLocalHost,
|
|
308
314
|
ConnectionsService
|
|
309
315
|
};
|
|
310
|
-
//# sourceMappingURL=chunk-
|
|
316
|
+
//# sourceMappingURL=chunk-X7VXO4QU.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 [key: string]: string | undefined\n}\n\nexport interface StoreContext {\n db: Database\n config: StorefrontConfig\n env: StoreEnv\n mail: MailAdapter\n /** ISO timestamp, injectable for tests. */\n now(): string\n siteUrl: string\n log(message: string, data?: unknown): void\n}\n\n/** Split an IN (...) list so no statement exceeds D1's bound-parameter limit. */\nexport function chunk<T>(items: T[], size = 80): T[][] {\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\nexport const placeholders = (n: number) => Array.from({ length: n }, () => '?').join(',')\n\nexport function parseJson<T>(raw: unknown, fallback: T): T {\n if (typeof raw !== 'string' || !raw) return fallback\n try {\n return JSON.parse(raw) as T\n } catch {\n return fallback\n }\n}\n","import { parseJson, type StoreContext } from './context'\nimport { newId, randomBase64Url, randomToken, sha256Hex } from './crypto'\nimport { StoreError } from './errors'\nimport { RateLimiter } from './ratelimit'\n\nexport const CONNECT_CODE_MINUTES = 10\nexport const API_KEY_PREFIX = 'sk_store_'\n/**\n * Operations systems the store can name on its approval screen. Any other\n * client id may connect too — an ERP, an OMS, a script — because nothing is\n * granted until the owner approves the request in /admin/connect, which shows\n * the client, the account and the exact host the browser returns to.\n */\nexport const KNOWN_CONNECT_CLIENTS: Readonly<Record<string, string>> = { skuio: 'SKU.io' }\n/** @deprecated Every well-formed client id is accepted; see KNOWN_CONNECT_CLIENTS. */\nexport const CONNECT_CLIENT_IDS = Object.keys(KNOWN_CONNECT_CLIENTS) as readonly string[]\nconst CLIENT_ID = /^[a-z0-9][a-z0-9._-]{1,62}$/\n\n/** The display name for a client id: a known system's name, else the id itself. */\nexport function connectClientName(clientId: string): string {\n return KNOWN_CONNECT_CLIENTS[clientId] ?? clientId\n}\nexport const CONNECT_SCOPES = ['orders', 'payments', 'fulfillments', 'inventory', 'catalog'] as const\nexport type ConnectScope = (typeof CONNECT_SCOPES)[number]\n\nexport const SCOPE_DESCRIPTIONS: Record<ConnectScope, string> = {\n orders: 'Read orders, stamp its own order number on them and cancel them',\n payments: 'Mark orders paid, refunded or failed (for payments taken outside this store)',\n fulfillments: 'Push shipments and tracking to orders',\n inventory: 'Update available stock',\n catalog: 'Read and update products and categories',\n}\n\nexport interface Connection {\n id: string\n name: string\n tenant: string | null\n redirectHost: string | null\n scopes: ConnectScope[]\n keyPrefix: string | null\n createdByOwnerId: string | null\n createdAt: string\n issuedAt: string | null\n lastUsedAt: string | null\n revokedAt: string | null\n}\n\nexport interface AuthorizeParams {\n client_id?: string | null\n redirect_uri?: string | null\n state?: string | null\n tenant?: string | null\n scope?: string | null\n}\n\n/** A validated connect request, ready to show the owner. */\nexport interface AuthorizeRequest {\n clientId: string\n /** Display name: \"SKU.io\" for skuio, else the client id. */\n clientName: string\n redirectUri: string\n redirectHost: string\n state: string\n tenant: string\n scopes: ConnectScope[]\n}\n\nexport interface IssuedKey {\n apiKey: string\n connection: Connection\n}\n\ninterface ConnectionRow {\n id: string\n name: string\n tenant: string | null\n redirect_host: string | null\n scopes_json: string\n key_hash: string | null\n key_prefix: string | null\n created_by_owner_id: string | null\n created_at: string\n issued_at: string | null\n last_used_at: string | null\n revoked_at: string | null\n}\n\nconst toConnection = (r: ConnectionRow): Connection => ({\n id: r.id,\n name: r.name,\n tenant: r.tenant,\n redirectHost: r.redirect_host,\n scopes: parseJson<string[]>(r.scopes_json, []).filter((s): s is ConnectScope => (CONNECT_SCOPES as readonly string[]).includes(s)),\n keyPrefix: r.key_prefix,\n createdByOwnerId: r.created_by_owner_id,\n createdAt: r.created_at,\n issuedAt: r.issued_at,\n lastUsedAt: r.last_used_at,\n revokedAt: r.revoked_at,\n})\n\n/** http is allowed only where TLS cannot exist: a developer's machine. */\nexport function isLocalHost(hostname: string): boolean {\n const h = hostname.toLowerCase()\n return h === 'localhost' || h === '127.0.0.1' || h === '[::1]' || h.endsWith('.localhost') || h.endsWith('.test')\n}\n\n/**\n * Consumers the owner approved. The store is the authorization server: an\n * owner approves a request in the browser, the consumer trades the resulting\n * one-time code for a per-connection API key, and only the key's SHA-256 is\n * ever stored. Revoking a connection turns its key into a 401.\n */\nexport class ConnectionsService {\n private readonly limiter: RateLimiter\n\n constructor(private readonly ctx: StoreContext) {\n this.limiter = new RateLimiter(ctx, { maxFailures: 20, windowMinutes: 15, lockMinutes: 15 })\n }\n\n /** Validate the query of a connect request. Throws `validation_failed` with a field per problem. */\n validateAuthorize(params: AuthorizeParams): AuthorizeRequest {\n const details: Record<string, string> = {}\n const clientId = (params.client_id ?? '').trim()\n if (!CLIENT_ID.test(clientId)) details.client_id = 'must be 2-63 lowercase letters, digits, dots, dashes or underscores'\n const state = (params.state ?? '').trim()\n if (!state) details.state = 'is required'\n else if (state.length > 1024) details.state = 'must be at most 1024 characters'\n const tenant = (params.tenant ?? '').trim()\n if (!tenant) details.tenant = 'is required'\n else if (tenant.length > 120) details.tenant = 'must be at most 120 characters'\n\n let redirect: URL | null = null\n const raw = (params.redirect_uri ?? '').trim()\n if (!raw) details.redirect_uri = 'is required'\n else {\n try {\n redirect = new URL(raw)\n } catch {\n details.redirect_uri = 'must be an absolute URL'\n }\n }\n if (redirect) {\n if (redirect.hash) details.redirect_uri = 'must not carry a fragment'\n else if (redirect.protocol !== 'https:' && !(redirect.protocol === 'http:' && isLocalHost(redirect.hostname))) details.redirect_uri = 'must use https'\n else if (redirect.username || redirect.password) details.redirect_uri = 'must not carry credentials'\n }\n\n const scopeRaw = (params.scope ?? '').split(/[\\s,]+/).map((s) => s.trim()).filter(Boolean)\n const scopes = [...new Set(scopeRaw)] as string[]\n const unknown = scopes.filter((s) => !(CONNECT_SCOPES as readonly string[]).includes(s))\n if (!scopes.length) details.scope = 'is required'\n else if (unknown.length) details.scope = `unknown scope: ${unknown.join(', ')}`\n\n if (Object.keys(details).length) throw new StoreError('validation_failed', 'Invalid connect request', details)\n return { clientId, clientName: connectClientName(clientId), redirectUri: redirect!.href, redirectHost: redirect!.host, state, tenant, scopes: scopes as ConnectScope[] }\n }\n\n /** The owner said yes: create the connection and a one-time code. Returns the URL to send the browser to. */\n async approve(request: AuthorizeRequest, ownerId: string | null): Promise<{ connection: Connection; code: string; redirectTo: string }> {\n const now = this.ctx.now()\n const id = newId('con')\n const code = randomToken(32)\n const expiresAt = new Date(new Date(now).getTime() + CONNECT_CODE_MINUTES * 60_000).toISOString()\n const name = `${request.tenant} on ${request.clientName}`\n await this.ctx.db.batch([\n this.ctx.db\n .prepare(`INSERT INTO connections (id, name, tenant, redirect_host, scopes_json, created_by_owner_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`)\n .bind(id, name, request.tenant, request.redirectHost, JSON.stringify(request.scopes), ownerId, now),\n this.ctx.db\n .prepare(`INSERT INTO connect_codes (code_hash, connection_id, redirect_uri, expires_at, created_at) VALUES (?, ?, ?, ?, ?)`)\n .bind(await sha256Hex(code), id, request.redirectUri, expiresAt, now),\n ])\n const connection = (await this.byId(id))!\n return { connection, code, redirectTo: withQuery(request.redirectUri, { code, state: request.state }) }\n }\n\n /** The owner said no. */\n denyUrl(request: AuthorizeRequest): string {\n return withQuery(request.redirectUri, { error: 'access_denied', state: request.state })\n }\n\n /**\n * Trade a code for the connection's key. Exactly once: the code is marked used\n * in the same batch that stores the key hash, so a replay finds it used.\n * `address` keys the failure counter.\n */\n async exchange(input: { code?: unknown; redirect_uri?: unknown }, address: string): Promise<IssuedKey> {\n const limitKey = `token:${address}`\n await this.limiter.assertAllowed(limitKey, 'Too many failed exchanges. Try again in a few minutes.')\n const fail = async (message: string) => {\n await this.limiter.recordFailure(limitKey)\n return new StoreError('invalid_grant', message)\n }\n const code = typeof input.code === 'string' ? input.code.trim() : ''\n const redirectUri = typeof input.redirect_uri === 'string' ? input.redirect_uri.trim() : ''\n if (!code || !redirectUri) throw await fail('code and redirect_uri are required')\n const now = this.ctx.now()\n const codeHash = await sha256Hex(code)\n const row = await this.ctx.db\n .prepare(`SELECT connection_id, redirect_uri, expires_at, used_at FROM connect_codes WHERE code_hash = ?`)\n .bind(codeHash)\n .first<{ connection_id: string; redirect_uri: string; expires_at: string; used_at: string | null }>()\n if (!row || row.used_at || row.expires_at <= now) throw await fail('The code is unknown, used or expired')\n if (row.redirect_uri !== redirectUri) throw await fail('redirect_uri does not match the one the code was issued for')\n const existing = await this.byId(row.connection_id)\n if (!existing || existing.revokedAt || existing.keyPrefix) throw await fail('The code is unknown, used or expired')\n\n const apiKey = `${API_KEY_PREFIX}${randomBase64Url(32)}`\n await this.ctx.db.batch([\n this.ctx.db.prepare(`UPDATE connect_codes SET used_at = ? WHERE code_hash = ? AND used_at IS NULL`).bind(now, codeHash),\n this.ctx.db\n .prepare(`UPDATE connections SET key_hash = ?, key_prefix = ?, issued_at = ? WHERE id = ? AND key_hash IS NULL`)\n .bind(await sha256Hex(apiKey), apiKey.slice(0, API_KEY_PREFIX.length + 8), now, row.connection_id),\n this.limiter.clearStatement(limitKey),\n ])\n const connection = (await this.byId(row.connection_id))!\n if (!connection.keyPrefix) throw await fail('The code is unknown, used or expired')\n return { apiKey, connection }\n }\n\n /** The live connection behind a bearer key, or null. Bumps last_used_at at most once a minute. */\n async resolveKey(bearer: string): Promise<Connection | null> {\n if (!bearer.startsWith(API_KEY_PREFIX)) return null\n const row = await this.ctx.db.prepare(`SELECT * FROM connections WHERE key_hash = ?`).bind(await sha256Hex(bearer)).first<ConnectionRow>()\n if (!row || row.revoked_at) return null\n const now = this.ctx.now()\n if (!row.last_used_at || new Date(now).getTime() - new Date(row.last_used_at).getTime() > 60_000) {\n await this.ctx.db.prepare(`UPDATE connections SET last_used_at = ? WHERE id = ?`).bind(now, row.id).run()\n row.last_used_at = now\n }\n return toConnection(row)\n }\n\n async byId(id: string): Promise<Connection | null> {\n const row = await this.ctx.db.prepare(`SELECT * FROM connections WHERE id = ?`).bind(id).first<ConnectionRow>()\n return row ? toConnection(row) : null\n }\n\n /** Every connection that was issued a key, newest first. Pending (never exchanged) ones are not listed. */\n async list(): Promise<Connection[]> {\n const { results } = await this.ctx.db.prepare(`SELECT * FROM connections WHERE key_hash IS NOT NULL ORDER BY created_at DESC`).all<ConnectionRow>()\n return results.map(toConnection)\n }\n\n async revoke(id: string): Promise<boolean> {\n const result = await this.ctx.db.prepare(`UPDATE connections SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL`).bind(this.ctx.now(), id).run()\n return (result.meta?.changes ?? 0) > 0\n }\n\n /** Housekeeping: expired codes, connections whose code was never exchanged, stale limiter rows. */\n async prune(): Promise<void> {\n const now = this.ctx.now()\n await this.ctx.db.batch([\n this.ctx.db.prepare(`DELETE FROM connect_codes WHERE expires_at <= ? OR used_at IS NOT NULL`).bind(now),\n this.ctx.db.prepare(`DELETE FROM connections WHERE key_hash IS NULL AND created_at <= ?`).bind(new Date(new Date(now).getTime() - CONNECT_CODE_MINUTES * 60_000).toISOString()),\n this.limiter.pruneStatement(),\n ])\n }\n}\n\nfunction withQuery(base: string, params: Record<string, string>): string {\n const url = new URL(base)\n for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)\n return url.href\n}\n"],"mappings":"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","names":[]}
|
|
@@ -397,8 +397,14 @@ interface StorefrontInput {
|
|
|
397
397
|
lowStockThreshold?: number;
|
|
398
398
|
};
|
|
399
399
|
orders?: {
|
|
400
|
-
/** Prefix for human order numbers: 'VLD-' → VLD-1001. */
|
|
400
|
+
/** Prefix for human order numbers: 'VLD-' → VLD-1001. Shown on pages, emails and receipts, and sent to SKU.io. */
|
|
401
401
|
numberPrefix?: string;
|
|
402
|
+
/**
|
|
403
|
+
* The lowest number a new order may take (default 1001). New orders take the next number above the highest
|
|
404
|
+
* order on file, or this, whichever is larger: set it above numbers an earlier platform used (4200 after
|
|
405
|
+
* WooCommerce's VLD4154) so a new order never repeats an old one.
|
|
406
|
+
*/
|
|
407
|
+
numberStart?: number;
|
|
402
408
|
};
|
|
403
409
|
/**
|
|
404
410
|
* Discount codes the store prices itself (see discounts.ts). A code with a
|
|
@@ -427,6 +433,7 @@ interface StorefrontConfig extends Omit<StorefrontInput, 'routes' | 'locale' | '
|
|
|
427
433
|
};
|
|
428
434
|
orders: {
|
|
429
435
|
numberPrefix: string;
|
|
436
|
+
numberStart: number;
|
|
430
437
|
};
|
|
431
438
|
discounts: DiscountCode[];
|
|
432
439
|
quantityDiscounts: QuantityDiscount[];
|
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, Q as QuantityDiscount, R as ReferralSettings, r as ResolvedShipping, s as RoutePatterns, t as Routes, S as ShippingMethod, u as ShippingOption, v as ShippingRegion, w as StorefrontConfig, x as StorefrontInput, y as commissionAmount, z as defineRoutes, B as defineStorefront, E as discountAmount, G as findDiscount, H as flatRate, I as freeShipping, J as hasInternationalRegion, K as normalizeDiscountCode, L as publicDiscount, N as quantityLabel, T as quantityTier, U as regionForCountry, V as shippingForCountry, W as shippingForRegion } from './config-
|
|
1
|
+
import { M as Money } from './config-C4YCi7m4.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, Q as QuantityDiscount, R as ReferralSettings, r as ResolvedShipping, s as RoutePatterns, t as Routes, S as ShippingMethod, u as ShippingOption, v as ShippingRegion, w as StorefrontConfig, x as StorefrontInput, y as commissionAmount, z as defineRoutes, B as defineStorefront, E as discountAmount, G as findDiscount, H as flatRate, I as freeShipping, J as hasInternationalRegion, K as normalizeDiscountCode, L as publicDiscount, N as quantityLabel, T as quantityTier, U as regionForCountry, V as shippingForCountry, W as shippingForRegion } from './config-C4YCi7m4.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Format minor units for display. Whole amounts drop the cents ($1,099), which
|
package/dist/index.js
CHANGED
|
@@ -100,7 +100,8 @@ function defineStorefront(input) {
|
|
|
100
100
|
lowStockThreshold: input.inventory?.lowStockThreshold ?? 3
|
|
101
101
|
},
|
|
102
102
|
orders: {
|
|
103
|
-
numberPrefix: input.orders?.numberPrefix ?? ""
|
|
103
|
+
numberPrefix: input.orders?.numberPrefix ?? "",
|
|
104
|
+
numberStart: Math.max(1, Math.trunc(input.orders?.numberStart ?? 1001))
|
|
104
105
|
},
|
|
105
106
|
discounts: input.discounts ?? [],
|
|
106
107
|
quantityDiscounts: input.quantityDiscounts ?? [],
|