@thebes/cadmea-plugin-ecommerce 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/checkout.ts","../src/collections.ts","../src/webhook.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\n/**\n * Thrown by `createCheckoutHandler` on price-mismatch/inventory rejection,\n * and by `PaymentProvider` implementations on charge failure.\n * `createCheckoutHandler` catches it internally (`instanceof\n * CadmeaPaymentError`) and maps it to HTTP 402 itself — checkout/webhook\n * handlers are plain Hono routes, never mounted through\n * `@thebes/cadmus/hono`'s `mountCmsRoutes`, so there's no shared\n * error-to-status pipeline this needs to participate in.\n *\n * Deliberately a plain `Error` subclass, not a `CadmusCmsError` one — this\n * is a Cadmea-plugin-owned error, not a Cadmus-primitive one (every real\n * `CadmusError` subclass is owned by a `packages/cadmus/src/<primitive>/`\n * folder; a payment error belongs to a plugin, not a primitive), and\n * `CadmusCmsError` is only reachable via the root `@thebes/cadmus` package\n * export (not `@thebes/cadmus/cms`) — importing that root barrel here\n * would pull in every other primitive's runtime code (including\n * Workers-only modules like `cloudflare:email`) just for one base class.\n * Keeping this plugin-local and dependency-free is the honest shape.\n */\nexport class CadmeaPaymentError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"CadmeaPaymentError\";\n }\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\nimport type { LocalApi } from \"@thebes/cadmus/cms\";\nimport { checkRateLimit } from \"@thebes/cadmus/rate-limit\";\nimport type { Context } from \"hono\";\nimport { CadmeaPaymentError } from \"./errors.js\";\nimport type { CartLineItem, PaymentProvider } from \"./types.js\";\n\n// biome-ignore lint/suspicious/noExplicitAny: LocalApi's table generic is erased at this call boundary — same pattern as @thebes/cadmus/hono's CmsRoutesOptions\ntype AnyLocalApi<TContext> = LocalApi<any, TContext>;\n\nexport interface CheckoutRequestBody {\n lineItems: CartLineItem[];\n paymentSourceToken: string;\n customerEmail?: string;\n idempotencyKey: string;\n shippingAddress?: Record<string, string | undefined>;\n metadata?: Record<string, string>;\n}\n\nexport interface CheckoutHandlerOptions<TContext> {\n provider: PaymentProvider;\n orders: AnyLocalApi<TContext>;\n payments: AnyLocalApi<TContext>;\n /**\n * Resolves the per-request access context passed to `orders`/`payments`\n * — called once per request, the same shape and timing as\n * `@thebes/cadmus/hono`'s `mountCmsRoutes`'s own `resolveContext`. This\n * is a real customer-initiated HTTP request (unlike a `CollectionHooks`\n * hook), so a real per-request context is available here, not a fixed\n * trusted value.\n */\n resolveContext: (c: Context) => Promise<TContext> | TContext;\n rateLimit?: {\n kv: KVNamespace;\n limit: number;\n windowSeconds: number;\n /** Default: \"checkout\". */\n keyPrefix?: string;\n };\n}\n\nfunction subtotalCents(lineItems: CartLineItem[]): number {\n return lineItems.reduce(\n (sum, item) => sum + item.clientUnitPrice.amount * item.quantity,\n 0,\n );\n}\n\nfunction generateOrderNumber(): string {\n return `ORD-${crypto.randomUUID().replace(/-/g, \"\").slice(0, 12).toUpperCase()}`;\n}\n\n/**\n * Returns a Hono handler implementing the checkout flow against a\n * `PaymentProvider`: rate limit → re-verify cart prices/availability\n * (never trust client-submitted prices) → idempotent customer find-or-\n * create → charge → persist `orders`/`payments` rows. A DB-write failure\n * *after* a successful charge degrades to a 200-with-warning response,\n * never a false \"payment failed\" — the customer's card was actually\n * charged, telling them otherwise would be worse than a delayed manual\n * reconciliation.\n *\n * Mount it as a plain Hono route alongside `mountCmsRoutes` — checkout\n * isn't part of the generic CMS REST surface that function mounts.\n *\n * ```ts\n * app.post(\"/api/checkout\", createCheckoutHandler({ provider, orders, payments, resolveContext }));\n * ```\n */\nexport function createCheckoutHandler<TContext>(\n options: CheckoutHandlerOptions<TContext>,\n) {\n return async (c: Context): Promise<Response> => {\n if (options.rateLimit) {\n const ip = c.req.header(\"CF-Connecting-IP\") ?? \"unknown\";\n const key = `${options.rateLimit.keyPrefix ?? \"checkout\"}:${ip}`;\n const result = await checkRateLimit(\n options.rateLimit.kv,\n key,\n options.rateLimit.limit,\n options.rateLimit.windowSeconds,\n );\n if (!result.allowed) {\n return c.json({ error: \"Rate limit exceeded\" }, 429);\n }\n }\n\n const body = await c.req.json<CheckoutRequestBody>();\n // Malformed-request checks return 400 directly, not via\n // CadmeaPaymentError — that class is reserved for \"the request was\n // well-formed but the checkout itself can't proceed\" (price/inventory\n // rejections, charge failure below), which this function maps to 402.\n if (!Array.isArray(body.lineItems) || body.lineItems.length === 0) {\n return c.json(\n { error: \"Checkout request must include at least one line item\" },\n 400,\n );\n }\n if (!body.idempotencyKey) {\n return c.json(\n { error: \"Checkout request must include an idempotencyKey\" },\n 400,\n );\n }\n\n try {\n // Re-verify every line item's price/availability against the live\n // catalog — the client-submitted price is never trusted as-is.\n const refs = body.lineItems.map((item) => item.catalogRef);\n const priceChecks = await options.provider.checkCatalogPrices(refs);\n const checkByRef = new Map(\n priceChecks.map((check) => [check.catalogRef, check]),\n );\n for (const item of body.lineItems) {\n const check = checkByRef.get(item.catalogRef);\n if (!check) {\n throw new CadmeaPaymentError(\n `Unknown catalog item \"${item.catalogRef}\"`,\n );\n }\n if (check.serverUnitPrice.amount !== item.clientUnitPrice.amount) {\n throw new CadmeaPaymentError(\n `Price mismatch for \"${item.catalogRef}\" — checkout rejected`,\n );\n }\n if (\n check.availableQuantity !== undefined &&\n check.availableQuantity < item.quantity\n ) {\n throw new CadmeaPaymentError(\n `Insufficient inventory for \"${item.catalogRef}\"`,\n );\n }\n }\n } catch (error) {\n if (error instanceof CadmeaPaymentError) {\n return c.json({ error: error.message }, 402);\n }\n throw error;\n }\n\n if (body.customerEmail) {\n // Idempotent find-or-create — result isn't used directly below\n // (the provider's own checkout() call resolves the customer again\n // internally via paymentSourceToken/customerEmail), but calling it\n // here ensures a customer record exists in the provider before the\n // charge, matching the reference Square plugin's own step ordering.\n await options.provider.findOrCreateCustomer(\n body.customerEmail,\n body.idempotencyKey,\n );\n }\n\n const result = await options.provider.checkout({\n lineItems: body.lineItems,\n paymentSourceToken: body.paymentSourceToken,\n customerEmail: body.customerEmail,\n idempotencyKey: body.idempotencyKey,\n metadata: body.metadata,\n });\n\n const context = await options.resolveContext(c);\n const orderData = {\n orderNumber: generateOrderNumber(),\n status: result.status === \"succeeded\" ? \"paid\" : \"pending\",\n totalCents: result.amount.amount,\n subtotalCents: subtotalCents(body.lineItems),\n currency: result.amount.currency,\n provider: options.provider.name,\n providerOrderRef: result.providerOrderRef,\n providerPaymentRef: result.providerPaymentRef,\n guestEmail: body.customerEmail,\n lineItems: body.lineItems.map((item) => ({\n productName: item.catalogRef,\n quantity: item.quantity,\n unitPriceCents: item.clientUnitPrice.amount,\n totalPriceCents: item.clientUnitPrice.amount * item.quantity,\n catalogRef: item.catalogRef,\n })),\n shippingAddress: body.shippingAddress,\n };\n\n try {\n const order = await options.orders.create(context, orderData);\n await options.payments.create(context, {\n provider: options.provider.name,\n providerPaymentRef: result.providerPaymentRef,\n providerOrderRef: result.providerOrderRef,\n order: order.id,\n status: result.status,\n amountCents: result.amount.amount,\n currency: result.amount.currency,\n rawResponse: result.raw,\n });\n return c.json({ order }, 201);\n } catch (cause) {\n // The charge already succeeded — never report it as failed because\n // our own record-keeping write failed afterwards.\n return c.json(\n {\n warning:\n \"Payment succeeded but order record-keeping failed — contact support\",\n providerPaymentRef: result.providerPaymentRef,\n providerOrderRef: result.providerOrderRef,\n cause: cause instanceof Error ? cause.message : String(cause),\n },\n 200,\n );\n }\n };\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Provider-agnostic ecommerce collections — Products/Variants, Orders,\n// Customers, Payments (audit log), WebhookEvents (dedup), and an optional\n// Subscriptions collection. Field types are restricted to what\n// @thebes/cadmus/cms actually supports, including the Section 3 `group`\n// (shippingAddress, flattened to real columns) and `json` (rawResponse)\n// additions.\n\nimport type { CadmeaPlugin, CollectionConfig } from \"@thebes/cadmus/cms\";\n\nexport interface EcommercePluginOptions {\n productsSlug?: string;\n ordersSlug?: string;\n customersSlug?: string;\n paymentsSlug?: string;\n webhookEventsSlug?: string;\n subscriptionsSlug?: string;\n /** What `customers.linkedUser` relates to. Default: \"users\". */\n usersSlug?: string;\n /**\n * Adds the `subscriptions` collection. Default: false — Square and\n * Stripe model recurring billing differently enough\n * (`PaymentProvider.subscriptions` is itself optional) that this\n * collection is opt-in, not assumed needed by every store.\n */\n includeSubscriptions?: boolean;\n}\n\nconst DEFAULTS = {\n products: \"products\",\n orders: \"orders\",\n customers: \"customers\",\n payments: \"payments\",\n webhookEvents: \"webhook_events\",\n subscriptions: \"subscriptions\",\n users: \"users\",\n};\n\nfunction buildProductsCollection(slug: string): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n name: { type: \"text\", required: true },\n description: { type: \"text\" },\n status: {\n type: \"select\",\n options: [\"draft\", \"active\", \"archived\"],\n required: true,\n defaultValue: \"draft\",\n },\n // No discriminator needed — every variant has the same shape.\n variants: {\n type: \"array\",\n required: true,\n fields: {\n sku: { type: \"text\", required: true },\n catalogRef: { type: \"text\", required: true },\n priceCents: { type: \"number\", required: true },\n currency: { type: \"text\", defaultValue: \"USD\" },\n inventoryCount: { type: \"number\" },\n },\n },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildOrdersCollection(\n slug: string,\n customersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n orderNumber: { type: \"text\", required: true, unique: true },\n status: {\n type: \"select\",\n options: [\n \"pending\",\n \"paid\",\n \"failed\",\n \"refunded\",\n \"partially_refunded\",\n ],\n required: true,\n defaultValue: \"pending\",\n },\n totalCents: { type: \"number\", required: true },\n subtotalCents: { type: \"number\", required: true },\n taxCents: { type: \"number\" },\n currency: { type: \"text\", defaultValue: \"USD\" },\n // Which PaymentProvider created this order — needed so webhook\n // dispatch and any provider-specific lookups (tracking, refunds)\n // know which provider's REST API to call back into.\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n providerOrderRef: { type: \"text\" },\n providerPaymentRef: { type: \"text\" },\n customer: { type: \"relationship\", relationTo: customersSlug },\n // No native `email` field type — same as the SMB form-builder's own\n // email-field handling, this is a plain `text` column; validate\n // shape in a beforeChange hook if the operator wants that.\n guestEmail: { type: \"text\" },\n lineItems: {\n type: \"array\",\n required: true,\n fields: {\n productName: { type: \"text\", required: true },\n quantity: { type: \"number\", required: true },\n unitPriceCents: { type: \"number\", required: true },\n totalPriceCents: { type: \"number\", required: true },\n catalogRef: { type: \"text\" },\n },\n },\n // The `group` field type (Section 3) — flattens to real prefixed\n // columns (shipping_address_first_name, etc), not a JSON blob, so\n // SQL-level querying on a subfield still works.\n shippingAddress: {\n type: \"group\",\n fields: {\n firstName: { type: \"text\" },\n lastName: { type: \"text\" },\n address1: { type: \"text\" },\n address2: { type: \"text\" },\n city: { type: \"text\" },\n state: { type: \"text\" },\n zip: { type: \"text\" },\n country: { type: \"text\", defaultValue: \"US\" },\n phone: { type: \"text\" },\n },\n },\n fulfillmentStatus: {\n type: \"select\",\n options: [\"pending\", \"shipped\", \"delivered\", \"failed\"],\n },\n trackingNumber: { type: \"text\" },\n trackingCarrier: { type: \"text\" },\n trackingUrl: { type: \"text\" },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildCustomersCollection(\n slug: string,\n usersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n email: { type: \"text\", required: true, unique: true },\n provider: { type: \"select\", options: [\"square\", \"stripe\"] },\n providerCustomerRef: { type: \"text\" },\n linkedUser: { type: \"relationship\", relationTo: usersSlug },\n // Square-specific; null for Stripe customers — fine to keep on the\n // shared collection since unused fields are simply null.\n loyaltyAccountRef: { type: \"text\" },\n loyaltyPoints: { type: \"number\", defaultValue: 0 },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildPaymentsCollection(\n slug: string,\n ordersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n providerPaymentRef: { type: \"text\", required: true },\n providerOrderRef: { type: \"text\" },\n order: { type: \"relationship\", relationTo: ordersSlug },\n // The provider's own status string, stored as-is for audit fidelity\n // (not normalized) — a `select` would force enumerating every\n // provider's status vocabulary here, exactly the provider-coupling\n // the core/provider split exists to avoid.\n status: { type: \"text\" },\n amountCents: { type: \"number\", required: true },\n currency: { type: \"text\", defaultValue: \"USD\" },\n // The `json` field type (Section 3) — the full raw provider payload,\n // for audit/debugging. The one place a freeform-blob column is\n // genuinely the right shape, not a workaround.\n rawResponse: { type: \"json\" },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildWebhookEventsCollection(slug: string): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n // The unique constraint *is* the concurrency-safe dedup guard — a\n // concurrent duplicate naturally throws a unique-constraint\n // CadmusCmsError from create(), which createWebhookHandler treats as\n // \"already processed,\" not a real error.\n eventId: { type: \"text\", required: true, unique: true },\n eventType: { type: \"text\" },\n receivedAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\nfunction buildSubscriptionsCollection(\n slug: string,\n customersSlug: string,\n): CollectionConfig {\n return {\n slug,\n fields: {\n id: { type: \"number\", autoIncrement: true },\n provider: {\n type: \"select\",\n options: [\"square\", \"stripe\"],\n required: true,\n },\n providerSubscriptionRef: { type: \"text\", required: true },\n customer: { type: \"relationship\", relationTo: customersSlug },\n status: { type: \"text\" },\n chargedThroughDate: { type: \"date\", mode: \"timestamp\" },\n createdAt: { type: \"date\", mode: \"timestamp\", defaultValue: \"now\" },\n },\n };\n}\n\n/**\n * Returns a Cadmea plugin that adds the provider-agnostic ecommerce\n * collections — a no-op for any collection slug already present, the same\n * idempotent-add convention `cadmea-plugin-redirects`/`cadmea-plugin-crm`\n * use.\n */\nexport function ecommercePlugin(\n options: EcommercePluginOptions = {},\n): CadmeaPlugin {\n const slugs = {\n products: options.productsSlug ?? DEFAULTS.products,\n orders: options.ordersSlug ?? DEFAULTS.orders,\n customers: options.customersSlug ?? DEFAULTS.customers,\n payments: options.paymentsSlug ?? DEFAULTS.payments,\n webhookEvents: options.webhookEventsSlug ?? DEFAULTS.webhookEvents,\n subscriptions: options.subscriptionsSlug ?? DEFAULTS.subscriptions,\n users: options.usersSlug ?? DEFAULTS.users,\n };\n\n return (config) => {\n const collections = [...config.collections];\n const addIfMissing = (collection: CollectionConfig) => {\n if (!collections.some((c) => c.slug === collection.slug)) {\n collections.push(collection);\n }\n };\n\n addIfMissing(buildProductsCollection(slugs.products));\n addIfMissing(buildOrdersCollection(slugs.orders, slugs.customers));\n addIfMissing(buildCustomersCollection(slugs.customers, slugs.users));\n addIfMissing(buildPaymentsCollection(slugs.payments, slugs.orders));\n addIfMissing(buildWebhookEventsCollection(slugs.webhookEvents));\n if (options.includeSubscriptions) {\n addIfMissing(\n buildSubscriptionsCollection(slugs.subscriptions, slugs.customers),\n );\n }\n\n return { ...config, collections };\n };\n}\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n\nimport type { LocalApi } from \"@thebes/cadmus/cms\";\nimport type { Context } from \"hono\";\nimport type { NormalizedWebhookEvent, PaymentProvider } from \"./types.js\";\n\n// biome-ignore lint/suspicious/noExplicitAny: see checkout.ts's identical note\ntype AnyLocalApi<TContext> = LocalApi<any, TContext>;\n\nexport interface WebhookHandlerOptions<TContext> {\n provider: PaymentProvider;\n webhookEvents: AnyLocalApi<TContext>;\n orders: AnyLocalApi<TContext>;\n payments: AnyLocalApi<TContext>;\n /** Only needed if `provider.subscriptions` is wired and the consumer included the optional `subscriptions` collection. */\n subscriptions?: AnyLocalApi<TContext>;\n secret: string;\n /** Some providers (Square) sign over the full notification URL. */\n notificationUrl?: string;\n /**\n * Webhooks are server-to-server calls with no real user session behind\n * them — same reasoning as `@thebes/cadmea-plugin-crm`'s\n * `createContactUpsertHook`'s `context` option. Pass whatever trusted\n * context value the `orders`/`payments`/`webhookEvents` collections'\n * own `access` config accepts for system-level writes.\n */\n context: TContext;\n}\n\n// Matches the exact message text `localApi.ts`'s `wrapWriteError` authors\n// for a unique-constraint failure — Cadmus-internal, a contract this\n// plugin controls indirectly (same precedent as `@thebes/cadmus/hono`'s\n// `mountCmsRoutes`'s own `statusForError`, which matches the same way\n// rather than importing `CadmusCmsError` from the root `@thebes/cadmus`\n// package — see errors.ts's doc comment for why that root import is\n// avoided here).\nfunction isUniqueConstraintError(error: unknown): boolean {\n return (\n error instanceof Error &&\n error.message.includes(\"Unique constraint violated\")\n );\n}\n\nasync function findOneByField<TContext>(\n api: AnyLocalApi<TContext>,\n context: TContext,\n field: string,\n value: string,\n): Promise<Record<string, unknown> | undefined> {\n // In-memory filter after a plain find() rather than a `where`-filtered\n // query — the same \"don't build for scale you don't have\" tradeoff\n // `cadmea-plugin-redirects`/`cadmea-plugin-crm` make elsewhere. Revisit\n // with an indexed lookup if a high-volume store's orders/payments\n // tables make this a measured problem, not a theoretical one.\n const rows = (await api.find(context)) as Array<Record<string, unknown>>;\n return rows.find((row) => row[field] === value);\n}\n\nasync function dispatchEvent<TContext>(\n event: NormalizedWebhookEvent,\n options: WebhookHandlerOptions<TContext>,\n): Promise<void> {\n switch (event.kind) {\n case \"payment.updated\": {\n const payment = await findOneByField(\n options.payments,\n options.context,\n \"providerPaymentRef\",\n event.providerPaymentRef,\n );\n if (payment) {\n await options.payments.update(options.context, payment.id as number, {\n status: event.status,\n });\n }\n const order = await findOneByField(\n options.orders,\n options.context,\n \"providerPaymentRef\",\n event.providerPaymentRef,\n );\n if (order) {\n const status =\n event.status === \"succeeded\"\n ? \"paid\"\n : event.status === \"refunded\"\n ? \"refunded\"\n : \"failed\";\n await options.orders.update(options.context, order.id as number, {\n status,\n });\n }\n return;\n }\n case \"order.updated\": {\n const order = await findOneByField(\n options.orders,\n options.context,\n \"providerOrderRef\",\n event.providerOrderRef,\n );\n if (order) {\n await options.orders.update(options.context, order.id as number, {\n status: event.status,\n });\n }\n return;\n }\n case \"subscription.updated\": {\n if (!options.subscriptions) return;\n const subscription = await findOneByField(\n options.subscriptions,\n options.context,\n \"providerSubscriptionRef\",\n event.providerSubscriptionRef,\n );\n if (subscription) {\n await options.subscriptions.update(\n options.context,\n subscription.id as number,\n { status: event.status },\n );\n }\n return;\n }\n case \"unhandled\":\n return;\n }\n}\n\n/**\n * Returns a Hono handler implementing inbound webhook handling against a\n * `PaymentProvider`: verify signature (raw body, before any parsing) →\n * dedup via the `webhook_events` collection's unique `eventId` constraint\n * → dispatch by normalized event kind. Each step isolated so a handler bug\n * never prevents the 200 response a provider needs to stop retrying — the\n * dedup write IS the source of truth for \"already processed,\" checked\n * before dispatch, not after.\n *\n * Mount alongside `mountCmsRoutes`, same as `createCheckoutHandler` — not\n * part of the generic CMS REST surface.\n */\nexport function createWebhookHandler<TContext>(\n options: WebhookHandlerOptions<TContext>,\n) {\n return async (c: Context): Promise<Response> => {\n // Read raw body as text *before* any JSON parsing — signature is\n // computed over raw bytes, matching @thebes/cadmus/cms's own\n // outbound webhooks.ts HMAC idiom.\n const rawBody = await c.req.text();\n\n const verified = await options.provider.verifyWebhookSignature({\n rawBody,\n headers: c.req.raw.headers,\n secret: options.secret,\n notificationUrl: options.notificationUrl,\n });\n if (!verified) {\n return c.json({ error: \"Invalid signature\" }, 401);\n }\n\n const { eventId, event } = options.provider.parseWebhookEvent(rawBody);\n\n try {\n await options.webhookEvents.create(options.context, {\n provider: options.provider.name,\n eventId,\n eventType: event.kind,\n });\n } catch (error) {\n if (isUniqueConstraintError(error)) {\n // Already processed — the unique constraint is the actual guard,\n // not a preceding find() (avoids a TOCTOU window on concurrent\n // delivery of the same event).\n return c.json({ ok: true, duplicate: true }, 200);\n }\n throw error;\n }\n\n try {\n await dispatchEvent(event, options);\n } catch (error) {\n // A dispatch-handler bug must not cause the provider to retry the\n // whole event (it's already recorded as processed above) — log and\n // move on, same \"each handler isolated\" precedent the reference\n // Square plugin's own webhook.ts follows.\n console.error(\"[cadmea-plugin-ecommerce] webhook dispatch failed\", error);\n }\n\n return c.json({ ok: true }, 200);\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,268 @@
1
+ import { CadmeaPlugin, LocalApi } from "@thebes/cadmus/cms";
2
+ import { Context } from "hono";
3
+
4
+ //#region src/types.d.ts
5
+ /** Money is always integer minor units (cents) + an ISO 4217 currency code —
6
+ * matching both providers' own native representations and every money
7
+ * field on the collections this plugin defines. */
8
+ interface Money {
9
+ amount: number;
10
+ currency: string;
11
+ }
12
+ interface CartLineItem {
13
+ /** Provider-specific catalog identifier (Square variation ID / Stripe price ID), opaque to this plugin. */
14
+ catalogRef: string;
15
+ quantity: number;
16
+ /**
17
+ * Client-submitted price — NEVER trusted as-is. `createCheckoutHandler`
18
+ * re-verifies it against `PaymentProvider.checkCatalogPrices` and rejects
19
+ * the request on any mismatch, rather than silently using the server
20
+ * price (a mismatch is a sign of a tampered request, not a typo to
21
+ * paper over).
22
+ */
23
+ clientUnitPrice: Money;
24
+ }
25
+ interface CheckoutRequest {
26
+ lineItems: CartLineItem[];
27
+ /** Provider-specific tokenized payment source (Square sourceId / Stripe PaymentMethod id) — produced client-side, never raw card data. */
28
+ paymentSourceToken: string;
29
+ customerEmail?: string;
30
+ /** Caller-generated via `crypto.randomUUID()`, per checkout attempt. */
31
+ idempotencyKey: string;
32
+ /** Free-form metadata the provider attaches to its own order/PaymentIntent object. */
33
+ metadata?: Record<string, string>;
34
+ }
35
+ interface CheckoutResult {
36
+ /** Provider's own identifier for the created order/PaymentIntent — stored on the `orders` row. */
37
+ providerOrderRef: string;
38
+ /** Provider's own identifier for the payment/charge — stored on the `payments` row. */
39
+ providerPaymentRef: string;
40
+ status: "succeeded" | "requires_action" | "failed";
41
+ amount: Money;
42
+ /** Raw provider response, JSON-serializable — stored in `payments.rawResponse`. No BigInt: providers that return BigInt-typed amounts (Square's SDK does; its REST API does not) must convert before returning this. */
43
+ raw: Record<string, unknown>;
44
+ }
45
+ /** A price/availability check against the live provider catalog — never trust client-submitted prices for a checkout. */
46
+ interface CatalogPriceCheck {
47
+ catalogRef: string;
48
+ serverUnitPrice: Money;
49
+ /** `undefined` when the provider doesn't track inventory for this item. */
50
+ availableQuantity?: number;
51
+ }
52
+ /**
53
+ * The shape every provider's raw webhook payload is translated into, so
54
+ * webhook-dispatch code in `createWebhookHandler` never branches on
55
+ * provider-specific event-type strings.
56
+ */
57
+ type NormalizedWebhookEvent = {
58
+ kind: "payment.updated";
59
+ providerPaymentRef: string;
60
+ status: "succeeded" | "failed" | "refunded";
61
+ } | {
62
+ kind: "order.updated";
63
+ providerOrderRef: string;
64
+ status: "paid" | "canceled" | "failed";
65
+ } | {
66
+ kind: "subscription.updated";
67
+ providerSubscriptionRef: string;
68
+ status: string;
69
+ } | {
70
+ kind: "unhandled";
71
+ rawType: string;
72
+ };
73
+ interface PaymentProvider {
74
+ readonly name: "square" | "stripe";
75
+ /** Re-verifies cart line item prices/availability against the live provider catalog. */
76
+ checkCatalogPrices(refs: string[]): Promise<CatalogPriceCheck[]>;
77
+ /** Idempotent customer find-or-create. Returns the provider's own customer id. */
78
+ findOrCreateCustomer(email: string, idempotencyKey: string): Promise<string>;
79
+ /** Creates and charges an order/PaymentIntent in one call. */
80
+ checkout(request: CheckoutRequest): Promise<CheckoutResult>;
81
+ /**
82
+ * Verifies an inbound webhook's signature against the raw request.
83
+ * Returns `false` rather than throwing on a bad signature — the caller
84
+ * (`createWebhookHandler`) decides the HTTP response (401).
85
+ */
86
+ verifyWebhookSignature(args: {
87
+ rawBody: string;
88
+ headers: Headers;
89
+ secret: string; /** Some providers (Square) sign over the full notification URL, not just the body — pass it through unconditionally; providers that don't need it (Stripe) ignore the field. */
90
+ notificationUrl?: string;
91
+ }): Promise<boolean>;
92
+ /**
93
+ * Parses an already-signature-verified raw webhook body into a
94
+ * normalized event, and extracts the provider's own event id for dedup
95
+ * against the `webhook_events` collection.
96
+ */
97
+ parseWebhookEvent(rawBody: string): {
98
+ eventId: string;
99
+ event: NormalizedWebhookEvent;
100
+ };
101
+ /**
102
+ * Optional capability — providers that don't model a syncable catalog
103
+ * omit this; `createCheckoutHandler` only ever calls
104
+ * `checkCatalogPrices` (always required), not this.
105
+ */
106
+ catalogSync?: {
107
+ listCatalogItems(): Promise<Array<{
108
+ catalogRef: string;
109
+ name: string;
110
+ unitPrice: Money;
111
+ sku?: string;
112
+ }>>;
113
+ };
114
+ /**
115
+ * Optional capability — Square and Stripe model recurring billing
116
+ * differently enough (loyalty/membership-style recurring orders vs. a
117
+ * native Subscriptions object) that this is never assumed present.
118
+ */
119
+ subscriptions?: {
120
+ create(args: {
121
+ customerRef: string;
122
+ planRef: string;
123
+ idempotencyKey: string;
124
+ }): Promise<{
125
+ providerSubscriptionRef: string;
126
+ status: string;
127
+ }>;
128
+ cancel(providerSubscriptionRef: string): Promise<void>;
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region src/checkout.d.ts
133
+ type AnyLocalApi$1<TContext> = LocalApi<any, TContext>;
134
+ interface CheckoutRequestBody {
135
+ lineItems: CartLineItem[];
136
+ paymentSourceToken: string;
137
+ customerEmail?: string;
138
+ idempotencyKey: string;
139
+ shippingAddress?: Record<string, string | undefined>;
140
+ metadata?: Record<string, string>;
141
+ }
142
+ interface CheckoutHandlerOptions<TContext> {
143
+ provider: PaymentProvider;
144
+ orders: AnyLocalApi$1<TContext>;
145
+ payments: AnyLocalApi$1<TContext>;
146
+ /**
147
+ * Resolves the per-request access context passed to `orders`/`payments`
148
+ * — called once per request, the same shape and timing as
149
+ * `@thebes/cadmus/hono`'s `mountCmsRoutes`'s own `resolveContext`. This
150
+ * is a real customer-initiated HTTP request (unlike a `CollectionHooks`
151
+ * hook), so a real per-request context is available here, not a fixed
152
+ * trusted value.
153
+ */
154
+ resolveContext: (c: Context) => Promise<TContext> | TContext;
155
+ rateLimit?: {
156
+ kv: KVNamespace;
157
+ limit: number;
158
+ windowSeconds: number; /** Default: "checkout". */
159
+ keyPrefix?: string;
160
+ };
161
+ }
162
+ /**
163
+ * Returns a Hono handler implementing the checkout flow against a
164
+ * `PaymentProvider`: rate limit → re-verify cart prices/availability
165
+ * (never trust client-submitted prices) → idempotent customer find-or-
166
+ * create → charge → persist `orders`/`payments` rows. A DB-write failure
167
+ * *after* a successful charge degrades to a 200-with-warning response,
168
+ * never a false "payment failed" — the customer's card was actually
169
+ * charged, telling them otherwise would be worse than a delayed manual
170
+ * reconciliation.
171
+ *
172
+ * Mount it as a plain Hono route alongside `mountCmsRoutes` — checkout
173
+ * isn't part of the generic CMS REST surface that function mounts.
174
+ *
175
+ * ```ts
176
+ * app.post("/api/checkout", createCheckoutHandler({ provider, orders, payments, resolveContext }));
177
+ * ```
178
+ */
179
+ declare function createCheckoutHandler<TContext>(options: CheckoutHandlerOptions<TContext>): (c: Context) => Promise<Response>;
180
+ //#endregion
181
+ //#region src/collections.d.ts
182
+ interface EcommercePluginOptions {
183
+ productsSlug?: string;
184
+ ordersSlug?: string;
185
+ customersSlug?: string;
186
+ paymentsSlug?: string;
187
+ webhookEventsSlug?: string;
188
+ subscriptionsSlug?: string;
189
+ /** What `customers.linkedUser` relates to. Default: "users". */
190
+ usersSlug?: string;
191
+ /**
192
+ * Adds the `subscriptions` collection. Default: false — Square and
193
+ * Stripe model recurring billing differently enough
194
+ * (`PaymentProvider.subscriptions` is itself optional) that this
195
+ * collection is opt-in, not assumed needed by every store.
196
+ */
197
+ includeSubscriptions?: boolean;
198
+ }
199
+ /**
200
+ * Returns a Cadmea plugin that adds the provider-agnostic ecommerce
201
+ * collections — a no-op for any collection slug already present, the same
202
+ * idempotent-add convention `cadmea-plugin-redirects`/`cadmea-plugin-crm`
203
+ * use.
204
+ */
205
+ declare function ecommercePlugin(options?: EcommercePluginOptions): CadmeaPlugin;
206
+ //#endregion
207
+ //#region src/errors.d.ts
208
+ /**
209
+ * Thrown by `createCheckoutHandler` on price-mismatch/inventory rejection,
210
+ * and by `PaymentProvider` implementations on charge failure.
211
+ * `createCheckoutHandler` catches it internally (`instanceof
212
+ * CadmeaPaymentError`) and maps it to HTTP 402 itself — checkout/webhook
213
+ * handlers are plain Hono routes, never mounted through
214
+ * `@thebes/cadmus/hono`'s `mountCmsRoutes`, so there's no shared
215
+ * error-to-status pipeline this needs to participate in.
216
+ *
217
+ * Deliberately a plain `Error` subclass, not a `CadmusCmsError` one — this
218
+ * is a Cadmea-plugin-owned error, not a Cadmus-primitive one (every real
219
+ * `CadmusError` subclass is owned by a `packages/cadmus/src/<primitive>/`
220
+ * folder; a payment error belongs to a plugin, not a primitive), and
221
+ * `CadmusCmsError` is only reachable via the root `@thebes/cadmus` package
222
+ * export (not `@thebes/cadmus/cms`) — importing that root barrel here
223
+ * would pull in every other primitive's runtime code (including
224
+ * Workers-only modules like `cloudflare:email`) just for one base class.
225
+ * Keeping this plugin-local and dependency-free is the honest shape.
226
+ */
227
+ declare class CadmeaPaymentError extends Error {
228
+ readonly cause?: unknown | undefined;
229
+ constructor(message: string, cause?: unknown | undefined);
230
+ }
231
+ //#endregion
232
+ //#region src/webhook.d.ts
233
+ type AnyLocalApi<TContext> = LocalApi<any, TContext>;
234
+ interface WebhookHandlerOptions<TContext> {
235
+ provider: PaymentProvider;
236
+ webhookEvents: AnyLocalApi<TContext>;
237
+ orders: AnyLocalApi<TContext>;
238
+ payments: AnyLocalApi<TContext>;
239
+ /** Only needed if `provider.subscriptions` is wired and the consumer included the optional `subscriptions` collection. */
240
+ subscriptions?: AnyLocalApi<TContext>;
241
+ secret: string;
242
+ /** Some providers (Square) sign over the full notification URL. */
243
+ notificationUrl?: string;
244
+ /**
245
+ * Webhooks are server-to-server calls with no real user session behind
246
+ * them — same reasoning as `@thebes/cadmea-plugin-crm`'s
247
+ * `createContactUpsertHook`'s `context` option. Pass whatever trusted
248
+ * context value the `orders`/`payments`/`webhookEvents` collections'
249
+ * own `access` config accepts for system-level writes.
250
+ */
251
+ context: TContext;
252
+ }
253
+ /**
254
+ * Returns a Hono handler implementing inbound webhook handling against a
255
+ * `PaymentProvider`: verify signature (raw body, before any parsing) →
256
+ * dedup via the `webhook_events` collection's unique `eventId` constraint
257
+ * → dispatch by normalized event kind. Each step isolated so a handler bug
258
+ * never prevents the 200 response a provider needs to stop retrying — the
259
+ * dedup write IS the source of truth for "already processed," checked
260
+ * before dispatch, not after.
261
+ *
262
+ * Mount alongside `mountCmsRoutes`, same as `createCheckoutHandler` — not
263
+ * part of the generic CMS REST surface.
264
+ */
265
+ declare function createWebhookHandler<TContext>(options: WebhookHandlerOptions<TContext>): (c: Context) => Promise<Response>;
266
+ //#endregion
267
+ export { CadmeaPaymentError, CartLineItem, CatalogPriceCheck, CheckoutHandlerOptions, CheckoutRequest, CheckoutRequestBody, CheckoutResult, EcommercePluginOptions, Money, NormalizedWebhookEvent, PaymentProvider, WebhookHandlerOptions, createCheckoutHandler, createWebhookHandler, ecommercePlugin };
268
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/checkout.ts","../src/collections.ts","../src/errors.ts","../src/webhook.ts"],"mappings":"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"}
@@ -0,0 +1,268 @@
1
+ import { CadmeaPlugin, LocalApi } from "@thebes/cadmus/cms";
2
+ import { Context } from "hono";
3
+
4
+ //#region src/types.d.ts
5
+ /** Money is always integer minor units (cents) + an ISO 4217 currency code —
6
+ * matching both providers' own native representations and every money
7
+ * field on the collections this plugin defines. */
8
+ interface Money {
9
+ amount: number;
10
+ currency: string;
11
+ }
12
+ interface CartLineItem {
13
+ /** Provider-specific catalog identifier (Square variation ID / Stripe price ID), opaque to this plugin. */
14
+ catalogRef: string;
15
+ quantity: number;
16
+ /**
17
+ * Client-submitted price — NEVER trusted as-is. `createCheckoutHandler`
18
+ * re-verifies it against `PaymentProvider.checkCatalogPrices` and rejects
19
+ * the request on any mismatch, rather than silently using the server
20
+ * price (a mismatch is a sign of a tampered request, not a typo to
21
+ * paper over).
22
+ */
23
+ clientUnitPrice: Money;
24
+ }
25
+ interface CheckoutRequest {
26
+ lineItems: CartLineItem[];
27
+ /** Provider-specific tokenized payment source (Square sourceId / Stripe PaymentMethod id) — produced client-side, never raw card data. */
28
+ paymentSourceToken: string;
29
+ customerEmail?: string;
30
+ /** Caller-generated via `crypto.randomUUID()`, per checkout attempt. */
31
+ idempotencyKey: string;
32
+ /** Free-form metadata the provider attaches to its own order/PaymentIntent object. */
33
+ metadata?: Record<string, string>;
34
+ }
35
+ interface CheckoutResult {
36
+ /** Provider's own identifier for the created order/PaymentIntent — stored on the `orders` row. */
37
+ providerOrderRef: string;
38
+ /** Provider's own identifier for the payment/charge — stored on the `payments` row. */
39
+ providerPaymentRef: string;
40
+ status: "succeeded" | "requires_action" | "failed";
41
+ amount: Money;
42
+ /** Raw provider response, JSON-serializable — stored in `payments.rawResponse`. No BigInt: providers that return BigInt-typed amounts (Square's SDK does; its REST API does not) must convert before returning this. */
43
+ raw: Record<string, unknown>;
44
+ }
45
+ /** A price/availability check against the live provider catalog — never trust client-submitted prices for a checkout. */
46
+ interface CatalogPriceCheck {
47
+ catalogRef: string;
48
+ serverUnitPrice: Money;
49
+ /** `undefined` when the provider doesn't track inventory for this item. */
50
+ availableQuantity?: number;
51
+ }
52
+ /**
53
+ * The shape every provider's raw webhook payload is translated into, so
54
+ * webhook-dispatch code in `createWebhookHandler` never branches on
55
+ * provider-specific event-type strings.
56
+ */
57
+ type NormalizedWebhookEvent = {
58
+ kind: "payment.updated";
59
+ providerPaymentRef: string;
60
+ status: "succeeded" | "failed" | "refunded";
61
+ } | {
62
+ kind: "order.updated";
63
+ providerOrderRef: string;
64
+ status: "paid" | "canceled" | "failed";
65
+ } | {
66
+ kind: "subscription.updated";
67
+ providerSubscriptionRef: string;
68
+ status: string;
69
+ } | {
70
+ kind: "unhandled";
71
+ rawType: string;
72
+ };
73
+ interface PaymentProvider {
74
+ readonly name: "square" | "stripe";
75
+ /** Re-verifies cart line item prices/availability against the live provider catalog. */
76
+ checkCatalogPrices(refs: string[]): Promise<CatalogPriceCheck[]>;
77
+ /** Idempotent customer find-or-create. Returns the provider's own customer id. */
78
+ findOrCreateCustomer(email: string, idempotencyKey: string): Promise<string>;
79
+ /** Creates and charges an order/PaymentIntent in one call. */
80
+ checkout(request: CheckoutRequest): Promise<CheckoutResult>;
81
+ /**
82
+ * Verifies an inbound webhook's signature against the raw request.
83
+ * Returns `false` rather than throwing on a bad signature — the caller
84
+ * (`createWebhookHandler`) decides the HTTP response (401).
85
+ */
86
+ verifyWebhookSignature(args: {
87
+ rawBody: string;
88
+ headers: Headers;
89
+ secret: string; /** Some providers (Square) sign over the full notification URL, not just the body — pass it through unconditionally; providers that don't need it (Stripe) ignore the field. */
90
+ notificationUrl?: string;
91
+ }): Promise<boolean>;
92
+ /**
93
+ * Parses an already-signature-verified raw webhook body into a
94
+ * normalized event, and extracts the provider's own event id for dedup
95
+ * against the `webhook_events` collection.
96
+ */
97
+ parseWebhookEvent(rawBody: string): {
98
+ eventId: string;
99
+ event: NormalizedWebhookEvent;
100
+ };
101
+ /**
102
+ * Optional capability — providers that don't model a syncable catalog
103
+ * omit this; `createCheckoutHandler` only ever calls
104
+ * `checkCatalogPrices` (always required), not this.
105
+ */
106
+ catalogSync?: {
107
+ listCatalogItems(): Promise<Array<{
108
+ catalogRef: string;
109
+ name: string;
110
+ unitPrice: Money;
111
+ sku?: string;
112
+ }>>;
113
+ };
114
+ /**
115
+ * Optional capability — Square and Stripe model recurring billing
116
+ * differently enough (loyalty/membership-style recurring orders vs. a
117
+ * native Subscriptions object) that this is never assumed present.
118
+ */
119
+ subscriptions?: {
120
+ create(args: {
121
+ customerRef: string;
122
+ planRef: string;
123
+ idempotencyKey: string;
124
+ }): Promise<{
125
+ providerSubscriptionRef: string;
126
+ status: string;
127
+ }>;
128
+ cancel(providerSubscriptionRef: string): Promise<void>;
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region src/checkout.d.ts
133
+ type AnyLocalApi$1<TContext> = LocalApi<any, TContext>;
134
+ interface CheckoutRequestBody {
135
+ lineItems: CartLineItem[];
136
+ paymentSourceToken: string;
137
+ customerEmail?: string;
138
+ idempotencyKey: string;
139
+ shippingAddress?: Record<string, string | undefined>;
140
+ metadata?: Record<string, string>;
141
+ }
142
+ interface CheckoutHandlerOptions<TContext> {
143
+ provider: PaymentProvider;
144
+ orders: AnyLocalApi$1<TContext>;
145
+ payments: AnyLocalApi$1<TContext>;
146
+ /**
147
+ * Resolves the per-request access context passed to `orders`/`payments`
148
+ * — called once per request, the same shape and timing as
149
+ * `@thebes/cadmus/hono`'s `mountCmsRoutes`'s own `resolveContext`. This
150
+ * is a real customer-initiated HTTP request (unlike a `CollectionHooks`
151
+ * hook), so a real per-request context is available here, not a fixed
152
+ * trusted value.
153
+ */
154
+ resolveContext: (c: Context) => Promise<TContext> | TContext;
155
+ rateLimit?: {
156
+ kv: KVNamespace;
157
+ limit: number;
158
+ windowSeconds: number; /** Default: "checkout". */
159
+ keyPrefix?: string;
160
+ };
161
+ }
162
+ /**
163
+ * Returns a Hono handler implementing the checkout flow against a
164
+ * `PaymentProvider`: rate limit → re-verify cart prices/availability
165
+ * (never trust client-submitted prices) → idempotent customer find-or-
166
+ * create → charge → persist `orders`/`payments` rows. A DB-write failure
167
+ * *after* a successful charge degrades to a 200-with-warning response,
168
+ * never a false "payment failed" — the customer's card was actually
169
+ * charged, telling them otherwise would be worse than a delayed manual
170
+ * reconciliation.
171
+ *
172
+ * Mount it as a plain Hono route alongside `mountCmsRoutes` — checkout
173
+ * isn't part of the generic CMS REST surface that function mounts.
174
+ *
175
+ * ```ts
176
+ * app.post("/api/checkout", createCheckoutHandler({ provider, orders, payments, resolveContext }));
177
+ * ```
178
+ */
179
+ declare function createCheckoutHandler<TContext>(options: CheckoutHandlerOptions<TContext>): (c: Context) => Promise<Response>;
180
+ //#endregion
181
+ //#region src/collections.d.ts
182
+ interface EcommercePluginOptions {
183
+ productsSlug?: string;
184
+ ordersSlug?: string;
185
+ customersSlug?: string;
186
+ paymentsSlug?: string;
187
+ webhookEventsSlug?: string;
188
+ subscriptionsSlug?: string;
189
+ /** What `customers.linkedUser` relates to. Default: "users". */
190
+ usersSlug?: string;
191
+ /**
192
+ * Adds the `subscriptions` collection. Default: false — Square and
193
+ * Stripe model recurring billing differently enough
194
+ * (`PaymentProvider.subscriptions` is itself optional) that this
195
+ * collection is opt-in, not assumed needed by every store.
196
+ */
197
+ includeSubscriptions?: boolean;
198
+ }
199
+ /**
200
+ * Returns a Cadmea plugin that adds the provider-agnostic ecommerce
201
+ * collections — a no-op for any collection slug already present, the same
202
+ * idempotent-add convention `cadmea-plugin-redirects`/`cadmea-plugin-crm`
203
+ * use.
204
+ */
205
+ declare function ecommercePlugin(options?: EcommercePluginOptions): CadmeaPlugin;
206
+ //#endregion
207
+ //#region src/errors.d.ts
208
+ /**
209
+ * Thrown by `createCheckoutHandler` on price-mismatch/inventory rejection,
210
+ * and by `PaymentProvider` implementations on charge failure.
211
+ * `createCheckoutHandler` catches it internally (`instanceof
212
+ * CadmeaPaymentError`) and maps it to HTTP 402 itself — checkout/webhook
213
+ * handlers are plain Hono routes, never mounted through
214
+ * `@thebes/cadmus/hono`'s `mountCmsRoutes`, so there's no shared
215
+ * error-to-status pipeline this needs to participate in.
216
+ *
217
+ * Deliberately a plain `Error` subclass, not a `CadmusCmsError` one — this
218
+ * is a Cadmea-plugin-owned error, not a Cadmus-primitive one (every real
219
+ * `CadmusError` subclass is owned by a `packages/cadmus/src/<primitive>/`
220
+ * folder; a payment error belongs to a plugin, not a primitive), and
221
+ * `CadmusCmsError` is only reachable via the root `@thebes/cadmus` package
222
+ * export (not `@thebes/cadmus/cms`) — importing that root barrel here
223
+ * would pull in every other primitive's runtime code (including
224
+ * Workers-only modules like `cloudflare:email`) just for one base class.
225
+ * Keeping this plugin-local and dependency-free is the honest shape.
226
+ */
227
+ declare class CadmeaPaymentError extends Error {
228
+ readonly cause?: unknown | undefined;
229
+ constructor(message: string, cause?: unknown | undefined);
230
+ }
231
+ //#endregion
232
+ //#region src/webhook.d.ts
233
+ type AnyLocalApi<TContext> = LocalApi<any, TContext>;
234
+ interface WebhookHandlerOptions<TContext> {
235
+ provider: PaymentProvider;
236
+ webhookEvents: AnyLocalApi<TContext>;
237
+ orders: AnyLocalApi<TContext>;
238
+ payments: AnyLocalApi<TContext>;
239
+ /** Only needed if `provider.subscriptions` is wired and the consumer included the optional `subscriptions` collection. */
240
+ subscriptions?: AnyLocalApi<TContext>;
241
+ secret: string;
242
+ /** Some providers (Square) sign over the full notification URL. */
243
+ notificationUrl?: string;
244
+ /**
245
+ * Webhooks are server-to-server calls with no real user session behind
246
+ * them — same reasoning as `@thebes/cadmea-plugin-crm`'s
247
+ * `createContactUpsertHook`'s `context` option. Pass whatever trusted
248
+ * context value the `orders`/`payments`/`webhookEvents` collections'
249
+ * own `access` config accepts for system-level writes.
250
+ */
251
+ context: TContext;
252
+ }
253
+ /**
254
+ * Returns a Hono handler implementing inbound webhook handling against a
255
+ * `PaymentProvider`: verify signature (raw body, before any parsing) →
256
+ * dedup via the `webhook_events` collection's unique `eventId` constraint
257
+ * → dispatch by normalized event kind. Each step isolated so a handler bug
258
+ * never prevents the 200 response a provider needs to stop retrying — the
259
+ * dedup write IS the source of truth for "already processed," checked
260
+ * before dispatch, not after.
261
+ *
262
+ * Mount alongside `mountCmsRoutes`, same as `createCheckoutHandler` — not
263
+ * part of the generic CMS REST surface.
264
+ */
265
+ declare function createWebhookHandler<TContext>(options: WebhookHandlerOptions<TContext>): (c: Context) => Promise<Response>;
266
+ //#endregion
267
+ export { CadmeaPaymentError, CartLineItem, CatalogPriceCheck, CheckoutHandlerOptions, CheckoutRequest, CheckoutRequestBody, CheckoutResult, EcommercePluginOptions, Money, NormalizedWebhookEvent, PaymentProvider, WebhookHandlerOptions, createCheckoutHandler, createWebhookHandler, ecommercePlugin };
268
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/checkout.ts","../src/collections.ts","../src/errors.ts","../src/webhook.ts"],"mappings":"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"}