@develemit/commerce-server 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -28,6 +28,9 @@ var InvalidCursorError = class extends Error {
28
28
  }
29
29
  };
30
30
  var PG_TIMESTAMP = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?[+-]\d{2}(:?\d{2})?$/;
31
+ function isCursorTimestamp(at) {
32
+ return PG_TIMESTAMP.test(at) || !Number.isNaN(Date.parse(at));
33
+ }
31
34
  function encodeCursor({ at, key }) {
32
35
  return Buffer.from(`${at}|${key}`).toString("base64url");
33
36
  }
@@ -677,7 +680,7 @@ var adminPlugin = async (app, { db, currency, authorize, storage }) => {
677
680
  if (!part) throw new InvalidImageError("An image file is required");
678
681
  const bytes = await part.toBuffer();
679
682
  const fields = part.fields;
680
- const alt = typeof fields.alt?.value === "string" ? fields.alt.value : void 0;
683
+ const alt = typeof fields.alt?.value === "string" ? productImagePatchSchema.parse({ alt: fields.alt.value }).alt : void 0;
681
684
  const product = await addProductImage(db, storage, req.params.id, {
682
685
  bytes,
683
686
  alt
@@ -1146,7 +1149,8 @@ function conditions(filter) {
1146
1149
  );
1147
1150
  }
1148
1151
  if (filter.after) {
1149
- const { createdAt, id } = filter.after;
1152
+ const { id } = filter.after;
1153
+ const createdAt = sql5`${filter.after.createdAt}::timestamptz`;
1150
1154
  parts.push(
1151
1155
  or3(
1152
1156
  lt2(orders.createdAt, createdAt),
@@ -1168,7 +1172,8 @@ async function selectOrderPage(db, filter) {
1168
1172
  totalCents: sql5`${orders.subtotalCents} - ${orders.discountCents} + ${orders.shippingCents}`,
1169
1173
  currency: orders.currency,
1170
1174
  itemCount,
1171
- createdAt: orders.createdAt
1175
+ createdAt: orders.createdAt,
1176
+ createdAtKey: sql5`${orders.createdAt}::text`
1172
1177
  }).from(orders).where(conditions(filter)).orderBy(desc(orders.createdAt), desc(orders.id)).limit(filter.limit);
1173
1178
  }
1174
1179
  async function findOrderRefunds(db, orderId) {
@@ -1385,9 +1390,9 @@ function isOrderId(value) {
1385
1390
  function decodeOrderCursor(raw) {
1386
1391
  const { at, key } = decodeCursor(
1387
1392
  raw,
1388
- (c) => isOrderId(c.key) && !Number.isNaN(+new Date(c.at))
1393
+ (c) => isOrderId(c.key) && isCursorTimestamp(c.at)
1389
1394
  );
1390
- return { createdAt: new Date(at), id: key };
1395
+ return { createdAt: at, id: key };
1391
1396
  }
1392
1397
  async function listAdminOrders(db, query) {
1393
1398
  const rows = await selectOrderPage(db, {
@@ -1400,8 +1405,16 @@ async function listAdminOrders(db, query) {
1400
1405
  const page = rows.slice(0, query.limit);
1401
1406
  const last = page[page.length - 1];
1402
1407
  return {
1403
- items: page.map((r) => ({ ...r, createdAt: r.createdAt.toISOString() })),
1404
- nextCursor: rows.length > query.limit && last ? encodeCursor({ at: last.createdAt.toISOString(), key: last.id }) : null
1408
+ items: page.map((r) => ({
1409
+ id: r.id,
1410
+ status: r.status,
1411
+ email: r.email,
1412
+ totalCents: r.totalCents,
1413
+ currency: r.currency,
1414
+ itemCount: r.itemCount,
1415
+ createdAt: r.createdAt.toISOString()
1416
+ })),
1417
+ nextCursor: rows.length > query.limit && last ? encodeCursor({ at: last.createdAtKey, key: last.id }) : null
1405
1418
  };
1406
1419
  }
1407
1420
  async function getAdminOrder(db, id) {
@@ -1565,7 +1578,8 @@ function listConditions(filter) {
1565
1578
  if (filter.status) parts.push(statusCondition(filter.status, filter.now));
1566
1579
  if (filter.q) parts.push(ilike3(discounts.code, `${escapeLike3(filter.q)}%`));
1567
1580
  if (filter.after) {
1568
- const { createdAt, id } = filter.after;
1581
+ const { id } = filter.after;
1582
+ const createdAt = sql8`${filter.after.createdAt}::timestamptz`;
1569
1583
  parts.push(
1570
1584
  or5(
1571
1585
  lt3(discounts.createdAt, createdAt),
@@ -1589,7 +1603,10 @@ var withCounts = {
1589
1603
  totalRedemptions: redemptionCount()
1590
1604
  };
1591
1605
  async function selectDiscountPage(db, filter) {
1592
- return db.select(withCounts).from(discounts).where(listConditions(filter)).orderBy(desc2(discounts.createdAt), desc2(discounts.id)).limit(filter.limit);
1606
+ return db.select({
1607
+ ...withCounts,
1608
+ createdAtKey: sql8`${discounts.createdAt}::text`
1609
+ }).from(discounts).where(listConditions(filter)).orderBy(desc2(discounts.createdAt), desc2(discounts.id)).limit(filter.limit);
1593
1610
  }
1594
1611
  async function findDiscountById(db, id, options = {}) {
1595
1612
  const query = db.select(withCounts).from(discounts).where(eq8(discounts.id, id));
@@ -2178,9 +2195,9 @@ function toDiscount(row) {
2178
2195
  function decodeDiscountCursor(raw) {
2179
2196
  const { at, key } = decodeCursor(
2180
2197
  raw,
2181
- (c) => isDiscountId(c.key) && !Number.isNaN(+new Date(c.at))
2198
+ (c) => isDiscountId(c.key) && isCursorTimestamp(c.at)
2182
2199
  );
2183
- return { createdAt: new Date(at), id: key };
2200
+ return { createdAt: at, id: key };
2184
2201
  }
2185
2202
  async function listAdminDiscounts(db, query, now = /* @__PURE__ */ new Date()) {
2186
2203
  const rows = await selectDiscountPage(db, {
@@ -2194,7 +2211,7 @@ async function listAdminDiscounts(db, query, now = /* @__PURE__ */ new Date()) {
2194
2211
  const last = page[page.length - 1];
2195
2212
  return {
2196
2213
  items: page.map(toDiscount),
2197
- nextCursor: rows.length > query.limit && last ? encodeCursor({ at: last.createdAt.toISOString(), key: last.id }) : null
2214
+ nextCursor: rows.length > query.limit && last ? encodeCursor({ at: last.createdAtKey, key: last.id }) : null
2198
2215
  };
2199
2216
  }
2200
2217
  async function getAdminDiscount(db, id) {
@@ -2219,9 +2236,15 @@ async function createDiscount(db, input) {
2219
2236
  if (!id) throw new DiscountCodeTakenError(data.code);
2220
2237
  return getAdminDiscount(db, id);
2221
2238
  }
2239
+ function sameValue(a, b) {
2240
+ if (typeof a === "string" && typeof b === "string") {
2241
+ return a.toUpperCase() === b.toUpperCase();
2242
+ }
2243
+ return a === b;
2244
+ }
2222
2245
  function changedLockedFields(row, patch) {
2223
2246
  return LOCKED_AFTER_USE.filter(
2224
- (field) => patch[field] !== void 0 && patch[field] !== row[field]
2247
+ (field) => patch[field] !== void 0 && !sameValue(patch[field], row[field])
2225
2248
  );
2226
2249
  }
2227
2250
  function assertCoherent(row, patch) {
@@ -2333,6 +2356,27 @@ var adminDiscountsPlugin = async (app, { db, authorize }) => {
2333
2356
  );
2334
2357
  };
2335
2358
 
2359
+ // src/orders/dev-checkout.ts
2360
+ function assertDevCheckoutAllowed() {
2361
+ if (process.env.NODE_ENV === "production") {
2362
+ throw new Error("devCheckout is not allowed when NODE_ENV=production");
2363
+ }
2364
+ }
2365
+ var devCheckoutRoutes = async (app, { db, payment, urls, hooks }) => {
2366
+ assertDevCheckoutAllowed();
2367
+ app.get("/checkout/dev/:sessionId/complete", async (req, reply) => {
2368
+ const { sessionId } = req.params;
2369
+ if (!payment.sessions.some((s) => s.id === sessionId)) {
2370
+ return reply.code(404).send({ error: "SESSION_NOT_FOUND" });
2371
+ }
2372
+ const expire = req.query.outcome === "expired";
2373
+ const { rawBody, signature } = expire ? payment.expired(sessionId) : payment.completed(sessionId);
2374
+ const event = await payment.verifyWebhook(rawBody, signature);
2375
+ await handlePaymentEvent(db, event, hooks, req.log);
2376
+ return reply.redirect(expire ? urls.cancel : urls.success(sessionId), 302);
2377
+ });
2378
+ };
2379
+
2336
2380
  // src/orders/routes.ts
2337
2381
  import {
2338
2382
  checkoutConflictSchema,
@@ -2597,6 +2641,119 @@ var ordersPlugin = async (app, { db, ...settings }) => {
2597
2641
  });
2598
2642
  };
2599
2643
 
2644
+ // src/payment/fake-gateway.ts
2645
+ import { createHmac, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
2646
+ function createFakeGateway({
2647
+ secret = "fake-webhook-secret"
2648
+ } = {}) {
2649
+ const sessions = [];
2650
+ const refunds = [];
2651
+ const refundCalls = [];
2652
+ let pendingFailure;
2653
+ let sessionUrl;
2654
+ let pendingRefundFailure;
2655
+ const hmac = (body) => createHmac("sha256", secret).update(body).digest("hex");
2656
+ function sign(event) {
2657
+ const rawBody = Buffer.from(JSON.stringify(event));
2658
+ return { rawBody, signature: hmac(rawBody) };
2659
+ }
2660
+ return {
2661
+ kind: "fake",
2662
+ useSessionUrl(build) {
2663
+ sessionUrl = build;
2664
+ },
2665
+ sessions,
2666
+ refunds,
2667
+ refundCalls,
2668
+ async createCheckoutSession(input) {
2669
+ if (pendingFailure) {
2670
+ const err = pendingFailure;
2671
+ pendingFailure = void 0;
2672
+ throw err;
2673
+ }
2674
+ const id = `fake_cs_${randomUUID3()}`;
2675
+ sessions.push({ ...input, id });
2676
+ return {
2677
+ id,
2678
+ url: sessionUrl?.(id) ?? input.successUrl.replace(SESSION_ID_PLACEHOLDER, id)
2679
+ };
2680
+ },
2681
+ async verifyWebhook(rawBody, signature) {
2682
+ const expected = Buffer.from(hmac(rawBody));
2683
+ const given = Buffer.from(signature ?? "");
2684
+ if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
2685
+ throw new WebhookSignatureError();
2686
+ }
2687
+ return JSON.parse(rawBody.toString("utf8"));
2688
+ },
2689
+ async refund(input) {
2690
+ refundCalls.push(input);
2691
+ if (pendingRefundFailure) {
2692
+ const err = pendingRefundFailure;
2693
+ pendingRefundFailure = void 0;
2694
+ throw err;
2695
+ }
2696
+ const prior = refunds.find(
2697
+ (r) => r.idempotencyKey === input.idempotencyKey
2698
+ );
2699
+ if (prior) return { gatewayRefundId: prior.gatewayRefundId };
2700
+ const gatewayRefundId = `fake_re_${refunds.length + 1}_${input.sessionId}`;
2701
+ refunds.push({ ...input, gatewayRefundId });
2702
+ return { gatewayRefundId };
2703
+ },
2704
+ failNextSession(error = new Error("Fake gateway failure")) {
2705
+ pendingFailure = error;
2706
+ },
2707
+ failNextRefund(error = new RefundRejectedError("Fake refund failure")) {
2708
+ pendingRefundFailure = error;
2709
+ },
2710
+ failNextRefundUncertain(error = new Error("Fake refund timeout")) {
2711
+ pendingRefundFailure = error;
2712
+ },
2713
+ completed(sessionId, details = {}) {
2714
+ return sign({
2715
+ id: `fake_evt_${randomUUID3()}`,
2716
+ type: "checkout.completed",
2717
+ sessionId,
2718
+ ...details
2719
+ });
2720
+ },
2721
+ asyncPaid(sessionId) {
2722
+ return sign({
2723
+ id: `fake_evt_${randomUUID3()}`,
2724
+ type: "checkout.async_paid",
2725
+ sessionId
2726
+ });
2727
+ },
2728
+ asyncFailed(sessionId) {
2729
+ return sign({
2730
+ id: `fake_evt_${randomUUID3()}`,
2731
+ type: "checkout.async_failed",
2732
+ sessionId
2733
+ });
2734
+ },
2735
+ expired(sessionId) {
2736
+ return sign({
2737
+ id: `fake_evt_${randomUUID3()}`,
2738
+ type: "checkout.expired",
2739
+ sessionId
2740
+ });
2741
+ },
2742
+ refunded(sessionId, refunds2) {
2743
+ return sign({
2744
+ id: `fake_evt_${randomUUID3()}`,
2745
+ type: "payment.refunded",
2746
+ sessionId,
2747
+ refunds: refunds2
2748
+ });
2749
+ },
2750
+ sign
2751
+ };
2752
+ }
2753
+ function isFakeGateway(gateway) {
2754
+ return gateway.kind === "fake";
2755
+ }
2756
+
2600
2757
  // src/plugin.ts
2601
2758
  var commercePlugin = async (app, {
2602
2759
  db,
@@ -2607,7 +2764,8 @@ var commercePlugin = async (app, {
2607
2764
  hooks,
2608
2765
  admin,
2609
2766
  categoryLabels,
2610
- catalogCache
2767
+ catalogCache,
2768
+ devCheckout
2611
2769
  }) => {
2612
2770
  app.get("/health", async () => ({ status: "ok" }));
2613
2771
  await app.register(catalogPlugin, {
@@ -2633,6 +2791,16 @@ var commercePlugin = async (app, {
2633
2791
  shippingCountries,
2634
2792
  hooks
2635
2793
  });
2794
+ if (devCheckout) {
2795
+ assertDevCheckoutAllowed();
2796
+ if (isFakeGateway(payment)) {
2797
+ const apiUrl = typeof devCheckout === "object" ? devCheckout.apiUrl : "";
2798
+ payment.useSessionUrl(
2799
+ (id) => `${apiUrl ?? ""}${app.prefix}/checkout/dev/${id}/complete`
2800
+ );
2801
+ await app.register(devCheckoutRoutes, { db, payment, urls, hooks });
2802
+ }
2803
+ }
2636
2804
  if (admin) {
2637
2805
  await app.register(adminOrdersPlugin, {
2638
2806
  db,
@@ -2765,12 +2933,13 @@ async function refundedEvent(stripe, event) {
2765
2933
  refunds: refunds.filter((refund) => refund.status !== "failed").map(toRefundEvent)
2766
2934
  };
2767
2935
  }
2768
- function createCoupon(stripe, { discount, currency }) {
2936
+ function createCoupon(stripe, { discount, currency, expiresAt }) {
2769
2937
  return stripe.coupons.create({
2770
2938
  amount_off: discount.amountCents,
2771
2939
  currency: currency.toLowerCase(),
2772
2940
  duration: "once",
2773
2941
  max_redemptions: 1,
2942
+ redeem_by: Math.floor(expiresAt.getTime() / 1e3),
2774
2943
  name: discount.code
2775
2944
  });
2776
2945
  }
@@ -2868,111 +3037,6 @@ function createStripeGateway({
2868
3037
  };
2869
3038
  }
2870
3039
 
2871
- // src/payment/fake-gateway.ts
2872
- import { createHmac, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
2873
- function createFakeGateway({
2874
- secret = "fake-webhook-secret"
2875
- } = {}) {
2876
- const sessions = [];
2877
- const refunds = [];
2878
- const refundCalls = [];
2879
- let pendingFailure;
2880
- let pendingRefundFailure;
2881
- const hmac = (body) => createHmac("sha256", secret).update(body).digest("hex");
2882
- function sign(event) {
2883
- const rawBody = Buffer.from(JSON.stringify(event));
2884
- return { rawBody, signature: hmac(rawBody) };
2885
- }
2886
- return {
2887
- sessions,
2888
- refunds,
2889
- refundCalls,
2890
- async createCheckoutSession(input) {
2891
- if (pendingFailure) {
2892
- const err = pendingFailure;
2893
- pendingFailure = void 0;
2894
- throw err;
2895
- }
2896
- const id = `fake_cs_${randomUUID3()}`;
2897
- sessions.push({ ...input, id });
2898
- return {
2899
- id,
2900
- url: input.successUrl.replace(SESSION_ID_PLACEHOLDER, id)
2901
- };
2902
- },
2903
- async verifyWebhook(rawBody, signature) {
2904
- const expected = Buffer.from(hmac(rawBody));
2905
- const given = Buffer.from(signature ?? "");
2906
- if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
2907
- throw new WebhookSignatureError();
2908
- }
2909
- return JSON.parse(rawBody.toString("utf8"));
2910
- },
2911
- async refund(input) {
2912
- refundCalls.push(input);
2913
- if (pendingRefundFailure) {
2914
- const err = pendingRefundFailure;
2915
- pendingRefundFailure = void 0;
2916
- throw err;
2917
- }
2918
- const prior = refunds.find(
2919
- (r) => r.idempotencyKey === input.idempotencyKey
2920
- );
2921
- if (prior) return { gatewayRefundId: prior.gatewayRefundId };
2922
- const gatewayRefundId = `fake_re_${refunds.length + 1}_${input.sessionId}`;
2923
- refunds.push({ ...input, gatewayRefundId });
2924
- return { gatewayRefundId };
2925
- },
2926
- failNextSession(error = new Error("Fake gateway failure")) {
2927
- pendingFailure = error;
2928
- },
2929
- failNextRefund(error = new RefundRejectedError("Fake refund failure")) {
2930
- pendingRefundFailure = error;
2931
- },
2932
- failNextRefundUncertain(error = new Error("Fake refund timeout")) {
2933
- pendingRefundFailure = error;
2934
- },
2935
- completed(sessionId, details = {}) {
2936
- return sign({
2937
- id: `fake_evt_${randomUUID3()}`,
2938
- type: "checkout.completed",
2939
- sessionId,
2940
- ...details
2941
- });
2942
- },
2943
- asyncPaid(sessionId) {
2944
- return sign({
2945
- id: `fake_evt_${randomUUID3()}`,
2946
- type: "checkout.async_paid",
2947
- sessionId
2948
- });
2949
- },
2950
- asyncFailed(sessionId) {
2951
- return sign({
2952
- id: `fake_evt_${randomUUID3()}`,
2953
- type: "checkout.async_failed",
2954
- sessionId
2955
- });
2956
- },
2957
- expired(sessionId) {
2958
- return sign({
2959
- id: `fake_evt_${randomUUID3()}`,
2960
- type: "checkout.expired",
2961
- sessionId
2962
- });
2963
- },
2964
- refunded(sessionId, refunds2) {
2965
- return sign({
2966
- id: `fake_evt_${randomUUID3()}`,
2967
- type: "payment.refunded",
2968
- sessionId,
2969
- refunds: refunds2
2970
- });
2971
- },
2972
- sign
2973
- };
2974
- }
2975
-
2976
3040
  // src/hold-sweeper.ts
2977
3041
  async function sweepExpiredHolds(db) {
2978
3042
  await db.transaction(async (tx) => {
@@ -2987,6 +3051,89 @@ function startHoldSweeper(db, { intervalMs = 6e4, onError } = {}) {
2987
3051
  return { stop: () => clearInterval(timer) };
2988
3052
  }
2989
3053
 
3054
+ // src/emails/order-lines.ts
3055
+ var defaultLabels = {
3056
+ subtotal: "Subtotal",
3057
+ discount: (code) => code ? `Discount (${code})` : "Discount",
3058
+ shipping: (label) => label ? `Shipping (${label})` : "Shipping",
3059
+ total: "Total"
3060
+ };
3061
+ var escapes = {
3062
+ "&": "&",
3063
+ "<": "&lt;",
3064
+ ">": "&gt;",
3065
+ '"': "&quot;",
3066
+ "'": "&#39;"
3067
+ };
3068
+ function escapeHtml(value) {
3069
+ return value.replace(/[&<>"']/g, (c) => escapes[c]);
3070
+ }
3071
+ function addressLines(order) {
3072
+ const a = order.shippingAddress;
3073
+ if (!a) return void 0;
3074
+ const lines = [
3075
+ a.name,
3076
+ a.line1,
3077
+ a.line2,
3078
+ [a.city, a.state, a.postalCode].filter(Boolean).join(" "),
3079
+ a.country
3080
+ ].filter((l) => Boolean(l));
3081
+ return lines.length ? lines : void 0;
3082
+ }
3083
+ function textBlock(lines) {
3084
+ return lines.map((l) => `${l.label} ${l.amount}`).join("\n");
3085
+ }
3086
+ function htmlRows(lines) {
3087
+ return lines.map(
3088
+ (l) => `<tr><td>${escapeHtml(l.label)}</td><td align="right">${escapeHtml(l.amount)}</td></tr>`
3089
+ ).join("");
3090
+ }
3091
+ function orderEmailLines(order, options = {}) {
3092
+ const locale = options.locale ?? "en-US";
3093
+ const labels = { ...defaultLabels, ...options.labels };
3094
+ const money = (cents) => new Intl.NumberFormat(locale, {
3095
+ style: "currency",
3096
+ currency: order.currency
3097
+ }).format(cents / 100);
3098
+ const items = order.items.map((i) => ({
3099
+ label: `${i.name} \xD7 ${i.quantity}`,
3100
+ amount: money(i.priceCents * i.quantity)
3101
+ }));
3102
+ const subtotal = {
3103
+ label: labels.subtotal,
3104
+ amount: money(order.subtotalCents)
3105
+ };
3106
+ const discount = order.discountCents > 0 ? {
3107
+ label: labels.discount(order.discountCode),
3108
+ amount: `-${money(order.discountCents)}`
3109
+ } : void 0;
3110
+ const shipping = {
3111
+ label: labels.shipping(order.shippingLabel),
3112
+ amount: money(order.shippingCents)
3113
+ };
3114
+ const total = { label: labels.total, amount: money(order.totalCents) };
3115
+ const address = addressLines(order);
3116
+ const totals = [subtotal, ...discount ? [discount] : [], shipping, total];
3117
+ return {
3118
+ items,
3119
+ subtotal,
3120
+ discount,
3121
+ shipping,
3122
+ total,
3123
+ address,
3124
+ text: {
3125
+ items: textBlock(items),
3126
+ totals: textBlock(totals),
3127
+ address: address?.join("\n") ?? ""
3128
+ },
3129
+ html: {
3130
+ items: htmlRows(items),
3131
+ totals: htmlRows(totals),
3132
+ address: address?.map((l) => `<p>${escapeHtml(l)}</p>`).join("") ?? ""
3133
+ }
3134
+ };
3135
+ }
3136
+
2990
3137
  // src/db/migrate.ts
2991
3138
  import path from "path";
2992
3139
  import { fileURLToPath } from "url";
@@ -3064,17 +3211,132 @@ function createMemoryStorage(baseUrl = "memory://") {
3064
3211
  }
3065
3212
  };
3066
3213
  }
3214
+
3215
+ // src/storage/disk.ts
3216
+ import { createReadStream, createWriteStream } from "fs";
3217
+ import { mkdir, realpath, rm, stat } from "fs/promises";
3218
+ import path2 from "path";
3219
+ import { pipeline } from "stream/promises";
3220
+ var contentTypes = {
3221
+ ".jpg": "image/jpeg",
3222
+ ".jpeg": "image/jpeg",
3223
+ ".png": "image/png",
3224
+ ".webp": "image/webp",
3225
+ ".gif": "image/gif",
3226
+ ".avif": "image/avif"
3227
+ };
3228
+ function hasTraversalSegment(key) {
3229
+ return key.split(/[\\/]/).some((segment) => segment === "..");
3230
+ }
3231
+ function decodesToTraversal(key) {
3232
+ try {
3233
+ return hasTraversalSegment(decodeURIComponent(key));
3234
+ } catch {
3235
+ return true;
3236
+ }
3237
+ }
3238
+ function isInside(root, target) {
3239
+ return target.startsWith(root + path2.sep);
3240
+ }
3241
+ function resolveKey(dir, key) {
3242
+ if (!key || key.includes("\0") || path2.isAbsolute(key)) return null;
3243
+ if (hasTraversalSegment(key) || decodesToTraversal(key)) return null;
3244
+ const root = path2.resolve(dir);
3245
+ const target = path2.resolve(root, key);
3246
+ return isInside(root, target) ? target : null;
3247
+ }
3248
+ async function realInside(dir, target) {
3249
+ const [root, real] = await Promise.all([
3250
+ realpath(dir),
3251
+ realpath(target)
3252
+ ]).catch(() => [null, null]);
3253
+ return root && real && isInside(root, real) ? real : null;
3254
+ }
3255
+ function createDiskStorage(options) {
3256
+ const { dir, publicUrl } = options;
3257
+ const base = publicUrl.replace(/\/+$/, "");
3258
+ return {
3259
+ async put(key, stream) {
3260
+ const target = resolveKey(dir, key);
3261
+ if (!target)
3262
+ throw new Error(`Invalid storage key: ${JSON.stringify(key)}`);
3263
+ await mkdir(path2.dirname(target), { recursive: true });
3264
+ if (!await realInside(dir, path2.dirname(target)).catch(() => null)) {
3265
+ throw new Error(`Invalid storage key: ${JSON.stringify(key)}`);
3266
+ }
3267
+ await pipeline(stream, createWriteStream(target));
3268
+ return { key, url: `${base}/${key}` };
3269
+ },
3270
+ async delete(key) {
3271
+ const target = resolveKey(dir, key);
3272
+ if (!target)
3273
+ throw new Error(`Invalid storage key: ${JSON.stringify(key)}`);
3274
+ if (!await realInside(dir, path2.dirname(target)).catch(() => null))
3275
+ return;
3276
+ await rm(target, { force: true });
3277
+ }
3278
+ };
3279
+ }
3280
+ async function openStoredFile(dir, key) {
3281
+ const target = resolveKey(dir, key);
3282
+ if (!target) return null;
3283
+ const real = await realInside(dir, target);
3284
+ if (!real) return null;
3285
+ const info = await stat(real).catch(() => null);
3286
+ if (!info?.isFile()) return null;
3287
+ const contentType = contentTypes[path2.extname(real).toLowerCase()];
3288
+ return { stream: createReadStream(real), contentType, size: info.size };
3289
+ }
3290
+
3291
+ // src/payment/resolve.ts
3292
+ function resolvePayment(env = process.env) {
3293
+ const secretKey = env.STRIPE_SECRET_KEY || void 0;
3294
+ const webhookSecret = env.STRIPE_WEBHOOK_SECRET || void 0;
3295
+ if (!secretKey !== !webhookSecret) {
3296
+ throw new Error(
3297
+ "Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET together, or neither"
3298
+ );
3299
+ }
3300
+ if (env.PAYMENT_GATEWAY === "stripe" && !secretKey) {
3301
+ throw new Error("PAYMENT_GATEWAY=stripe needs the Stripe keys");
3302
+ }
3303
+ if (env.PAYMENT_GATEWAY === "fake" || !secretKey) {
3304
+ if (env.NODE_ENV === "production") {
3305
+ throw new Error(
3306
+ "The fake payment gateway is not allowed in production; set the Stripe keys"
3307
+ );
3308
+ }
3309
+ return createFakeGateway();
3310
+ }
3311
+ return createStripeGateway({ secretKey, webhookSecret });
3312
+ }
3313
+ function checkoutUrls(webUrl, {
3314
+ successPath = "/checkout/success",
3315
+ cancelPath = "/shop"
3316
+ } = {}) {
3317
+ const base = webUrl.replace(/\/+$/, "");
3318
+ return {
3319
+ success: (sessionId) => `${base}${successPath}?session_id=${encodeURIComponent(sessionId)}`,
3320
+ cancel: `${base}${cancelPath}`
3321
+ };
3322
+ }
3067
3323
  export {
3068
3324
  ProductNotFoundError,
3069
3325
  SESSION_ID_PLACEHOLDER,
3070
3326
  WebhookSignatureError,
3071
3327
  catalogPlugin,
3328
+ checkoutUrls,
3072
3329
  commercePlugin,
3073
3330
  commerceSchema,
3331
+ createDiskStorage,
3074
3332
  createFakeGateway,
3075
3333
  createMemoryStorage,
3076
3334
  createStripeGateway,
3335
+ isFakeGateway,
3336
+ openStoredFile,
3337
+ orderEmailLines,
3077
3338
  ordersPlugin,
3339
+ resolvePayment,
3078
3340
  runCommerceMigrations,
3079
3341
  seedProducts,
3080
3342
  seedShippingRates,