@umec/core 0.1.0-alpha.0 → 0.1.0-alpha.10
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/dist/catalog-5BPLMAW2.js +15 -0
- package/dist/catalog-5BPLMAW2.js.map +1 -0
- package/dist/checkout.d.ts +10 -3
- package/dist/checkout.js +1 -1
- package/dist/{chunk-2NYFCO5V.js → chunk-2EBXJT3M.js} +30 -30
- package/dist/chunk-2EBXJT3M.js.map +1 -0
- package/dist/{chunk-WZFM6R5J.js → chunk-4V4GYE7E.js} +4 -10
- package/dist/chunk-4V4GYE7E.js.map +1 -0
- package/dist/chunk-CYKUAS3D.js +9 -0
- package/dist/chunk-CYKUAS3D.js.map +1 -0
- package/dist/chunk-DN3J2NQC.js +298 -0
- package/dist/chunk-DN3J2NQC.js.map +1 -0
- package/dist/chunk-PNICEHSO.js +255 -0
- package/dist/chunk-PNICEHSO.js.map +1 -0
- package/dist/{chunk-QRFFBMFN.js → chunk-RFEX32LI.js} +12 -11
- package/dist/chunk-RFEX32LI.js.map +1 -0
- package/dist/chunk-T2PO3YLZ.js +64 -0
- package/dist/chunk-T2PO3YLZ.js.map +1 -0
- package/dist/{chunk-REUSLO5I.js → chunk-ZP7IGPJW.js} +26 -4
- package/dist/chunk-ZP7IGPJW.js.map +1 -0
- package/dist/cli.js +823 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/index.d.ts +130 -0
- package/dist/db/index.js +81 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +15 -4
- package/dist/middleware.d.ts +7 -2
- package/dist/middleware.js +1 -1
- package/dist/notify.d.ts +7 -8
- package/dist/notify.js +1 -1
- package/dist/orders-DRZvcL3s.d.ts +99 -0
- package/dist/schema-version-F-YYC1UG.d.ts +25 -0
- package/dist/sql-J4YBS7N7.js +7 -0
- package/dist/sql-J4YBS7N7.js.map +1 -0
- package/dist/webhook.d.ts +10 -2
- package/dist/webhook.js +1 -1
- package/package.json +10 -2
- package/schema/frozen-checksums.json +11 -0
- package/schema/migrations/0001_processed_events.sql +4 -0
- package/schema/migrations/0002_orders.sql +19 -0
- package/schema/migrations/0003_inventory.sql +5 -0
- package/schema/migrations/0004_admin_audit_log.sql +15 -0
- package/schema/migrations/0005_orders_tracking.sql +3 -0
- package/schema/migrations/0006_products.sql +15 -0
- package/schema/migrations/0007_orders_extras.sql +1 -0
- package/schema/snapshot.json +433 -0
- package/schema/upgrades/types.ts +16 -0
- package/schema/upgrades/v2-to-v6.ts +205 -0
- package/dist/chunk-2NYFCO5V.js.map +0 -1
- package/dist/chunk-QRFFBMFN.js.map +0 -1
- package/dist/chunk-REUSLO5I.js.map +0 -1
- package/dist/chunk-WZFM6R5J.js.map +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
currentSchemaVersion,
|
|
3
|
+
listCoreMigrations,
|
|
4
|
+
loadSnapshotJson,
|
|
5
|
+
migrationsDir,
|
|
6
|
+
schemaDir
|
|
7
|
+
} from "./chunk-T2PO3YLZ.js";
|
|
8
|
+
export {
|
|
9
|
+
currentSchemaVersion,
|
|
10
|
+
listCoreMigrations,
|
|
11
|
+
loadSnapshotJson,
|
|
12
|
+
migrationsDir,
|
|
13
|
+
schemaDir
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=catalog-5BPLMAW2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/checkout.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
1
2
|
import { UmecProduct, UmecConfig } from './config.js';
|
|
2
3
|
import 'zod';
|
|
3
4
|
|
|
4
5
|
type CheckoutProductsMap = Record<string, UmecProduct | undefined>;
|
|
6
|
+
type CheckoutCustomField = Stripe.Checkout.SessionCreateParams.CustomField;
|
|
5
7
|
type CheckoutOffer = {
|
|
6
8
|
priceOverrides: Record<string, number>;
|
|
7
9
|
freeShipping: boolean;
|
|
8
|
-
|
|
10
|
+
customFields?: CheckoutCustomField[];
|
|
11
|
+
metadata?: Record<string, string>;
|
|
12
|
+
customerEmail?: string;
|
|
9
13
|
unitsRemaining?: Map<string, number>;
|
|
10
14
|
};
|
|
11
15
|
type CheckoutOfferUnavailable = {
|
|
@@ -17,12 +21,15 @@ type CheckOffer = (offerSlug: string) => Promise<CheckoutOffer | CheckoutOfferUn
|
|
|
17
21
|
type CheckoutHandlerOptions = {
|
|
18
22
|
products: CheckoutProductsMap;
|
|
19
23
|
shipping?: UmecConfig["shipping"];
|
|
24
|
+
customFields?: CheckoutCustomField[];
|
|
25
|
+
metadata?: Record<string, string>;
|
|
26
|
+
customerEmail?: string;
|
|
20
27
|
checkStock?: CheckStock;
|
|
21
28
|
checkOffer?: CheckOffer;
|
|
22
29
|
};
|
|
23
30
|
type CheckoutHandler = ({ request }: {
|
|
24
31
|
request: Request;
|
|
25
32
|
}) => Promise<Response>;
|
|
26
|
-
declare function createCheckoutHandler({ products, shipping, checkStock, checkOffer, }: CheckoutHandlerOptions): CheckoutHandler;
|
|
33
|
+
declare function createCheckoutHandler({ products, shipping, customFields: staticCustomFields, metadata: staticMetadata, customerEmail: staticCustomerEmail, checkStock, checkOffer, }: CheckoutHandlerOptions): CheckoutHandler;
|
|
27
34
|
|
|
28
|
-
export { type CheckOffer, type CheckStock, type CheckoutHandler, type CheckoutHandlerOptions, type CheckoutOffer, type CheckoutOfferUnavailable, type CheckoutProductsMap, createCheckoutHandler };
|
|
35
|
+
export { type CheckOffer, type CheckStock, type CheckoutCustomField, type CheckoutHandler, type CheckoutHandlerOptions, type CheckoutOffer, type CheckoutOfferUnavailable, type CheckoutProductsMap, createCheckoutHandler };
|
package/dist/checkout.js
CHANGED
|
@@ -22,6 +22,11 @@ function isValidQuantity(value) {
|
|
|
22
22
|
function isOfferUnavailable(value) {
|
|
23
23
|
return "error" in value && "status" in value;
|
|
24
24
|
}
|
|
25
|
+
function omitReservedMetadata(metadata) {
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
Object.entries(metadata).filter(([key]) => key !== "items" && key !== "offer_slug")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
25
30
|
function calcShipping(items, products, shipping) {
|
|
26
31
|
const totalUnits = items.reduce((sum, { sku, quantity }) => {
|
|
27
32
|
return sum + (products[sku]?.shippingUnits ?? 1) * quantity;
|
|
@@ -37,6 +42,9 @@ function calcShipping(items, products, shipping) {
|
|
|
37
42
|
function createCheckoutHandler({
|
|
38
43
|
products,
|
|
39
44
|
shipping,
|
|
45
|
+
customFields: staticCustomFields = [],
|
|
46
|
+
metadata: staticMetadata = {},
|
|
47
|
+
customerEmail: staticCustomerEmail,
|
|
40
48
|
checkStock,
|
|
41
49
|
checkOffer
|
|
42
50
|
}) {
|
|
@@ -77,13 +85,14 @@ function createCheckoutHandler({
|
|
|
77
85
|
const { offerSlug } = payload;
|
|
78
86
|
let priceOverrides = {};
|
|
79
87
|
let freeShipping = false;
|
|
80
|
-
let
|
|
88
|
+
let offerCustomFields = [];
|
|
89
|
+
let offerMetadata = {};
|
|
90
|
+
let customerEmail = staticCustomerEmail;
|
|
81
91
|
if (typeof offerSlug === "string" && offerSlug.trim() !== "") {
|
|
82
92
|
if (!checkOffer) return jsonError("Offer not available", 503);
|
|
83
93
|
const offer = await checkOffer(offerSlug.trim());
|
|
84
94
|
if (!offer) return jsonError("Offer not found", 404);
|
|
85
95
|
if (isOfferUnavailable(offer)) return jsonError(offer.error, offer.status);
|
|
86
|
-
recipientInfo = offer.recipientInfo;
|
|
87
96
|
const offerUnits = offer.unitsRemaining;
|
|
88
97
|
if (offerUnits && offerUnits.size > 0) {
|
|
89
98
|
for (const { sku, quantity } of checkoutItems) {
|
|
@@ -95,6 +104,9 @@ function createCheckoutHandler({
|
|
|
95
104
|
}
|
|
96
105
|
priceOverrides = offer.priceOverrides;
|
|
97
106
|
freeShipping = offer.freeShipping;
|
|
107
|
+
offerCustomFields = offer.customFields ?? [];
|
|
108
|
+
offerMetadata = offer.metadata ?? {};
|
|
109
|
+
customerEmail = offer.customerEmail ?? staticCustomerEmail;
|
|
98
110
|
}
|
|
99
111
|
const shippingRate = shipping ? freeShipping ? { amount: 0, label: shipping.freeLabel } : calcShipping(checkoutItems, products, shipping) : void 0;
|
|
100
112
|
const stock = checkStock ? await checkStock(checkoutItems.map((item) => item.sku)) : /* @__PURE__ */ new Map();
|
|
@@ -104,32 +116,17 @@ function createCheckoutHandler({
|
|
|
104
116
|
return jsonError(`Out of stock: ${sku}`, 409);
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
key: "child_name",
|
|
119
|
-
label: { type: "custom", custom: "\u304A\u5B50\u3055\u3093\u306E\u304A\u540D\u524D" },
|
|
120
|
-
type: "text",
|
|
121
|
-
optional: false,
|
|
122
|
-
text: { maximum_length: 50 }
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
customFields.push({
|
|
127
|
-
key: "message",
|
|
128
|
-
label: { type: "custom", custom: "\u30E1\u30C3\u30BB\u30FC\u30B8\u30FB\u3054\u8981\u671B\uFF08\u4EFB\u610F\uFF09" },
|
|
129
|
-
type: "text",
|
|
130
|
-
optional: true,
|
|
131
|
-
text: { maximum_length: 255 }
|
|
132
|
-
});
|
|
119
|
+
const checkoutCustomFields = [
|
|
120
|
+
...staticCustomFields,
|
|
121
|
+
...offerCustomFields,
|
|
122
|
+
{
|
|
123
|
+
key: "message",
|
|
124
|
+
label: { type: "custom", custom: "\u30E1\u30C3\u30BB\u30FC\u30B8\u30FB\u3054\u8981\u671B\uFF08\u4EFB\u610F\uFF09" },
|
|
125
|
+
type: "text",
|
|
126
|
+
optional: true,
|
|
127
|
+
text: { maximum_length: 255 }
|
|
128
|
+
}
|
|
129
|
+
];
|
|
133
130
|
try {
|
|
134
131
|
const sessionParams = {
|
|
135
132
|
ui_mode: "embedded_page",
|
|
@@ -143,11 +140,14 @@ function createCheckoutHandler({
|
|
|
143
140
|
quantity
|
|
144
141
|
})),
|
|
145
142
|
metadata: {
|
|
143
|
+
...omitReservedMetadata(staticMetadata),
|
|
144
|
+
...omitReservedMetadata(offerMetadata),
|
|
146
145
|
items: JSON.stringify(checkoutItems),
|
|
147
146
|
...typeof offerSlug === "string" && offerSlug.trim() ? { offer_slug: offerSlug.trim() } : {}
|
|
148
147
|
},
|
|
148
|
+
...customerEmail ? { customer_email: customerEmail } : {},
|
|
149
149
|
phone_number_collection: { enabled: true },
|
|
150
|
-
custom_fields:
|
|
150
|
+
custom_fields: checkoutCustomFields,
|
|
151
151
|
shipping_address_collection: { allowed_countries: ["JP"] },
|
|
152
152
|
...shippingRate ? {
|
|
153
153
|
shipping_options: [
|
|
@@ -189,4 +189,4 @@ function createCheckoutHandler({
|
|
|
189
189
|
export {
|
|
190
190
|
createCheckoutHandler
|
|
191
191
|
};
|
|
192
|
-
//# sourceMappingURL=chunk-
|
|
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":[]}
|
|
@@ -5,12 +5,6 @@ var SECURITY_HEADERS = {
|
|
|
5
5
|
"X-Frame-Options": "SAMEORIGIN",
|
|
6
6
|
"Referrer-Policy": "strict-origin-when-cross-origin"
|
|
7
7
|
};
|
|
8
|
-
var LIMITED_DOCS = [
|
|
9
|
-
{
|
|
10
|
-
pathPrefix: "/docs/asahiko2026special",
|
|
11
|
-
expiresAt: Date.parse("2026-08-12T14:59:59.999Z")
|
|
12
|
-
}
|
|
13
|
-
];
|
|
14
8
|
function withSecurityHeaders(response) {
|
|
15
9
|
const securedResponse = new Response(response.body, response);
|
|
16
10
|
for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
|
|
@@ -18,11 +12,11 @@ function withSecurityHeaders(response) {
|
|
|
18
12
|
}
|
|
19
13
|
return securedResponse;
|
|
20
14
|
}
|
|
21
|
-
function createUmecMiddleware({ maintenanceMode }) {
|
|
15
|
+
function createUmecMiddleware({ maintenanceMode, limitedPages = [] }) {
|
|
22
16
|
return async (context, next) => {
|
|
23
17
|
const path = context.url.pathname;
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
18
|
+
const limitedPage = limitedPages.find(({ pathPrefix }) => path === pathPrefix || path.startsWith(`${pathPrefix}/`));
|
|
19
|
+
if (limitedPage && Date.now() > limitedPage.expiresAt) {
|
|
26
20
|
return withSecurityHeaders(
|
|
27
21
|
new Response("Not found", {
|
|
28
22
|
status: 404,
|
|
@@ -44,4 +38,4 @@ function createUmecMiddleware({ maintenanceMode }) {
|
|
|
44
38
|
export {
|
|
45
39
|
createUmecMiddleware
|
|
46
40
|
};
|
|
47
|
-
//# sourceMappingURL=chunk-
|
|
41
|
+
//# sourceMappingURL=chunk-4V4GYE7E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["const SECURITY_HEADERS = {\n \"Strict-Transport-Security\": \"max-age=63072000; includeSubDomains\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"SAMEORIGIN\",\n \"Referrer-Policy\": \"strict-origin-when-cross-origin\",\n};\n\n// Astro の ValidRedirectStatus と同一のユニオンに揃える。\n// number に広げると Astro の APIContext が strict モードで代入不能になる(TS2345)\nexport type UmecRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308;\n\nexport type UmecMiddlewareContext = {\n url: URL;\n redirect: (path: string, status?: UmecRedirectStatus) => Response;\n};\n\nexport type UmecMiddlewareNext = () => Response | Promise<Response>;\n\nexport type UmecMiddleware = (context: UmecMiddlewareContext, next: UmecMiddlewareNext) => Promise<Response>;\n\nexport type UmecLimitedPage = {\n pathPrefix: string;\n expiresAt: number;\n};\n\nexport type UmecMiddlewareOptions = {\n maintenanceMode: boolean;\n limitedPages?: UmecLimitedPage[];\n};\n\nfunction withSecurityHeaders(response: Response) {\n const securedResponse = new Response(response.body, response);\n\n for (const [name, value] of Object.entries(SECURITY_HEADERS)) {\n securedResponse.headers.set(name, value);\n }\n\n return securedResponse;\n}\n\nexport function createUmecMiddleware({ maintenanceMode, limitedPages = [] }: UmecMiddlewareOptions): UmecMiddleware {\n return async (context, next) => {\n const path = context.url.pathname;\n\n const limitedPage = limitedPages.find(({ pathPrefix }) => path === pathPrefix || path.startsWith(`${pathPrefix}/`));\n if (limitedPage && Date.now() > limitedPage.expiresAt) {\n return withSecurityHeaders(\n new Response(\"Not found\", {\n status: 404,\n headers: {\n \"Cache-Control\": \"no-store\",\n },\n }),\n );\n }\n\n if (maintenanceMode && path !== \"/maintenance\" && !path.startsWith(\"/api/\")) {\n const response = context.redirect(\"/maintenance\", 302);\n response.headers.set(\"Cache-Control\", \"no-store\");\n return response;\n }\n\n return withSecurityHeaders(await next());\n };\n}\n"],"mappings":";AAAA,IAAM,mBAAmB;AAAA,EACvB,6BAA6B;AAAA,EAC7B,0BAA0B;AAAA,EAC1B,mBAAmB;AAAA,EACnB,mBAAmB;AACrB;AAyBA,SAAS,oBAAoB,UAAoB;AAC/C,QAAM,kBAAkB,IAAI,SAAS,SAAS,MAAM,QAAQ;AAE5D,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAC5D,oBAAgB,QAAQ,IAAI,MAAM,KAAK;AAAA,EACzC;AAEA,SAAO;AACT;AAEO,SAAS,qBAAqB,EAAE,iBAAiB,eAAe,CAAC,EAAE,GAA0C;AAClH,SAAO,OAAO,SAAS,SAAS;AAC9B,UAAM,OAAO,QAAQ,IAAI;AAEzB,UAAM,cAAc,aAAa,KAAK,CAAC,EAAE,WAAW,MAAM,SAAS,cAAc,KAAK,WAAW,GAAG,UAAU,GAAG,CAAC;AAClH,QAAI,eAAe,KAAK,IAAI,IAAI,YAAY,WAAW;AACrD,aAAO;AAAA,QACL,IAAI,SAAS,aAAa;AAAA,UACxB,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,iBAAiB;AAAA,UACnB;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI,mBAAmB,SAAS,kBAAkB,CAAC,KAAK,WAAW,OAAO,GAAG;AAC3E,YAAM,WAAW,QAAQ,SAAS,gBAAgB,GAAG;AACrD,eAAS,QAAQ,IAAI,iBAAiB,UAAU;AAChD,aAAO;AAAA,IACT;AAEA,WAAO,oBAAoB,MAAM,KAAK,CAAC;AAAA,EACzC;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/sql.ts"],"sourcesContent":["export function splitSqlStatements(sql: string): string[] {\n return sql\n .split(\";\")\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0);\n}\n"],"mappings":";AAAO,SAAS,mBAAmB,KAAuB;AACxD,SAAO,IACJ,MAAM,GAAG,EACT,IAAI,CAAC,cAAc,UAAU,KAAK,CAAC,EACnC,OAAO,CAAC,cAAc,UAAU,SAAS,CAAC;AAC/C;","names":[]}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// src/db/audit.ts
|
|
2
|
+
async function writeAuditLog(db, entry, createdAt = Date.now()) {
|
|
3
|
+
await db.prepare(
|
|
4
|
+
`INSERT INTO admin_audit_log (
|
|
5
|
+
entity_type, entity_id, action, delta, reason, admin_email, actor, detail_json, created_at
|
|
6
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7
|
+
).bind(
|
|
8
|
+
entry.entityType,
|
|
9
|
+
entry.entityId,
|
|
10
|
+
entry.action,
|
|
11
|
+
entry.delta ?? null,
|
|
12
|
+
entry.reason ?? null,
|
|
13
|
+
entry.adminEmail ?? null,
|
|
14
|
+
entry.actor ?? "human",
|
|
15
|
+
entry.detail === void 0 ? null : JSON.stringify(entry.detail),
|
|
16
|
+
createdAt
|
|
17
|
+
).run();
|
|
18
|
+
}
|
|
19
|
+
async function listAuditLog(db, options = {}) {
|
|
20
|
+
const limit = options.limit ?? 50;
|
|
21
|
+
const clauses = [];
|
|
22
|
+
const binds = [];
|
|
23
|
+
if (options.entityType) {
|
|
24
|
+
clauses.push("entity_type = ?");
|
|
25
|
+
binds.push(options.entityType);
|
|
26
|
+
}
|
|
27
|
+
if (options.entityId) {
|
|
28
|
+
clauses.push("entity_id = ?");
|
|
29
|
+
binds.push(options.entityId);
|
|
30
|
+
}
|
|
31
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
32
|
+
const result = await db.prepare(
|
|
33
|
+
`SELECT id, entity_type, entity_id, action, delta, reason, admin_email, actor, detail_json, created_at
|
|
34
|
+
FROM admin_audit_log
|
|
35
|
+
${where}
|
|
36
|
+
ORDER BY created_at DESC
|
|
37
|
+
LIMIT ?`
|
|
38
|
+
).bind(...binds, limit).all();
|
|
39
|
+
return result.results ?? [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/db/events.ts
|
|
43
|
+
async function claimEvent(db, eventId, now = Date.now()) {
|
|
44
|
+
const claim = await db.prepare("INSERT OR IGNORE INTO processed_events (event_id, processed_at) VALUES (?, ?)").bind(eventId, now).run();
|
|
45
|
+
return claim.meta.changes !== 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/db/extensions.ts
|
|
49
|
+
function parseExtras(schema, extrasJson) {
|
|
50
|
+
if (!extrasJson) return null;
|
|
51
|
+
const parsed = JSON.parse(extrasJson);
|
|
52
|
+
return schema.parse(parsed);
|
|
53
|
+
}
|
|
54
|
+
function serializeExtras(schema, value) {
|
|
55
|
+
return JSON.stringify(schema.parse(value));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/db/inventory.ts
|
|
59
|
+
var MAX_STOCK_DELTA = 1e4;
|
|
60
|
+
async function getStock(db, skus) {
|
|
61
|
+
if (skus.length === 0) return /* @__PURE__ */ new Map();
|
|
62
|
+
const placeholders = skus.map(() => "?").join(", ");
|
|
63
|
+
const result = await db.prepare(`SELECT sku, stock FROM inventory WHERE sku IN (${placeholders})`).bind(...skus).all();
|
|
64
|
+
return new Map((result.results ?? []).map((row) => [row.sku, row.stock]));
|
|
65
|
+
}
|
|
66
|
+
async function decrementStock(db, items) {
|
|
67
|
+
if (items.length === 0) return;
|
|
68
|
+
const updatedAt = Date.now();
|
|
69
|
+
for (const { sku, quantity } of items) {
|
|
70
|
+
const result = await db.prepare("UPDATE inventory SET stock = stock - ?, updated_at = ? WHERE sku = ? AND stock >= ?").bind(quantity, updatedAt, sku, quantity).run();
|
|
71
|
+
if (result.meta.changes === 0) {
|
|
72
|
+
console.error("[inventory] stock decrement skipped: insufficient stock", { sku, quantity });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function restoreStock(db, items) {
|
|
77
|
+
if (items.length === 0) return;
|
|
78
|
+
const updatedAt = Date.now();
|
|
79
|
+
for (const { sku, quantity } of items) {
|
|
80
|
+
await db.prepare("UPDATE inventory SET stock = stock + ?, updated_at = ? WHERE sku = ?").bind(quantity, updatedAt, sku).run();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function setStock(db, sku, stock, updatedAt = Date.now()) {
|
|
84
|
+
const result = await db.prepare("UPDATE inventory SET stock = ?, updated_at = ? WHERE sku = ?").bind(stock, updatedAt, sku).run();
|
|
85
|
+
return result.meta.changes === 0 ? "not_found" : "ok";
|
|
86
|
+
}
|
|
87
|
+
async function adjustStock(db, sku, delta) {
|
|
88
|
+
const existing = await db.prepare("SELECT stock FROM inventory WHERE sku = ?").bind(sku).first();
|
|
89
|
+
if (!existing) return "not_found";
|
|
90
|
+
const result = await db.prepare("UPDATE inventory SET stock = stock + ?, updated_at = ? WHERE sku = ? AND stock + ? >= 0").bind(delta, Date.now(), sku, delta).run();
|
|
91
|
+
if (result.meta.changes === 0) return "insufficient";
|
|
92
|
+
return "ok";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/db/orders.ts
|
|
96
|
+
function parseCheckoutItems(items) {
|
|
97
|
+
if (!items) return [];
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(items);
|
|
100
|
+
if (!Array.isArray(parsed)) return [];
|
|
101
|
+
return parsed.filter((item) => {
|
|
102
|
+
if (typeof item !== "object" || item === null) return false;
|
|
103
|
+
const { sku, quantity } = item;
|
|
104
|
+
return typeof sku === "string" && typeof quantity === "number" && Number.isSafeInteger(quantity) && quantity > 0;
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function recordOrder(db, order, extrasJson = null) {
|
|
111
|
+
await db.prepare(
|
|
112
|
+
`INSERT INTO orders (
|
|
113
|
+
id, email, name, phone, items_json, amount, shipping, address_json,
|
|
114
|
+
payment_intent, custom_fields_json, run_id, extras_json, created_at
|
|
115
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
116
|
+
).bind(
|
|
117
|
+
order.id,
|
|
118
|
+
order.email,
|
|
119
|
+
order.name,
|
|
120
|
+
order.phone,
|
|
121
|
+
JSON.stringify(order.items),
|
|
122
|
+
order.amount,
|
|
123
|
+
order.shippingCost,
|
|
124
|
+
order.shippingAddress ? JSON.stringify(order.shippingAddress) : null,
|
|
125
|
+
order.paymentIntent,
|
|
126
|
+
order.customFields ? JSON.stringify(order.customFields) : null,
|
|
127
|
+
order.metadata?.run_id ?? null,
|
|
128
|
+
extrasJson,
|
|
129
|
+
order.createdAt
|
|
130
|
+
).run();
|
|
131
|
+
}
|
|
132
|
+
async function recordRefund(db, paymentIntentId) {
|
|
133
|
+
await db.prepare("UPDATE orders SET status = ? WHERE payment_intent = ?").bind("refunded", paymentIntentId).run();
|
|
134
|
+
const orderRow = await db.prepare("SELECT items_json FROM orders WHERE payment_intent = ?").bind(paymentIntentId).first();
|
|
135
|
+
if (!orderRow) return null;
|
|
136
|
+
return { items: parseCheckoutItems(orderRow.items_json), offerSlug: null };
|
|
137
|
+
}
|
|
138
|
+
async function recordExpiry(db, sessionId) {
|
|
139
|
+
await db.prepare("UPDATE orders SET status = ? WHERE id = ?").bind("cancelled", sessionId).run();
|
|
140
|
+
const orderRow = await db.prepare("SELECT items_json FROM orders WHERE id = ?").bind(sessionId).first();
|
|
141
|
+
if (!orderRow) return null;
|
|
142
|
+
return { items: parseCheckoutItems(orderRow.items_json), offerSlug: null };
|
|
143
|
+
}
|
|
144
|
+
async function recordEmailResult(db, sessionId, status) {
|
|
145
|
+
await db.prepare("UPDATE orders SET email_status = ? WHERE id = ?").bind(status, sessionId).run();
|
|
146
|
+
}
|
|
147
|
+
async function listUnshipped(db, cutoff) {
|
|
148
|
+
const result = await db.prepare(
|
|
149
|
+
`SELECT id, name, items_json, amount, created_at
|
|
150
|
+
FROM orders
|
|
151
|
+
WHERE status = ? AND created_at < ?
|
|
152
|
+
ORDER BY created_at ASC`
|
|
153
|
+
).bind("pending", cutoff).all();
|
|
154
|
+
return result.results ?? [];
|
|
155
|
+
}
|
|
156
|
+
async function getOrder(db, id) {
|
|
157
|
+
const row = await db.prepare("SELECT id, status, tracking_number, carrier FROM orders WHERE id = ?").bind(id).first();
|
|
158
|
+
return row ?? null;
|
|
159
|
+
}
|
|
160
|
+
async function markShipped(db, id, options = {}) {
|
|
161
|
+
const result = await db.prepare(
|
|
162
|
+
`UPDATE orders
|
|
163
|
+
SET status = ?, shipped_at = ?, tracking_number = COALESCE(?, tracking_number), carrier = COALESCE(?, carrier)
|
|
164
|
+
WHERE id = ? AND status = ?`
|
|
165
|
+
).bind("shipped", options.shippedAt ?? Date.now(), options.trackingNumber ?? null, options.carrier ?? null, id, "pending").run();
|
|
166
|
+
return result.meta.changes === 0 ? "not_pending" : "ok";
|
|
167
|
+
}
|
|
168
|
+
async function updateTracking(db, id, trackingNumber, carrier) {
|
|
169
|
+
const result = await db.prepare("UPDATE orders SET tracking_number = ?, carrier = ? WHERE id = ?").bind(trackingNumber, carrier, id).run();
|
|
170
|
+
return result.meta.changes === 0 ? "not_found" : "ok";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/db/products.ts
|
|
174
|
+
async function listProductRows(db, options = {}) {
|
|
175
|
+
const sql = options.includeInactive ? `SELECT sku, name, description, price, shipping_units, images_json, gtin, category,
|
|
176
|
+
active, low_stock_threshold, created_at, updated_at, updated_by
|
|
177
|
+
FROM products
|
|
178
|
+
ORDER BY sku ASC` : `SELECT sku, name, description, price, shipping_units, images_json, gtin, category,
|
|
179
|
+
active, low_stock_threshold, created_at, updated_at, updated_by
|
|
180
|
+
FROM products
|
|
181
|
+
WHERE active = 1
|
|
182
|
+
ORDER BY sku ASC`;
|
|
183
|
+
const result = await db.prepare(sql).all();
|
|
184
|
+
return result.results ?? [];
|
|
185
|
+
}
|
|
186
|
+
async function getProductRow(db, sku) {
|
|
187
|
+
const row = await db.prepare(
|
|
188
|
+
`SELECT sku, name, description, price, shipping_units, images_json, gtin, category,
|
|
189
|
+
active, low_stock_threshold, created_at, updated_at, updated_by
|
|
190
|
+
FROM products
|
|
191
|
+
WHERE sku = ?`
|
|
192
|
+
).bind(sku).first();
|
|
193
|
+
return row ?? null;
|
|
194
|
+
}
|
|
195
|
+
async function createProduct(db, sku, fields, updatedBy, now = Date.now()) {
|
|
196
|
+
const existing = await db.prepare("SELECT sku FROM products WHERE sku = ?").bind(sku).first();
|
|
197
|
+
if (existing) return "conflict";
|
|
198
|
+
const imagesJson = fields.images.length > 0 ? JSON.stringify(fields.images) : null;
|
|
199
|
+
await db.batch([
|
|
200
|
+
db.prepare(
|
|
201
|
+
`INSERT INTO products (
|
|
202
|
+
sku, name, description, price, shipping_units, images_json, gtin, category,
|
|
203
|
+
active, low_stock_threshold, created_at, updated_at, updated_by
|
|
204
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
205
|
+
).bind(
|
|
206
|
+
sku,
|
|
207
|
+
fields.name,
|
|
208
|
+
fields.description,
|
|
209
|
+
fields.price,
|
|
210
|
+
fields.shippingUnits,
|
|
211
|
+
imagesJson,
|
|
212
|
+
fields.gtin,
|
|
213
|
+
fields.category,
|
|
214
|
+
fields.active,
|
|
215
|
+
fields.lowStockThreshold,
|
|
216
|
+
now,
|
|
217
|
+
now,
|
|
218
|
+
updatedBy
|
|
219
|
+
),
|
|
220
|
+
db.prepare("INSERT OR IGNORE INTO inventory (sku, stock, updated_at) VALUES (?, ?, ?)").bind(sku, 0, now)
|
|
221
|
+
]);
|
|
222
|
+
return "ok";
|
|
223
|
+
}
|
|
224
|
+
async function updateProduct(db, sku, fields, updatedBy, now = Date.now()) {
|
|
225
|
+
const existing = await getProductRow(db, sku);
|
|
226
|
+
if (!existing) return { result: "not_found", before: null, after: null };
|
|
227
|
+
const next = {
|
|
228
|
+
name: fields.name ?? existing.name,
|
|
229
|
+
description: fields.description !== void 0 ? fields.description : existing.description,
|
|
230
|
+
price: fields.price ?? existing.price,
|
|
231
|
+
shippingUnits: fields.shippingUnits ?? existing.shipping_units,
|
|
232
|
+
imagesJson: fields.images !== void 0 ? fields.images.length > 0 ? JSON.stringify(fields.images) : null : existing.images_json,
|
|
233
|
+
gtin: fields.gtin !== void 0 ? fields.gtin : existing.gtin,
|
|
234
|
+
category: fields.category !== void 0 ? fields.category : existing.category,
|
|
235
|
+
active: fields.active ?? existing.active,
|
|
236
|
+
lowStockThreshold: fields.lowStockThreshold !== void 0 ? fields.lowStockThreshold : existing.low_stock_threshold
|
|
237
|
+
};
|
|
238
|
+
await db.prepare(
|
|
239
|
+
`UPDATE products
|
|
240
|
+
SET name = ?, description = ?, price = ?, shipping_units = ?, images_json = ?,
|
|
241
|
+
gtin = ?, category = ?, active = ?, low_stock_threshold = ?, updated_at = ?, updated_by = ?
|
|
242
|
+
WHERE sku = ?`
|
|
243
|
+
).bind(
|
|
244
|
+
next.name,
|
|
245
|
+
next.description,
|
|
246
|
+
next.price,
|
|
247
|
+
next.shippingUnits,
|
|
248
|
+
next.imagesJson,
|
|
249
|
+
next.gtin,
|
|
250
|
+
next.category,
|
|
251
|
+
next.active,
|
|
252
|
+
next.lowStockThreshold,
|
|
253
|
+
now,
|
|
254
|
+
updatedBy,
|
|
255
|
+
sku
|
|
256
|
+
).run();
|
|
257
|
+
const after = await getProductRow(db, sku);
|
|
258
|
+
return { result: "ok", before: existing, after };
|
|
259
|
+
}
|
|
260
|
+
async function discontinueProduct(db, sku, updatedBy, now = Date.now()) {
|
|
261
|
+
const existing = await db.prepare("SELECT sku, active FROM products WHERE sku = ?").bind(sku).first();
|
|
262
|
+
if (!existing) return "not_found";
|
|
263
|
+
if (existing.active === 0) return "already_inactive";
|
|
264
|
+
await db.prepare("UPDATE products SET active = 0, updated_at = ?, updated_by = ? WHERE sku = ?").bind(now, updatedBy, sku).run();
|
|
265
|
+
return "ok";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/db/types.generated.ts
|
|
269
|
+
var SNAPSHOT_SCHEMA_VERSION = 7;
|
|
270
|
+
|
|
271
|
+
export {
|
|
272
|
+
writeAuditLog,
|
|
273
|
+
listAuditLog,
|
|
274
|
+
claimEvent,
|
|
275
|
+
parseExtras,
|
|
276
|
+
serializeExtras,
|
|
277
|
+
MAX_STOCK_DELTA,
|
|
278
|
+
getStock,
|
|
279
|
+
decrementStock,
|
|
280
|
+
restoreStock,
|
|
281
|
+
setStock,
|
|
282
|
+
adjustStock,
|
|
283
|
+
recordOrder,
|
|
284
|
+
recordRefund,
|
|
285
|
+
recordExpiry,
|
|
286
|
+
recordEmailResult,
|
|
287
|
+
listUnshipped,
|
|
288
|
+
getOrder,
|
|
289
|
+
markShipped,
|
|
290
|
+
updateTracking,
|
|
291
|
+
listProductRows,
|
|
292
|
+
getProductRow,
|
|
293
|
+
createProduct,
|
|
294
|
+
updateProduct,
|
|
295
|
+
discontinueProduct,
|
|
296
|
+
SNAPSHOT_SCHEMA_VERSION
|
|
297
|
+
};
|
|
298
|
+
//# sourceMappingURL=chunk-DN3J2NQC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/audit.ts","../src/db/events.ts","../src/db/extensions.ts","../src/db/inventory.ts","../src/db/orders.ts","../src/db/products.ts","../src/db/types.generated.ts"],"sourcesContent":["import type { ShopDatabase } from \"./d1.js\";\nimport type { AdminAuditLogRow } from \"./types.generated.js\";\n\nexport type AuditEntityType = \"inventory\" | \"product\" | \"order\";\nexport type AuditActor = \"human\" | \"system\" | \"agent\";\n\nexport type AdminAuditEntry = {\n entityType: AuditEntityType;\n entityId: string;\n action: string;\n delta?: number | null;\n reason?: string | null;\n adminEmail?: string | null;\n actor?: AuditActor;\n detail?: unknown;\n};\n\nexport async function writeAuditLog(\n db: ShopDatabase,\n entry: AdminAuditEntry,\n createdAt = Date.now(),\n): Promise<void> {\n await db\n .prepare(\n `INSERT INTO admin_audit_log (\n entity_type, entity_id, action, delta, reason, admin_email, actor, detail_json, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n entry.entityType,\n entry.entityId,\n entry.action,\n entry.delta ?? null,\n entry.reason ?? null,\n entry.adminEmail ?? null,\n entry.actor ?? \"human\",\n entry.detail === undefined ? null : JSON.stringify(entry.detail),\n createdAt,\n )\n .run();\n}\n\nexport async function listAuditLog(\n db: ShopDatabase,\n options: {\n entityType?: AuditEntityType;\n entityId?: string;\n limit?: number;\n } = {},\n): Promise<AdminAuditLogRow[]> {\n const limit = options.limit ?? 50;\n const clauses: string[] = [];\n const binds: unknown[] = [];\n\n if (options.entityType) {\n clauses.push(\"entity_type = ?\");\n binds.push(options.entityType);\n }\n if (options.entityId) {\n clauses.push(\"entity_id = ?\");\n binds.push(options.entityId);\n }\n\n const where = clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\";\n const result = await db\n .prepare(\n `SELECT id, entity_type, entity_id, action, delta, reason, admin_email, actor, detail_json, created_at\n FROM admin_audit_log\n ${where}\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .bind(...binds, limit)\n .all<AdminAuditLogRow>();\n\n return result.results ?? [];\n}\n","import type { ShopDatabase } from \"./d1.js\";\n\nexport async function claimEvent(db: ShopDatabase, eventId: string, now = Date.now()): Promise<boolean> {\n const claim = await db\n .prepare(\"INSERT OR IGNORE INTO processed_events (event_id, processed_at) VALUES (?, ?)\")\n .bind(eventId, now)\n .run();\n return claim.meta.changes !== 0;\n}\n","import type { z } from \"zod\";\n\nexport function parseExtras<S extends z.ZodType>(schema: S, extrasJson: string | null): z.infer<S> | null {\n if (!extrasJson) return null;\n const parsed: unknown = JSON.parse(extrasJson);\n return schema.parse(parsed);\n}\n\nexport function serializeExtras<S extends z.ZodType>(schema: S, value: unknown): string {\n return JSON.stringify(schema.parse(value));\n}\n","import type { ShopDatabase } from \"./d1.js\";\n\nexport type InventoryItem = {\n sku: string;\n quantity: number;\n};\n\nexport const MAX_STOCK_DELTA = 10000;\n\nexport type AdjustStockResult = \"ok\" | \"not_found\" | \"insufficient\";\nexport type SetStockResult = \"ok\" | \"not_found\";\n\nexport async function getStock(db: ShopDatabase, skus: string[]): Promise<Map<string, number>> {\n if (skus.length === 0) return new Map();\n\n const placeholders = skus.map(() => \"?\").join(\", \");\n const result = await db\n .prepare(`SELECT sku, stock FROM inventory WHERE sku IN (${placeholders})`)\n .bind(...skus)\n .all<{ sku: string; stock: number }>();\n\n return new Map((result.results ?? []).map((row) => [row.sku, row.stock]));\n}\n\nexport async function decrementStock(db: ShopDatabase, items: InventoryItem[]): Promise<void> {\n if (items.length === 0) return;\n\n const updatedAt = Date.now();\n for (const { sku, quantity } of items) {\n const result = await db\n .prepare(\"UPDATE inventory SET stock = stock - ?, updated_at = ? WHERE sku = ? AND stock >= ?\")\n .bind(quantity, updatedAt, sku, quantity)\n .run();\n\n if (result.meta.changes === 0) {\n console.error(\"[inventory] stock decrement skipped: insufficient stock\", { sku, quantity });\n }\n }\n}\n\nexport async function restoreStock(db: ShopDatabase, items: InventoryItem[]): Promise<void> {\n if (items.length === 0) return;\n\n const updatedAt = Date.now();\n for (const { sku, quantity } of items) {\n await db\n .prepare(\"UPDATE inventory SET stock = stock + ?, updated_at = ? WHERE sku = ?\")\n .bind(quantity, updatedAt, sku)\n .run();\n }\n}\n\nexport async function setStock(\n db: ShopDatabase,\n sku: string,\n stock: number,\n updatedAt = Date.now(),\n): Promise<SetStockResult> {\n const result = await db\n .prepare(\"UPDATE inventory SET stock = ?, updated_at = ? WHERE sku = ?\")\n .bind(stock, updatedAt, sku)\n .run();\n return result.meta.changes === 0 ? \"not_found\" : \"ok\";\n}\n\nexport async function adjustStock(db: ShopDatabase, sku: string, delta: number): Promise<AdjustStockResult> {\n const existing = await db.prepare(\"SELECT stock FROM inventory WHERE sku = ?\").bind(sku).first<{ stock: number }>();\n if (!existing) return \"not_found\";\n\n const result = await db\n .prepare(\"UPDATE inventory SET stock = stock + ?, updated_at = ? WHERE sku = ? AND stock + ? >= 0\")\n .bind(delta, Date.now(), sku, delta)\n .run();\n\n if (result.meta.changes === 0) return \"insufficient\";\n return \"ok\";\n}\n","import type { WebhookItem, WebhookOrder, WebhookRecordedOrder } from \"../webhook.js\";\nimport type { ShopDatabase } from \"./d1.js\";\nimport type { OrdersRow } from \"./types.generated.js\";\n\nexport type UnshippedOrder = Pick<OrdersRow, \"id\" | \"name\" | \"items_json\" | \"amount\" | \"created_at\">;\n\nexport type Carrier = \"yamato\" | \"sagawa\" | \"japanpost\" | \"other\";\n\nexport type MarkShippedResult = \"ok\" | \"not_pending\";\nexport type UpdateTrackingResult = \"ok\" | \"not_found\";\n\nfunction parseCheckoutItems(items: string | null | undefined): WebhookItem[] {\n if (!items) return [];\n\n try {\n const parsed: unknown = JSON.parse(items);\n if (!Array.isArray(parsed)) return [];\n\n return parsed.filter((item): item is WebhookItem => {\n if (typeof item !== \"object\" || item === null) return false;\n const { sku, quantity } = item as { sku?: unknown; quantity?: unknown };\n return typeof sku === \"string\" && typeof quantity === \"number\" && Number.isSafeInteger(quantity) && quantity > 0;\n });\n } catch {\n return [];\n }\n}\n\nexport async function recordOrder(\n db: ShopDatabase,\n order: WebhookOrder,\n extrasJson: string | null = null,\n): Promise<void> {\n await db\n .prepare(\n `INSERT INTO orders (\n id, email, name, phone, items_json, amount, shipping, address_json,\n payment_intent, custom_fields_json, run_id, extras_json, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n order.id,\n order.email,\n order.name,\n order.phone,\n JSON.stringify(order.items),\n order.amount,\n order.shippingCost,\n order.shippingAddress ? JSON.stringify(order.shippingAddress) : null,\n order.paymentIntent,\n order.customFields ? JSON.stringify(order.customFields) : null,\n order.metadata?.run_id ?? null,\n extrasJson,\n order.createdAt,\n )\n .run();\n}\n\nexport async function recordRefund(\n db: ShopDatabase,\n paymentIntentId: string,\n): Promise<WebhookRecordedOrder | null> {\n await db.prepare(\"UPDATE orders SET status = ? WHERE payment_intent = ?\").bind(\"refunded\", paymentIntentId).run();\n const orderRow = await db\n .prepare(\"SELECT items_json FROM orders WHERE payment_intent = ?\")\n .bind(paymentIntentId)\n .first<{ items_json: string | null }>();\n if (!orderRow) return null;\n return { items: parseCheckoutItems(orderRow.items_json), offerSlug: null };\n}\n\nexport async function recordExpiry(\n db: ShopDatabase,\n sessionId: string,\n): Promise<WebhookRecordedOrder | null> {\n await db.prepare(\"UPDATE orders SET status = ? WHERE id = ?\").bind(\"cancelled\", sessionId).run();\n const orderRow = await db\n .prepare(\"SELECT items_json FROM orders WHERE id = ?\")\n .bind(sessionId)\n .first<{ items_json: string | null }>();\n if (!orderRow) return null;\n return { items: parseCheckoutItems(orderRow.items_json), offerSlug: null };\n}\n\nexport async function recordEmailResult(\n db: ShopDatabase,\n sessionId: string,\n status: \"sent\" | \"failed\",\n): Promise<void> {\n await db.prepare(\"UPDATE orders SET email_status = ? WHERE id = ?\").bind(status, sessionId).run();\n}\n\nexport async function listUnshipped(db: ShopDatabase, cutoff: number): Promise<UnshippedOrder[]> {\n const result = await db\n .prepare(\n `SELECT id, name, items_json, amount, created_at\n FROM orders\n WHERE status = ? AND created_at < ?\n ORDER BY created_at ASC`,\n )\n .bind(\"pending\", cutoff)\n .all<UnshippedOrder>();\n return result.results ?? [];\n}\n\nexport async function getOrder(\n db: ShopDatabase,\n id: string,\n): Promise<Pick<OrdersRow, \"id\" | \"status\" | \"tracking_number\" | \"carrier\"> | null> {\n const row = await db\n .prepare(\"SELECT id, status, tracking_number, carrier FROM orders WHERE id = ?\")\n .bind(id)\n .first<Pick<OrdersRow, \"id\" | \"status\" | \"tracking_number\" | \"carrier\">>();\n return row ?? null;\n}\n\nexport async function markShipped(\n db: ShopDatabase,\n id: string,\n options: { trackingNumber?: string | null; carrier?: Carrier | null; shippedAt?: number } = {},\n): Promise<MarkShippedResult> {\n const result = await db\n .prepare(\n `UPDATE orders\n SET status = ?, shipped_at = ?, tracking_number = COALESCE(?, tracking_number), carrier = COALESCE(?, carrier)\n WHERE id = ? AND status = ?`,\n )\n .bind(\"shipped\", options.shippedAt ?? Date.now(), options.trackingNumber ?? null, options.carrier ?? null, id, \"pending\")\n .run();\n return result.meta.changes === 0 ? \"not_pending\" : \"ok\";\n}\n\nexport async function updateTracking(\n db: ShopDatabase,\n id: string,\n trackingNumber: string,\n carrier: string | null,\n): Promise<UpdateTrackingResult> {\n const result = await db\n .prepare(\"UPDATE orders SET tracking_number = ?, carrier = ? WHERE id = ?\")\n .bind(trackingNumber, carrier, id)\n .run();\n return result.meta.changes === 0 ? \"not_found\" : \"ok\";\n}\n","import type { ShopDatabase } from \"./d1.js\";\nimport type { ProductsRow } from \"./types.generated.js\";\n\nexport type ProductWriteFields = {\n name: string;\n description: string | null;\n price: number;\n shippingUnits: number;\n images: string[];\n gtin: string | null;\n category: string | null;\n active: 0 | 1;\n lowStockThreshold: number | null;\n};\n\nexport type CreateProductResult = \"ok\" | \"conflict\";\nexport type UpdateProductResult = \"ok\" | \"not_found\";\nexport type DiscontinueProductResult = \"ok\" | \"not_found\" | \"already_inactive\";\n\nexport async function listProductRows(\n db: ShopDatabase,\n options: { includeInactive?: boolean } = {},\n): Promise<ProductsRow[]> {\n const sql = options.includeInactive\n ? `SELECT sku, name, description, price, shipping_units, images_json, gtin, category,\n active, low_stock_threshold, created_at, updated_at, updated_by\n FROM products\n ORDER BY sku ASC`\n : `SELECT sku, name, description, price, shipping_units, images_json, gtin, category,\n active, low_stock_threshold, created_at, updated_at, updated_by\n FROM products\n WHERE active = 1\n ORDER BY sku ASC`;\n\n const result = await db.prepare(sql).all<ProductsRow>();\n return result.results ?? [];\n}\n\nexport async function getProductRow(db: ShopDatabase, sku: string): Promise<ProductsRow | null> {\n const row = await db\n .prepare(\n `SELECT sku, name, description, price, shipping_units, images_json, gtin, category,\n active, low_stock_threshold, created_at, updated_at, updated_by\n FROM products\n WHERE sku = ?`,\n )\n .bind(sku)\n .first<ProductsRow>();\n return row ?? null;\n}\n\nexport async function createProduct(\n db: ShopDatabase,\n sku: string,\n fields: ProductWriteFields,\n updatedBy: string | null,\n now = Date.now(),\n): Promise<CreateProductResult> {\n const existing = await db.prepare(\"SELECT sku FROM products WHERE sku = ?\").bind(sku).first<{ sku: string }>();\n if (existing) return \"conflict\";\n\n const imagesJson = fields.images.length > 0 ? JSON.stringify(fields.images) : null;\n await db.batch([\n db\n .prepare(\n `INSERT INTO products (\n sku, name, description, price, shipping_units, images_json, gtin, category,\n active, low_stock_threshold, created_at, updated_at, updated_by\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n sku,\n fields.name,\n fields.description,\n fields.price,\n fields.shippingUnits,\n imagesJson,\n fields.gtin,\n fields.category,\n fields.active,\n fields.lowStockThreshold,\n now,\n now,\n updatedBy,\n ),\n db.prepare(\"INSERT OR IGNORE INTO inventory (sku, stock, updated_at) VALUES (?, ?, ?)\").bind(sku, 0, now),\n ]);\n return \"ok\";\n}\n\nexport async function updateProduct(\n db: ShopDatabase,\n sku: string,\n fields: Partial<ProductWriteFields>,\n updatedBy: string | null,\n now = Date.now(),\n): Promise<{ result: UpdateProductResult; before: ProductsRow | null; after: ProductsRow | null }> {\n const existing = await getProductRow(db, sku);\n if (!existing) return { result: \"not_found\", before: null, after: null };\n\n const next = {\n name: fields.name ?? existing.name,\n description: fields.description !== undefined ? fields.description : existing.description,\n price: fields.price ?? existing.price,\n shippingUnits: fields.shippingUnits ?? existing.shipping_units,\n imagesJson:\n fields.images !== undefined\n ? fields.images.length > 0\n ? JSON.stringify(fields.images)\n : null\n : existing.images_json,\n gtin: fields.gtin !== undefined ? fields.gtin : existing.gtin,\n category: fields.category !== undefined ? fields.category : existing.category,\n active: fields.active ?? existing.active,\n lowStockThreshold:\n fields.lowStockThreshold !== undefined ? fields.lowStockThreshold : existing.low_stock_threshold,\n };\n\n await db\n .prepare(\n `UPDATE products\n SET name = ?, description = ?, price = ?, shipping_units = ?, images_json = ?,\n gtin = ?, category = ?, active = ?, low_stock_threshold = ?, updated_at = ?, updated_by = ?\n WHERE sku = ?`,\n )\n .bind(\n next.name,\n next.description,\n next.price,\n next.shippingUnits,\n next.imagesJson,\n next.gtin,\n next.category,\n next.active,\n next.lowStockThreshold,\n now,\n updatedBy,\n sku,\n )\n .run();\n\n const after = await getProductRow(db, sku);\n return { result: \"ok\", before: existing, after };\n}\n\nexport async function discontinueProduct(\n db: ShopDatabase,\n sku: string,\n updatedBy: string | null,\n now = Date.now(),\n): Promise<DiscontinueProductResult> {\n const existing = await db\n .prepare(\"SELECT sku, active FROM products WHERE sku = ?\")\n .bind(sku)\n .first<{ sku: string; active: number }>();\n if (!existing) return \"not_found\";\n if (existing.active === 0) return \"already_inactive\";\n\n await db\n .prepare(\"UPDATE products SET active = 0, updated_at = ?, updated_by = ? WHERE sku = ?\")\n .bind(now, updatedBy, sku)\n .run();\n return \"ok\";\n}\n","// Generated from schema/snapshot.json. Do not edit by hand.\nexport const SNAPSHOT_SCHEMA_VERSION = 7 as const;\n\nexport type AdminAuditLogRow = {\n id: number;\n entity_type: string;\n entity_id: string;\n action: string;\n delta: number | null;\n reason: string | null;\n admin_email: string | null;\n actor: string;\n detail_json: string | null;\n created_at: number;\n};\nexport type InventoryRow = {\n sku: string;\n stock: number;\n updated_at: number;\n};\nexport type OrdersRow = {\n id: string;\n email: string;\n name: string | null;\n phone: string | null;\n items_json: string;\n amount: number;\n shipping: number | null;\n address_json: string | null;\n payment_intent: string | null;\n custom_fields_json: string | null;\n status: string;\n run_id: string | null;\n email_status: string | null;\n created_at: number;\n shipped_at: number | null;\n tracking_number: string | null;\n carrier: string | null;\n extras_json: string | null;\n};\nexport type ProcessedEventsRow = {\n event_id: string;\n processed_at: number;\n};\nexport type ProductsRow = {\n sku: string;\n name: string;\n description: string | null;\n price: number;\n shipping_units: number;\n images_json: string | null;\n gtin: string | null;\n category: string | null;\n active: number;\n low_stock_threshold: number | null;\n created_at: number;\n updated_at: number;\n updated_by: string | null;\n};\n"],"mappings":";AAiBA,eAAsB,cACpB,IACA,OACA,YAAY,KAAK,IAAI,GACN;AACf,QAAM,GACH;AAAA,IACC;AAAA;AAAA;AAAA,EAGF,EACC;AAAA,IACC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,SAAS;AAAA,IACf,MAAM,UAAU;AAAA,IAChB,MAAM,cAAc;AAAA,IACpB,MAAM,SAAS;AAAA,IACf,MAAM,WAAW,SAAY,OAAO,KAAK,UAAU,MAAM,MAAM;AAAA,IAC/D;AAAA,EACF,EACC,IAAI;AACT;AAEA,eAAsB,aACpB,IACA,UAII,CAAC,GACwB;AAC7B,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,UAAoB,CAAC;AAC3B,QAAM,QAAmB,CAAC;AAE1B,MAAI,QAAQ,YAAY;AACtB,YAAQ,KAAK,iBAAiB;AAC9B,UAAM,KAAK,QAAQ,UAAU;AAAA,EAC/B;AACA,MAAI,QAAQ,UAAU;AACpB,YAAQ,KAAK,eAAe;AAC5B,UAAM,KAAK,QAAQ,QAAQ;AAAA,EAC7B;AAEA,QAAM,QAAQ,QAAQ,SAAS,IAAI,SAAS,QAAQ,KAAK,OAAO,CAAC,KAAK;AACtE,QAAM,SAAS,MAAM,GAClB;AAAA,IACC;AAAA;AAAA,SAEG,KAAK;AAAA;AAAA;AAAA,EAGV,EACC,KAAK,GAAG,OAAO,KAAK,EACpB,IAAsB;AAEzB,SAAO,OAAO,WAAW,CAAC;AAC5B;;;AC1EA,eAAsB,WAAW,IAAkB,SAAiB,MAAM,KAAK,IAAI,GAAqB;AACtG,QAAM,QAAQ,MAAM,GACjB,QAAQ,+EAA+E,EACvF,KAAK,SAAS,GAAG,EACjB,IAAI;AACP,SAAO,MAAM,KAAK,YAAY;AAChC;;;ACNO,SAAS,YAAiC,QAAW,YAA8C;AACxG,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,SAAkB,KAAK,MAAM,UAAU;AAC7C,SAAO,OAAO,MAAM,MAAM;AAC5B;AAEO,SAAS,gBAAqC,QAAW,OAAwB;AACtF,SAAO,KAAK,UAAU,OAAO,MAAM,KAAK,CAAC;AAC3C;;;ACHO,IAAM,kBAAkB;AAK/B,eAAsB,SAAS,IAAkB,MAA8C;AAC7F,MAAI,KAAK,WAAW,EAAG,QAAO,oBAAI,IAAI;AAEtC,QAAM,eAAe,KAAK,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI;AAClD,QAAM,SAAS,MAAM,GAClB,QAAQ,kDAAkD,YAAY,GAAG,EACzE,KAAK,GAAG,IAAI,EACZ,IAAoC;AAEvC,SAAO,IAAI,KAAK,OAAO,WAAW,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC;AAC1E;AAEA,eAAsB,eAAe,IAAkB,OAAuC;AAC5F,MAAI,MAAM,WAAW,EAAG;AAExB,QAAM,YAAY,KAAK,IAAI;AAC3B,aAAW,EAAE,KAAK,SAAS,KAAK,OAAO;AACrC,UAAM,SAAS,MAAM,GAClB,QAAQ,qFAAqF,EAC7F,KAAK,UAAU,WAAW,KAAK,QAAQ,EACvC,IAAI;AAEP,QAAI,OAAO,KAAK,YAAY,GAAG;AAC7B,cAAQ,MAAM,2DAA2D,EAAE,KAAK,SAAS,CAAC;AAAA,IAC5F;AAAA,EACF;AACF;AAEA,eAAsB,aAAa,IAAkB,OAAuC;AAC1F,MAAI,MAAM,WAAW,EAAG;AAExB,QAAM,YAAY,KAAK,IAAI;AAC3B,aAAW,EAAE,KAAK,SAAS,KAAK,OAAO;AACrC,UAAM,GACH,QAAQ,sEAAsE,EAC9E,KAAK,UAAU,WAAW,GAAG,EAC7B,IAAI;AAAA,EACT;AACF;AAEA,eAAsB,SACpB,IACA,KACA,OACA,YAAY,KAAK,IAAI,GACI;AACzB,QAAM,SAAS,MAAM,GAClB,QAAQ,8DAA8D,EACtE,KAAK,OAAO,WAAW,GAAG,EAC1B,IAAI;AACP,SAAO,OAAO,KAAK,YAAY,IAAI,cAAc;AACnD;AAEA,eAAsB,YAAY,IAAkB,KAAa,OAA2C;AAC1G,QAAM,WAAW,MAAM,GAAG,QAAQ,2CAA2C,EAAE,KAAK,GAAG,EAAE,MAAyB;AAClH,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,SAAS,MAAM,GAClB,QAAQ,yFAAyF,EACjG,KAAK,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,EAClC,IAAI;AAEP,MAAI,OAAO,KAAK,YAAY,EAAG,QAAO;AACtC,SAAO;AACT;;;ACjEA,SAAS,mBAAmB,OAAiD;AAC3E,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,KAAK;AACxC,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAEpC,WAAO,OAAO,OAAO,CAAC,SAA8B;AAClD,UAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AACtD,YAAM,EAAE,KAAK,SAAS,IAAI;AAC1B,aAAO,OAAO,QAAQ,YAAY,OAAO,aAAa,YAAY,OAAO,cAAc,QAAQ,KAAK,WAAW;AAAA,IACjH,CAAC;AAAA,EACH,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,YACpB,IACA,OACA,aAA4B,MACb;AACf,QAAM,GACH;AAAA,IACC;AAAA;AAAA;AAAA;AAAA,EAIF,EACC;AAAA,IACC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK,UAAU,MAAM,KAAK;AAAA,IAC1B,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,kBAAkB,KAAK,UAAU,MAAM,eAAe,IAAI;AAAA,IAChE,MAAM;AAAA,IACN,MAAM,eAAe,KAAK,UAAU,MAAM,YAAY,IAAI;AAAA,IAC1D,MAAM,UAAU,UAAU;AAAA,IAC1B;AAAA,IACA,MAAM;AAAA,EACR,EACC,IAAI;AACT;AAEA,eAAsB,aACpB,IACA,iBACsC;AACtC,QAAM,GAAG,QAAQ,uDAAuD,EAAE,KAAK,YAAY,eAAe,EAAE,IAAI;AAChH,QAAM,WAAW,MAAM,GACpB,QAAQ,wDAAwD,EAChE,KAAK,eAAe,EACpB,MAAqC;AACxC,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,EAAE,OAAO,mBAAmB,SAAS,UAAU,GAAG,WAAW,KAAK;AAC3E;AAEA,eAAsB,aACpB,IACA,WACsC;AACtC,QAAM,GAAG,QAAQ,2CAA2C,EAAE,KAAK,aAAa,SAAS,EAAE,IAAI;AAC/F,QAAM,WAAW,MAAM,GACpB,QAAQ,4CAA4C,EACpD,KAAK,SAAS,EACd,MAAqC;AACxC,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,EAAE,OAAO,mBAAmB,SAAS,UAAU,GAAG,WAAW,KAAK;AAC3E;AAEA,eAAsB,kBACpB,IACA,WACA,QACe;AACf,QAAM,GAAG,QAAQ,iDAAiD,EAAE,KAAK,QAAQ,SAAS,EAAE,IAAI;AAClG;AAEA,eAAsB,cAAc,IAAkB,QAA2C;AAC/F,QAAM,SAAS,MAAM,GAClB;AAAA,IACC;AAAA;AAAA;AAAA;AAAA,EAIF,EACC,KAAK,WAAW,MAAM,EACtB,IAAoB;AACvB,SAAO,OAAO,WAAW,CAAC;AAC5B;AAEA,eAAsB,SACpB,IACA,IACkF;AAClF,QAAM,MAAM,MAAM,GACf,QAAQ,sEAAsE,EAC9E,KAAK,EAAE,EACP,MAAwE;AAC3E,SAAO,OAAO;AAChB;AAEA,eAAsB,YACpB,IACA,IACA,UAA4F,CAAC,GACjE;AAC5B,QAAM,SAAS,MAAM,GAClB;AAAA,IACC;AAAA;AAAA;AAAA,EAGF,EACC,KAAK,WAAW,QAAQ,aAAa,KAAK,IAAI,GAAG,QAAQ,kBAAkB,MAAM,QAAQ,WAAW,MAAM,IAAI,SAAS,EACvH,IAAI;AACP,SAAO,OAAO,KAAK,YAAY,IAAI,gBAAgB;AACrD;AAEA,eAAsB,eACpB,IACA,IACA,gBACA,SAC+B;AAC/B,QAAM,SAAS,MAAM,GAClB,QAAQ,iEAAiE,EACzE,KAAK,gBAAgB,SAAS,EAAE,EAChC,IAAI;AACP,SAAO,OAAO,KAAK,YAAY,IAAI,cAAc;AACnD;;;AC5HA,eAAsB,gBACpB,IACA,UAAyC,CAAC,GAClB;AACxB,QAAM,MAAM,QAAQ,kBAChB;AAAA;AAAA;AAAA,2BAIA;AAAA;AAAA;AAAA;AAAA;AAMJ,QAAM,SAAS,MAAM,GAAG,QAAQ,GAAG,EAAE,IAAiB;AACtD,SAAO,OAAO,WAAW,CAAC;AAC5B;AAEA,eAAsB,cAAc,IAAkB,KAA0C;AAC9F,QAAM,MAAM,MAAM,GACf;AAAA,IACC;AAAA;AAAA;AAAA;AAAA,EAIF,EACC,KAAK,GAAG,EACR,MAAmB;AACtB,SAAO,OAAO;AAChB;AAEA,eAAsB,cACpB,IACA,KACA,QACA,WACA,MAAM,KAAK,IAAI,GACe;AAC9B,QAAM,WAAW,MAAM,GAAG,QAAQ,wCAAwC,EAAE,KAAK,GAAG,EAAE,MAAuB;AAC7G,MAAI,SAAU,QAAO;AAErB,QAAM,aAAa,OAAO,OAAO,SAAS,IAAI,KAAK,UAAU,OAAO,MAAM,IAAI;AAC9E,QAAM,GAAG,MAAM;AAAA,IACb,GACG;AAAA,MACC;AAAA;AAAA;AAAA;AAAA,IAIF,EACC;AAAA,MACC;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACF,GAAG,QAAQ,2EAA2E,EAAE,KAAK,KAAK,GAAG,GAAG;AAAA,EAC1G,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,cACpB,IACA,KACA,QACA,WACA,MAAM,KAAK,IAAI,GACkF;AACjG,QAAM,WAAW,MAAM,cAAc,IAAI,GAAG;AAC5C,MAAI,CAAC,SAAU,QAAO,EAAE,QAAQ,aAAa,QAAQ,MAAM,OAAO,KAAK;AAEvE,QAAM,OAAO;AAAA,IACX,MAAM,OAAO,QAAQ,SAAS;AAAA,IAC9B,aAAa,OAAO,gBAAgB,SAAY,OAAO,cAAc,SAAS;AAAA,IAC9E,OAAO,OAAO,SAAS,SAAS;AAAA,IAChC,eAAe,OAAO,iBAAiB,SAAS;AAAA,IAChD,YACE,OAAO,WAAW,SACd,OAAO,OAAO,SAAS,IACrB,KAAK,UAAU,OAAO,MAAM,IAC5B,OACF,SAAS;AAAA,IACf,MAAM,OAAO,SAAS,SAAY,OAAO,OAAO,SAAS;AAAA,IACzD,UAAU,OAAO,aAAa,SAAY,OAAO,WAAW,SAAS;AAAA,IACrE,QAAQ,OAAO,UAAU,SAAS;AAAA,IAClC,mBACE,OAAO,sBAAsB,SAAY,OAAO,oBAAoB,SAAS;AAAA,EACjF;AAEA,QAAM,GACH;AAAA,IACC;AAAA;AAAA;AAAA;AAAA,EAIF,EACC;AAAA,IACC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,IAAI;AAEP,QAAM,QAAQ,MAAM,cAAc,IAAI,GAAG;AACzC,SAAO,EAAE,QAAQ,MAAM,QAAQ,UAAU,MAAM;AACjD;AAEA,eAAsB,mBACpB,IACA,KACA,WACA,MAAM,KAAK,IAAI,GACoB;AACnC,QAAM,WAAW,MAAM,GACpB,QAAQ,gDAAgD,EACxD,KAAK,GAAG,EACR,MAAuC;AAC1C,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,GACH,QAAQ,8EAA8E,EACtF,KAAK,KAAK,WAAW,GAAG,EACxB,IAAI;AACP,SAAO;AACT;;;AClKO,IAAM,0BAA0B;","names":[]}
|