@umec/core 0.1.0-alpha.5 → 0.1.0-alpha.6

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.
@@ -9,6 +9,7 @@ type CheckoutOffer = {
9
9
  freeShipping: boolean;
10
10
  customFields?: CheckoutCustomField[];
11
11
  metadata?: Record<string, string>;
12
+ customerEmail?: string;
12
13
  unitsRemaining?: Map<string, number>;
13
14
  };
14
15
  type CheckoutOfferUnavailable = {
@@ -22,12 +23,13 @@ type CheckoutHandlerOptions = {
22
23
  shipping?: UmecConfig["shipping"];
23
24
  customFields?: CheckoutCustomField[];
24
25
  metadata?: Record<string, string>;
26
+ customerEmail?: string;
25
27
  checkStock?: CheckStock;
26
28
  checkOffer?: CheckOffer;
27
29
  };
28
30
  type CheckoutHandler = ({ request }: {
29
31
  request: Request;
30
32
  }) => Promise<Response>;
31
- declare function createCheckoutHandler({ products, shipping, customFields: staticCustomFields, metadata: staticMetadata, checkStock, checkOffer, }: CheckoutHandlerOptions): CheckoutHandler;
33
+ declare function createCheckoutHandler({ products, shipping, customFields: staticCustomFields, metadata: staticMetadata, customerEmail: staticCustomerEmail, checkStock, checkOffer, }: CheckoutHandlerOptions): CheckoutHandler;
32
34
 
33
35
  export { type CheckOffer, type CheckStock, type CheckoutCustomField, type CheckoutHandler, type CheckoutHandlerOptions, type CheckoutOffer, type CheckoutOfferUnavailable, type CheckoutProductsMap, createCheckoutHandler };
package/dist/checkout.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createCheckoutHandler
3
- } from "./chunk-XSJYDBH7.js";
3
+ } from "./chunk-2EBXJT3M.js";
4
4
  import "./chunk-7TJANK6S.js";
5
5
  export {
6
6
  createCheckoutHandler
@@ -44,6 +44,7 @@ function createCheckoutHandler({
44
44
  shipping,
45
45
  customFields: staticCustomFields = [],
46
46
  metadata: staticMetadata = {},
47
+ customerEmail: staticCustomerEmail,
47
48
  checkStock,
48
49
  checkOffer
49
50
  }) {
@@ -86,6 +87,7 @@ function createCheckoutHandler({
86
87
  let freeShipping = false;
87
88
  let offerCustomFields = [];
88
89
  let offerMetadata = {};
90
+ let customerEmail = staticCustomerEmail;
89
91
  if (typeof offerSlug === "string" && offerSlug.trim() !== "") {
90
92
  if (!checkOffer) return jsonError("Offer not available", 503);
91
93
  const offer = await checkOffer(offerSlug.trim());
@@ -104,6 +106,7 @@ function createCheckoutHandler({
104
106
  freeShipping = offer.freeShipping;
105
107
  offerCustomFields = offer.customFields ?? [];
106
108
  offerMetadata = offer.metadata ?? {};
109
+ customerEmail = offer.customerEmail ?? staticCustomerEmail;
107
110
  }
108
111
  const shippingRate = shipping ? freeShipping ? { amount: 0, label: shipping.freeLabel } : calcShipping(checkoutItems, products, shipping) : void 0;
109
112
  const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : /* @__PURE__ */ new Map();
@@ -142,6 +145,7 @@ function createCheckoutHandler({
142
145
  items: JSON.stringify(checkoutItems),
143
146
  ...typeof offerSlug === "string" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}
144
147
  },
148
+ ...customerEmail ? { customer_email: customerEmail } : {},
145
149
  phone_number_collection: { enabled: true },
146
150
  custom_fields: checkoutCustomFields,
147
151
  shipping_address_collection: { allowed_countries: ["JP"] },
@@ -185,4 +189,4 @@ function createCheckoutHandler({
185
189
  export {
186
190
  createCheckoutHandler
187
191
  };
188
- //# sourceMappingURL=chunk-XSJYDBH7.js.map
192
+ //# sourceMappingURL=chunk-2EBXJT3M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/checkout.ts"],"sourcesContent":["import Stripe from \"stripe\";\nimport type { UmecConfig, UmecProduct } from \"./config.js\";\nimport { getServerEnv } from \"./env.js\";\n\nconst STRIPE_API_VERSION = \"2026-05-27.dahlia\";\nconst MAX_ITEMS = 10;\nconst MAX_QUANTITY = 20;\n\ntype CheckoutItem = {\n sku: string;\n quantity: number;\n};\n\nexport type CheckoutProductsMap = Record<string, UmecProduct | undefined>;\n\nexport type CheckoutCustomField = Stripe.Checkout.SessionCreateParams.CustomField;\n\nexport type CheckoutOffer = {\n priceOverrides: Record<string, number>;\n freeShipping: boolean;\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n customerEmail?: string;\n unitsRemaining?: Map<string, number>;\n};\n\nexport type CheckoutOfferUnavailable = {\n error: \"Offer not found\" | \"Offer expired\";\n status: 404 | 410;\n};\n\nexport type CheckStock = (skus: string[]) => Promise<Map<string, number>>;\nexport type CheckOffer = (offerSlug: string) => Promise<CheckoutOffer | CheckoutOfferUnavailable | null>;\n\nexport type CheckoutHandlerOptions = {\n products: CheckoutProductsMap;\n shipping?: UmecConfig[\"shipping\"];\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n customerEmail?: string;\n checkStock?: CheckStock;\n checkOffer?: CheckOffer;\n};\n\nexport type CheckoutHandler = ({ request }: { request: Request }) => Promise<Response>;\n\nfunction jsonError(error: string, status: number) {\n return new Response(JSON.stringify({ error }), {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nfunction isCheckoutPayload(value: unknown): value is { items?: unknown; offerSlug?: unknown } {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isValidQuantity(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1;\n}\n\nfunction isOfferUnavailable(value: CheckoutOffer | CheckoutOfferUnavailable): value is CheckoutOfferUnavailable {\n return \"error\" in value && \"status\" in value;\n}\n\nfunction omitReservedMetadata(metadata: Record<string, string>) {\n return Object.fromEntries(\n Object.entries(metadata).filter(([key]) => key !== \"items\" && key !== \"offer_slug\"),\n );\n}\n\nfunction calcShipping(\n items: CheckoutItem[],\n products: CheckoutProductsMap,\n shipping: NonNullable<UmecConfig[\"shipping\"]>,\n) {\n const totalUnits = items.reduce((sum, { sku, quantity }) => {\n return sum + (products[sku]?.shippingUnits ?? 1) * quantity;\n }, 0);\n\n const tier = shipping.tiers.find((shippingTier) => {\n return shippingTier.maxUnits === undefined || totalUnits <= shippingTier.maxUnits;\n }) ?? shipping.tiers[shipping.tiers.length - 1]!;\n\n return {\n amount: tier.amount,\n label: tier.label,\n };\n}\n\nexport function createCheckoutHandler({\n products,\n shipping,\n customFields: staticCustomFields = [],\n metadata: staticMetadata = {},\n customerEmail: staticCustomerEmail,\n checkStock,\n checkOffer,\n}: CheckoutHandlerOptions): CheckoutHandler {\n return async ({ request }) => {\n const { STRIPE_SECRET_KEY: stripeKey } = getServerEnv();\n if (!stripeKey) return jsonError(\"Stripe not configured\", 503);\n\n const stripe = new Stripe(stripeKey, {\n apiVersion: STRIPE_API_VERSION,\n });\n let payload: unknown;\n\n try {\n payload = await request.json();\n } catch {\n return jsonError(\"Invalid request body\", 400);\n }\n\n if (!isCheckoutPayload(payload)) return jsonError(\"Invalid request body\", 400);\n\n const { items } = payload;\n if (!Array.isArray(items) || items.length === 0) return jsonError(\"Invalid items\", 400);\n if (items.length > MAX_ITEMS) return jsonError(\"Invalid items\", 400);\n\n const rawItems: CheckoutItem[] = [];\n for (const item of items) {\n if (typeof item !== \"object\" || item === null) return jsonError(\"Invalid SKU\", 400);\n\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n if (typeof sku !== \"string\" || !products[sku]) return jsonError(\"Invalid SKU\", 400);\n if (!isValidQuantity(quantity)) return jsonError(\"Invalid quantity\", 400);\n\n rawItems.push({ sku, quantity });\n }\n\n const quantityMap = new Map<string, number>();\n for (const { sku, quantity } of rawItems) {\n quantityMap.set(sku, (quantityMap.get(sku) ?? 0) + quantity);\n }\n\n for (const [, total] of quantityMap) {\n if (total > MAX_QUANTITY) return jsonError(\"Invalid quantity\", 400);\n }\n\n const checkoutItems: CheckoutItem[] = Array.from(quantityMap.entries()).map(\n ([sku, quantity]) => ({ sku, quantity }),\n );\n const { offerSlug } = payload as { items?: unknown; offerSlug?: unknown };\n let priceOverrides: Record<string, number> = {};\n let freeShipping = false;\n let offerCustomFields: CheckoutCustomField[] = [];\n let offerMetadata: Record<string, string> = {};\n let customerEmail = staticCustomerEmail;\n\n if (typeof offerSlug === \"string\" && offerSlug.trim() !== \"\") {\n if (!checkOffer) return jsonError(\"Offer not available\", 503);\n const offer = await checkOffer(offerSlug.trim());\n if (!offer) return jsonError(\"Offer not found\", 404);\n if (isOfferUnavailable(offer)) return jsonError(offer.error, offer.status);\n const offerUnits = offer.unitsRemaining;\n if (offerUnits && offerUnits.size > 0) {\n for (const { sku, quantity } of checkoutItems) {\n const remaining = offerUnits.get(sku);\n // TOCTOU race is acceptable for low-frequency sales. Move to Durable Objects if needed.\n if (remaining !== undefined && quantity > remaining) {\n return jsonError(`Offer unit limit reached: ${sku}`, 409);\n }\n }\n }\n priceOverrides = offer.priceOverrides;\n freeShipping = offer.freeShipping;\n offerCustomFields = offer.customFields ?? [];\n offerMetadata = offer.metadata ?? {};\n customerEmail = offer.customerEmail ?? staticCustomerEmail;\n }\n\n const shippingRate = shipping\n ? freeShipping\n ? { amount: 0, label: shipping.freeLabel }\n : calcShipping(checkoutItems, products, shipping)\n : undefined;\n const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : new Map<string, number>();\n for (const { sku, quantity } of checkoutItems) {\n const available = stock.get(sku);\n if (available !== undefined && quantity > available) {\n return jsonError(`Out of stock: ${sku}`, 409);\n }\n }\n\n const checkoutCustomFields: CheckoutCustomField[] = [\n ...staticCustomFields,\n ...offerCustomFields,\n {\n key: \"message\",\n label: { type: \"custom\", custom: \"メッセージ・ご要望(任意)\" },\n type: \"text\",\n optional: true,\n text: { maximum_length: 255 },\n },\n ];\n\n try {\n const sessionParams: Stripe.Checkout.SessionCreateParams = {\n ui_mode: \"embedded_page\",\n mode: \"payment\",\n line_items: checkoutItems.map(({ sku, quantity }) => ({\n price_data: {\n currency: \"jpy\",\n product_data: { name: products[sku]!.name },\n unit_amount: priceOverrides[sku] ?? products[sku]!.price,\n },\n quantity,\n })),\n metadata: {\n ...omitReservedMetadata(staticMetadata),\n ...omitReservedMetadata(offerMetadata),\n items: JSON.stringify(checkoutItems),\n ...(typeof offerSlug === \"string\" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}),\n },\n ...(customerEmail ? { customer_email: customerEmail } : {}),\n phone_number_collection: { enabled: true },\n custom_fields: checkoutCustomFields,\n shipping_address_collection: { allowed_countries: [\"JP\"] },\n ...(shippingRate\n ? {\n shipping_options: [\n {\n shipping_rate_data: {\n type: \"fixed_amount\",\n fixed_amount: { amount: shippingRate.amount, currency: \"jpy\" },\n display_name: shippingRate.label,\n delivery_estimate: {\n minimum: { unit: \"business_day\", value: 2 },\n maximum: { unit: \"business_day\", value: 5 },\n },\n },\n },\n ],\n }\n : {}),\n redirect_on_completion: \"never\",\n };\n\n const session = await stripe.checkout.sessions.create(sessionParams);\n\n return new Response(JSON.stringify({ clientSecret: session.client_secret }), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (error) {\n const stripeError = error as {\n type?: string;\n code?: string;\n statusCode?: number;\n requestId?: string;\n };\n console.error(\"[checkout] stripe session create failed:\", {\n type: stripeError.type,\n code: stripeError.code,\n statusCode: stripeError.statusCode,\n requestId: stripeError.requestId,\n });\n\n if (stripeError.type === \"StripePermissionError\") {\n return jsonError(\"Stripe key does not have permission to create Checkout Sessions\", 503);\n }\n\n return jsonError(\"Checkout session could not be created\", 503);\n }\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-2SNGXEIL.js";
8
8
  import {
9
9
  createCheckoutHandler
10
- } from "./chunk-XSJYDBH7.js";
10
+ } from "./chunk-2EBXJT3M.js";
11
11
  import {
12
12
  defineUmecConfig,
13
13
  formatEmailFrom,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umec/core",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "Headless EC logic for umec — config schema and shared handlers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/checkout.ts"],"sourcesContent":["import Stripe from \"stripe\";\nimport type { UmecConfig, UmecProduct } from \"./config.js\";\nimport { getServerEnv } from \"./env.js\";\n\nconst STRIPE_API_VERSION = \"2026-05-27.dahlia\";\nconst MAX_ITEMS = 10;\nconst MAX_QUANTITY = 20;\n\ntype CheckoutItem = {\n sku: string;\n quantity: number;\n};\n\nexport type CheckoutProductsMap = Record<string, UmecProduct | undefined>;\n\nexport type CheckoutCustomField = Stripe.Checkout.SessionCreateParams.CustomField;\n\nexport type CheckoutOffer = {\n priceOverrides: Record<string, number>;\n freeShipping: boolean;\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n unitsRemaining?: Map<string, number>;\n};\n\nexport type CheckoutOfferUnavailable = {\n error: \"Offer not found\" | \"Offer expired\";\n status: 404 | 410;\n};\n\nexport type CheckStock = (skus: string[]) => Promise<Map<string, number>>;\nexport type CheckOffer = (offerSlug: string) => Promise<CheckoutOffer | CheckoutOfferUnavailable | null>;\n\nexport type CheckoutHandlerOptions = {\n products: CheckoutProductsMap;\n shipping?: UmecConfig[\"shipping\"];\n customFields?: CheckoutCustomField[];\n metadata?: Record<string, string>;\n checkStock?: CheckStock;\n checkOffer?: CheckOffer;\n};\n\nexport type CheckoutHandler = ({ request }: { request: Request }) => Promise<Response>;\n\nfunction jsonError(error: string, status: number) {\n return new Response(JSON.stringify({ error }), {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nfunction isCheckoutPayload(value: unknown): value is { items?: unknown; offerSlug?: unknown } {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isValidQuantity(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1;\n}\n\nfunction isOfferUnavailable(value: CheckoutOffer | CheckoutOfferUnavailable): value is CheckoutOfferUnavailable {\n return \"error\" in value && \"status\" in value;\n}\n\nfunction omitReservedMetadata(metadata: Record<string, string>) {\n return Object.fromEntries(\n Object.entries(metadata).filter(([key]) => key !== \"items\" && key !== \"offer_slug\"),\n );\n}\n\nfunction calcShipping(\n items: CheckoutItem[],\n products: CheckoutProductsMap,\n shipping: NonNullable<UmecConfig[\"shipping\"]>,\n) {\n const totalUnits = items.reduce((sum, { sku, quantity }) => {\n return sum + (products[sku]?.shippingUnits ?? 1) * quantity;\n }, 0);\n\n const tier = shipping.tiers.find((shippingTier) => {\n return shippingTier.maxUnits === undefined || totalUnits <= shippingTier.maxUnits;\n }) ?? shipping.tiers[shipping.tiers.length - 1]!;\n\n return {\n amount: tier.amount,\n label: tier.label,\n };\n}\n\nexport function createCheckoutHandler({\n products,\n shipping,\n customFields: staticCustomFields = [],\n metadata: staticMetadata = {},\n checkStock,\n checkOffer,\n}: CheckoutHandlerOptions): CheckoutHandler {\n return async ({ request }) => {\n const { STRIPE_SECRET_KEY: stripeKey } = getServerEnv();\n if (!stripeKey) return jsonError(\"Stripe not configured\", 503);\n\n const stripe = new Stripe(stripeKey, {\n apiVersion: STRIPE_API_VERSION,\n });\n let payload: unknown;\n\n try {\n payload = await request.json();\n } catch {\n return jsonError(\"Invalid request body\", 400);\n }\n\n if (!isCheckoutPayload(payload)) return jsonError(\"Invalid request body\", 400);\n\n const { items } = payload;\n if (!Array.isArray(items) || items.length === 0) return jsonError(\"Invalid items\", 400);\n if (items.length > MAX_ITEMS) return jsonError(\"Invalid items\", 400);\n\n const rawItems: CheckoutItem[] = [];\n for (const item of items) {\n if (typeof item !== \"object\" || item === null) return jsonError(\"Invalid SKU\", 400);\n\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n if (typeof sku !== \"string\" || !products[sku]) return jsonError(\"Invalid SKU\", 400);\n if (!isValidQuantity(quantity)) return jsonError(\"Invalid quantity\", 400);\n\n rawItems.push({ sku, quantity });\n }\n\n const quantityMap = new Map<string, number>();\n for (const { sku, quantity } of rawItems) {\n quantityMap.set(sku, (quantityMap.get(sku) ?? 0) + quantity);\n }\n\n for (const [, total] of quantityMap) {\n if (total > MAX_QUANTITY) return jsonError(\"Invalid quantity\", 400);\n }\n\n const checkoutItems: CheckoutItem[] = Array.from(quantityMap.entries()).map(\n ([sku, quantity]) => ({ sku, quantity }),\n );\n const { offerSlug } = payload as { items?: unknown; offerSlug?: unknown };\n let priceOverrides: Record<string, number> = {};\n let freeShipping = false;\n let offerCustomFields: CheckoutCustomField[] = [];\n let offerMetadata: Record<string, string> = {};\n\n if (typeof offerSlug === \"string\" && offerSlug.trim() !== \"\") {\n if (!checkOffer) return jsonError(\"Offer not available\", 503);\n const offer = await checkOffer(offerSlug.trim());\n if (!offer) return jsonError(\"Offer not found\", 404);\n if (isOfferUnavailable(offer)) return jsonError(offer.error, offer.status);\n const offerUnits = offer.unitsRemaining;\n if (offerUnits && offerUnits.size > 0) {\n for (const { sku, quantity } of checkoutItems) {\n const remaining = offerUnits.get(sku);\n // TOCTOU race is acceptable for low-frequency sales. Move to Durable Objects if needed.\n if (remaining !== undefined && quantity > remaining) {\n return jsonError(`Offer unit limit reached: ${sku}`, 409);\n }\n }\n }\n priceOverrides = offer.priceOverrides;\n freeShipping = offer.freeShipping;\n offerCustomFields = offer.customFields ?? [];\n offerMetadata = offer.metadata ?? {};\n }\n\n const shippingRate = shipping\n ? freeShipping\n ? { amount: 0, label: shipping.freeLabel }\n : calcShipping(checkoutItems, products, shipping)\n : undefined;\n const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : new Map<string, number>();\n for (const { sku, quantity } of checkoutItems) {\n const available = stock.get(sku);\n if (available !== undefined && quantity > available) {\n return jsonError(`Out of stock: ${sku}`, 409);\n }\n }\n\n const checkoutCustomFields: CheckoutCustomField[] = [\n ...staticCustomFields,\n ...offerCustomFields,\n {\n key: \"message\",\n label: { type: \"custom\", custom: \"メッセージ・ご要望(任意)\" },\n type: \"text\",\n optional: true,\n text: { maximum_length: 255 },\n },\n ];\n\n try {\n const sessionParams: Stripe.Checkout.SessionCreateParams = {\n ui_mode: \"embedded_page\",\n mode: \"payment\",\n line_items: checkoutItems.map(({ sku, quantity }) => ({\n price_data: {\n currency: \"jpy\",\n product_data: { name: products[sku]!.name },\n unit_amount: priceOverrides[sku] ?? products[sku]!.price,\n },\n quantity,\n })),\n metadata: {\n ...omitReservedMetadata(staticMetadata),\n ...omitReservedMetadata(offerMetadata),\n items: JSON.stringify(checkoutItems),\n ...(typeof offerSlug === \"string\" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}),\n },\n phone_number_collection: { enabled: true },\n custom_fields: checkoutCustomFields,\n shipping_address_collection: { allowed_countries: [\"JP\"] },\n ...(shippingRate\n ? {\n shipping_options: [\n {\n shipping_rate_data: {\n type: \"fixed_amount\",\n fixed_amount: { amount: shippingRate.amount, currency: \"jpy\" },\n display_name: shippingRate.label,\n delivery_estimate: {\n minimum: { unit: \"business_day\", value: 2 },\n maximum: { unit: \"business_day\", value: 5 },\n },\n },\n },\n ],\n }\n : {}),\n redirect_on_completion: \"never\",\n };\n\n const session = await stripe.checkout.sessions.create(sessionParams);\n\n return new Response(JSON.stringify({ clientSecret: session.client_secret }), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (error) {\n const stripeError = error as {\n type?: string;\n code?: string;\n statusCode?: number;\n requestId?: string;\n };\n console.error(\"[checkout] stripe session create failed:\", {\n type: stripeError.type,\n code: stripeError.code,\n statusCode: stripeError.statusCode,\n requestId: stripeError.requestId,\n });\n\n if (stripeError.type === \"StripePermissionError\") {\n return jsonError(\"Stripe key does not have permission to create Checkout Sessions\", 503);\n }\n\n return jsonError(\"Checkout session could not be created\", 503);\n }\n };\n}\n"],"mappings":"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","names":[]}