@gigamusic/checkout 4.0.0 → 4.2.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.d.ts CHANGED
@@ -2,12 +2,25 @@ import { NextRequest } from 'next/server';
2
2
  import Stripe from 'stripe';
3
3
  import { Queries } from '@gigamusic/db';
4
4
  import { StorageProvider } from '@gigamusic/storage';
5
- import { EmailProvider, EmailBranding } from '@gigamusic/email';
5
+ import { EmailProvider, EmailBranding, PurchaseConfirmationArgs } from '@gigamusic/email';
6
6
 
7
+ /** Rendered email: a subject line and an HTML body. */
8
+ interface RenderedEmail {
9
+ subject: string;
10
+ html: string;
11
+ }
7
12
  /** Cart item shape accepted by `createCheckoutHandler`. `id` is required for "release" / "track"; ignored for "catalog". */
8
13
  interface CheckoutCartItem {
9
14
  kind: "release" | "track" | "catalog";
10
15
  id?: number;
16
+ /**
17
+ * Pay-what-you-want override, in cents. When present and ≥ the Stripe card
18
+ * minimum, the customer is charged this instead of the catalog price — the
19
+ * handler clamps it up to the minimum and stamps the charged amount onto the
20
+ * session so the webhook can record the true per-line price. Omit it (the
21
+ * fixed-price default) and the catalog `price` is used. Ignored for "catalog".
22
+ */
23
+ priceCents?: number;
11
24
  }
12
25
  interface CheckoutDeps {
13
26
  stripeSecret: string;
@@ -68,8 +81,23 @@ interface WebhookDeps {
68
81
  baseUrl: string;
69
82
  /** JWT secret for the magic-link order-verification token mailed to the customer. */
70
83
  orderTokenSecret: string;
84
+ /**
85
+ * Lifetime of the order magic-link token embedded in the confirmation email.
86
+ * Accepts any `jose` duration string (e.g. `"1h"`, `"30d"`). Pass `null` for
87
+ * a link that never expires. Defaults to `null` — the emailed link is the
88
+ * customer's durable path back to their downloads, so it should keep working.
89
+ */
90
+ orderTokenExpiry?: string | null;
71
91
  /** Defaults to "/orders/verify". */
72
92
  verifyPath?: string;
93
+ /**
94
+ * Override the purchase-confirmation email rendering. When provided, it's
95
+ * called instead of the built-in `renderPurchaseConfirmation`, giving the
96
+ * consumer full control over the email's subject and HTML — styling, layout,
97
+ * logo, copy — using the same data the default template receives. May be
98
+ * sync or async.
99
+ */
100
+ renderConfirmation?: (args: PurchaseConfirmationArgs) => RenderedEmail | Promise<RenderedEmail>;
73
101
  }
74
102
  interface DownloadDeps {
75
103
  queries: Queries;
package/dist/index.js CHANGED
@@ -8,6 +8,13 @@ import { finished } from 'stream/promises';
8
8
  import archiver from 'archiver';
9
9
 
10
10
  // src/checkout.ts
11
+ var STRIPE_MIN_CHARGE_CENTS = 50;
12
+ function resolveUnitAmount(overrideCents, catalogCents) {
13
+ if (typeof overrideCents === "number" && Number.isFinite(overrideCents) && overrideCents > 0) {
14
+ return Math.max(STRIPE_MIN_CHARGE_CENTS, Math.round(overrideCents));
15
+ }
16
+ return catalogCents;
17
+ }
11
18
  function createCheckoutHandler(deps) {
12
19
  const {
13
20
  stripeSecret,
@@ -69,6 +76,17 @@ function createCheckoutHandler(deps) {
69
76
  if (releaseIds.length === 0 && trackIds.length === 0) {
70
77
  return jsonError("No valid items found", 400);
71
78
  }
79
+ const releaseAmountOverride = new Map(
80
+ items.filter(
81
+ (i) => i.kind === "release" && i.id != null && typeof i.priceCents === "number"
82
+ ).map((i) => [i.id, i.priceCents])
83
+ );
84
+ const trackAmountOverride = new Map(
85
+ items.filter(
86
+ (i) => i.kind === "track" && i.id != null && typeof i.priceCents === "number"
87
+ ).map((i) => [i.id, i.priceCents])
88
+ );
89
+ const chargedAmounts = {};
72
90
  const allReleases = await queries.listPublishedReleases();
73
91
  const releaseById = new Map(allReleases.map((r) => [r.id, r]));
74
92
  const trackContext = /* @__PURE__ */ new Map();
@@ -77,30 +95,41 @@ function createCheckoutHandler(deps) {
77
95
  trackContext.set(track.id, { track, release });
78
96
  }
79
97
  }
80
- const releaseLineItems = releaseIds.map((id) => releaseById.get(id)).filter((r) => r !== void 0).map((r) => ({
81
- price_data: {
82
- currency,
83
- product_data: {
84
- name: r.name,
85
- ...r.coverImageUrl ? { images: [toAbsoluteUrl(r.coverImageUrl, baseUrl)] } : {}
98
+ const releaseLineItems = releaseIds.map((id) => releaseById.get(id)).filter((r) => r !== void 0).map((r) => {
99
+ const amount = resolveUnitAmount(releaseAmountOverride.get(r.id), r.price);
100
+ chargedAmounts[`r${r.id}`] = amount;
101
+ return {
102
+ price_data: {
103
+ currency,
104
+ product_data: {
105
+ name: r.name,
106
+ ...r.coverImageUrl ? { images: [toAbsoluteUrl(r.coverImageUrl, baseUrl)] } : {}
107
+ },
108
+ unit_amount: amount
86
109
  },
87
- unit_amount: r.price
88
- },
89
- quantity: 1
90
- }));
110
+ quantity: 1
111
+ };
112
+ });
91
113
  const trackLineItems = trackIds.map((id) => trackContext.get(id)).filter(
92
114
  (c) => c !== void 0
93
- ).map(({ track, release }) => ({
94
- price_data: {
95
- currency,
96
- product_data: {
97
- name: `${release.name} \u2014 ${track.name}`,
98
- ...release.coverImageUrl ? { images: [toAbsoluteUrl(release.coverImageUrl, baseUrl)] } : {}
115
+ ).map(({ track, release }) => {
116
+ const amount = resolveUnitAmount(
117
+ trackAmountOverride.get(track.id),
118
+ track.price
119
+ );
120
+ chargedAmounts[`t${track.id}`] = amount;
121
+ return {
122
+ price_data: {
123
+ currency,
124
+ product_data: {
125
+ name: `${release.name} \u2014 ${track.name}`,
126
+ ...release.coverImageUrl ? { images: [toAbsoluteUrl(release.coverImageUrl, baseUrl)] } : {}
127
+ },
128
+ unit_amount: amount
99
129
  },
100
- unit_amount: track.price
101
- },
102
- quantity: 1
103
- }));
130
+ quantity: 1
131
+ };
132
+ });
104
133
  const lineItems = [...releaseLineItems, ...trackLineItems];
105
134
  if (lineItems.length === 0) {
106
135
  return jsonError("No valid items found", 400);
@@ -113,7 +142,8 @@ function createCheckoutHandler(deps) {
113
142
  metadata: {
114
143
  site,
115
144
  release_ids: JSON.stringify(resolvedReleaseIds),
116
- track_ids: JSON.stringify(resolvedTrackIds)
145
+ track_ids: JSON.stringify(resolvedTrackIds),
146
+ amounts: JSON.stringify(chargedAmounts)
117
147
  },
118
148
  success_url: successUrl,
119
149
  cancel_url: cancelUrl
@@ -152,7 +182,9 @@ function createStripeWebhookHandler(deps) {
152
182
  emailFrom,
153
183
  baseUrl,
154
184
  orderTokenSecret,
155
- verifyPath = "/orders/verify"
185
+ orderTokenExpiry = null,
186
+ verifyPath = "/orders/verify",
187
+ renderConfirmation = renderPurchaseConfirmation
156
188
  } = deps;
157
189
  if (!stripeOverride && !stripeSecret) {
158
190
  throw new Error(
@@ -185,6 +217,7 @@ function createStripeWebhookHandler(deps) {
185
217
  }
186
218
  const releaseIds = safeJsonIdList(session.metadata?.release_ids);
187
219
  const trackIds = safeJsonIdList(session.metadata?.track_ids);
220
+ const chargedAmounts = safeAmountMap(session.metadata?.amounts);
188
221
  if (releaseIds.length === 0 && trackIds.length === 0) {
189
222
  return Response.json({ received: true });
190
223
  }
@@ -201,8 +234,14 @@ function createStripeWebhookHandler(deps) {
201
234
  (c) => c !== void 0
202
235
  );
203
236
  const orderItems = [
204
- ...matchedReleases.map((r) => ({ releaseId: r.id, price: r.price })),
205
- ...matchedTracks.map(({ track }) => ({ trackId: track.id, price: track.price }))
237
+ ...matchedReleases.map((r) => ({
238
+ releaseId: r.id,
239
+ price: chargedAmounts[`r${r.id}`] ?? r.price
240
+ })),
241
+ ...matchedTracks.map(({ track }) => ({
242
+ trackId: track.id,
243
+ price: chargedAmounts[`t${track.id}`] ?? track.price
244
+ }))
206
245
  ];
207
246
  const customerEmail = session.customer_details?.email ?? "";
208
247
  let recordedOrder;
@@ -235,10 +274,11 @@ function createStripeWebhookHandler(deps) {
235
274
  const verifyToken = await signOrderToken({
236
275
  orderId: String(recordedOrder.id),
237
276
  email: customerEmail,
238
- secret: orderTokenSecret
277
+ secret: orderTokenSecret,
278
+ expiresIn: orderTokenExpiry
239
279
  });
240
280
  const verifyUrl = `${baseUrl}${verifyPath}?token=${verifyToken}`;
241
- const { subject, html } = renderPurchaseConfirmation({
281
+ const { subject, html } = await renderConfirmation({
242
282
  branding,
243
283
  verifyUrl,
244
284
  itemNames,
@@ -260,6 +300,21 @@ function createStripeWebhookHandler(deps) {
260
300
  return Response.json({ received: true });
261
301
  };
262
302
  }
303
+ function safeAmountMap(value) {
304
+ if (!value) return {};
305
+ try {
306
+ const parsed = JSON.parse(value);
307
+ if (typeof parsed !== "object" || parsed === null) return {};
308
+ const out = {};
309
+ for (const [key, raw] of Object.entries(parsed)) {
310
+ const n = Number(raw);
311
+ if (Number.isFinite(n)) out[key] = n;
312
+ }
313
+ return out;
314
+ } catch {
315
+ return {};
316
+ }
317
+ }
263
318
  function safeJsonIdList(value) {
264
319
  if (!value) return [];
265
320
  try {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/checkout.ts","../src/webhook.ts","../src/download.ts","../src/zip.ts","../src/zip-stream.ts","../src/sw-zip-fallback.ts"],"names":["session","Stripe","url"],"mappings":";;;;;;;;;;AAeO,SAAS,sBACd,IAAA,EACyC;AACzC,EAAA,MAAM;AAAA,IACJ,YAAA;AAAA,IACA,OAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAA;AAAA,IACA,WAAA,GAAc,mBAAA;AAAA,IACd,UAAA,GAAa,OAAA;AAAA,IACb,QAAA,GAAW,KAAA;AAAA,IACX;AAAA,GACF,GAAI,IAAA;AAEJ,EAAA,MAAM,SAAS,IAAI,MAAA,CAAO,cAAc,EAAE,UAAA,EAAY,MAAM,CAAA;AAE5D,EAAA,OAAO,OAAO,GAAA,KAAwC;AACpD,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAW,MAAM,IAAI,IAAA,EAAK;AAAA,IAC5B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,SAAA,CAAU,qBAAqB,GAAG,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,QAAQ,OAAA,CAAQ,KAAA;AACtB,IAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAChC,MAAA,OAAO,SAAA,CAAU,qBAAqB,GAAG,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,UAAA,GAAa,CAAA,EAAG,OAAO,CAAA,EAAG,WAAW,CAAA,iCAAA,CAAA;AAC3C,IAAA,MAAM,SAAA,GAAY,CAAA,EAAG,OAAO,CAAA,EAAG,UAAU,CAAA,CAAA;AAEzC,IAAA,MAAM,oBAAoB,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,SAAS,CAAA;AAEhE,IAAA,IAAI;AACF,MAAA,IAAI,iBAAA,EAAmB;AACrB,QAAA,MAAM,WACJ,OAAO,eAAA,KAAoB,UAAA,GACvB,MAAM,iBAAgB,GACtB,eAAA;AACN,QAAA,IAAI,CAAC,QAAA,EAAU;AACb,UAAA,OAAO,SAAA,CAAU,mCAAmC,GAAG,CAAA;AAAA,QACzD;AAMA,QAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,qBAAA,EAAsB;AAErD,QAAA,MAAMA,QAAAA,GAAU,MAAM,MAAA,CAAO,QAAA,CAAS,SAAS,MAAA,CAAO;AAAA,UACpD,IAAA,EAAM,SAAA;AAAA,UACN,UAAA,EAAY;AAAA,YACV;AAAA,cACE,UAAA,EAAY;AAAA,gBACV,QAAA;AAAA,gBACA,YAAA,EAAc,EAAE,IAAA,EAAM,QAAA,CAAS,WAAA,EAAY;AAAA,gBAC3C,aAAa,QAAA,CAAS;AAAA,eACxB;AAAA,cACA,QAAA,EAAU;AAAA;AACZ,WACF;AAAA,UACA,QAAA,EAAU;AAAA,YACR,IAAA;AAAA,YACA,gBAAA,EAAkB,MAAA;AAAA,YAClB,WAAA,EAAa,KAAK,SAAA,CAAU,QAAA,CAAS,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AAAA,YACrD,SAAA,EAAW;AAAA,WACb;AAAA,UACA,WAAA,EAAa,UAAA;AAAA,UACb,UAAA,EAAY;AAAA,SACb,CAAA;AAED,QAAA,OAAO,SAAS,IAAA,CAAK,EAAE,GAAA,EAAKA,QAAAA,CAAQ,KAAK,CAAA;AAAA,MAC3C;AAEA,MAAA,MAAM,aAAa,KAAA,CAChB,MAAA,CAAO,CAAC,CAAA,KAAM,EAAE,IAAA,KAAS,SAAA,IAAa,CAAA,CAAE,EAAA,IAAM,IAAI,CAAA,CAClD,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,EAAY,CAAA;AAC5B,MAAA,MAAM,WAAW,KAAA,CACd,MAAA,CAAO,CAAC,CAAA,KAAM,EAAE,IAAA,KAAS,OAAA,IAAW,CAAA,CAAE,EAAA,IAAM,IAAI,CAAA,CAChD,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,EAAY,CAAA;AAE5B,MAAA,IAAI,UAAA,CAAW,MAAA,KAAW,CAAA,IAAK,QAAA,CAAS,WAAW,CAAA,EAAG;AACpD,QAAA,OAAO,SAAA,CAAU,wBAAwB,GAAG,CAAA;AAAA,MAC9C;AAKA,MAAA,MAAM,WAAA,GAAc,MAAM,OAAA,CAAQ,qBAAA,EAAsB;AACxD,MAAA,MAAM,WAAA,GAAc,IAAI,GAAA,CAAI,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAC7D,MAAA,MAAM,YAAA,uBAAmB,GAAA,EAGvB;AACF,MAAA,KAAA,MAAW,WAAW,WAAA,EAAa;AACjC,QAAA,KAAA,MAAW,KAAA,IAAS,QAAQ,MAAA,EAAQ;AAClC,UAAA,YAAA,CAAa,IAAI,KAAA,CAAM,EAAA,EAAI,EAAE,KAAA,EAAO,SAAS,CAAA;AAAA,QAC/C;AAAA,MACF;AAEA,MAAA,MAAM,mBAAmB,UAAA,CACtB,GAAA,CAAI,CAAC,EAAA,KAAO,WAAA,CAAY,IAAI,EAAE,CAAC,CAAA,CAC/B,MAAA,CAAO,CAAC,CAAA,KAA8B,CAAA,KAAM,MAAS,CAAA,CACrD,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACX,UAAA,EAAY;AAAA,UACV,QAAA;AAAA,UACA,YAAA,EAAc;AAAA,YACZ,MAAM,CAAA,CAAE,IAAA;AAAA,YACR,GAAI,CAAA,CAAE,aAAA,GACF,EAAE,MAAA,EAAQ,CAAC,aAAA,CAAc,CAAA,CAAE,aAAA,EAAe,OAAO,CAAC,CAAA,KAClD;AAAC,WACP;AAAA,UACA,aAAa,CAAA,CAAE;AAAA,SACjB;AAAA,QACA,QAAA,EAAU;AAAA,OACZ,CAAE,CAAA;AAEJ,MAAA,MAAM,cAAA,GAAiB,SACpB,GAAA,CAAI,CAAC,OAAO,YAAA,CAAa,GAAA,CAAI,EAAE,CAAC,CAAA,CAChC,MAAA;AAAA,QACC,CAAC,MACC,CAAA,KAAM,KAAA;AAAA,QAET,GAAA,CAAI,CAAC,EAAE,KAAA,EAAO,SAAQ,MAAO;AAAA,QAC5B,UAAA,EAAY;AAAA,UACV,QAAA;AAAA,UACA,YAAA,EAAc;AAAA,YACZ,MAAM,CAAA,EAAG,OAAA,CAAQ,IAAI,CAAA,QAAA,EAAM,MAAM,IAAI,CAAA,CAAA;AAAA,YACrC,GAAI,OAAA,CAAQ,aAAA,GACR,EAAE,MAAA,EAAQ,CAAC,aAAA,CAAc,OAAA,CAAQ,aAAA,EAAe,OAAO,CAAC,CAAA,KACxD;AAAC,WACP;AAAA,UACA,aAAa,KAAA,CAAM;AAAA,SACrB;AAAA,QACA,QAAA,EAAU;AAAA,OACZ,CAAE,CAAA;AAEJ,MAAA,MAAM,SAAA,GAAY,CAAC,GAAG,gBAAA,EAAkB,GAAG,cAAc,CAAA;AACzD,MAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,QAAA,OAAO,SAAA,CAAU,wBAAwB,GAAG,CAAA;AAAA,MAC9C;AAEA,MAAA,MAAM,kBAAA,GAAqB,gBAAA,CAAiB,MAAA,GACxC,UAAA,CAAW,MAAA,CAAO,CAAC,EAAA,KAAO,WAAA,CAAY,GAAA,CAAI,EAAE,CAAC,CAAA,GAC7C,EAAC;AACL,MAAA,MAAM,gBAAA,GAAmB,cAAA,CAAe,MAAA,GACpC,QAAA,CAAS,MAAA,CAAO,CAAC,EAAA,KAAO,YAAA,CAAa,GAAA,CAAI,EAAE,CAAC,CAAA,GAC5C,EAAC;AAEL,MAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,QAAA,CAAS,SAAS,MAAA,CAAO;AAAA,QACpD,IAAA,EAAM,SAAA;AAAA,QACN,UAAA,EAAY,SAAA;AAAA,QACZ,QAAA,EAAU;AAAA,UACR,IAAA;AAAA,UACA,WAAA,EAAa,IAAA,CAAK,SAAA,CAAU,kBAAkB,CAAA;AAAA,UAC9C,SAAA,EAAW,IAAA,CAAK,SAAA,CAAU,gBAAgB;AAAA,SAC5C;AAAA,QACA,WAAA,EAAa,UAAA;AAAA,QACb,UAAA,EAAY;AAAA,OACb,CAAA;AAED,MAAA,OAAO,SAAS,IAAA,CAAK,EAAE,GAAA,EAAK,OAAA,CAAQ,KAAK,CAAA;AAAA,IAC3C,SAAS,GAAA,EAAK;AACZ,MAAA,OAAO,SAAA,CAAU,mCAAA,EAAqC,GAAA,EAAK,GAAG,CAAA;AAAA,IAChE;AAAA,EACF,CAAA;AACF;AAGA,SAAS,aAAA,CAAc,KAAa,OAAA,EAAyB;AAC3D,EAAA,MAAM,MAAM,GAAA,CAAI,UAAA,CAAW,MAAM,CAAA,GAC7B,MACA,CAAA,EAAG,OAAO,CAAA,EAAG,GAAA,CAAI,WAAW,GAAG,CAAA,GAAI,EAAA,GAAK,GAAG,GAAG,GAAG,CAAA,CAAA;AACrD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAA,CAAO,QAAA,GAAW,SAAA,CAAU,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAC,CAAA;AACtD,IAAA,OAAO,OAAO,QAAA,EAAS;AAAA,EACzB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,GAAA;AAAA,EACT;AACF;AAEA,SAAS,SAAA,CAAU,OAAA,EAAiB,MAAA,EAAgB,KAAA,EAA2B;AAC7E,EAAA,IAAI,KAAA,EAAO;AAIT,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,WAAA,EAAc,OAAO,CAAA,CAAA,CAAA,EAAK,KAAK,CAAA;AAAA,EAC/C;AACA,EAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,SAAQ,EAAG,EAAE,QAAQ,CAAA;AACrD;ACzKO,SAAS,2BACd,IAAA,EACyC;AACzC,EAAA,MAAM;AAAA,IACJ,MAAA,EAAQ,cAAA;AAAA,IACR,YAAA;AAAA,IACA,aAAA;AAAA,IACA,OAAA;AAAA,IACA,IAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,SAAA;AAAA,IACA,OAAA;AAAA,IACA,gBAAA;AAAA,IACA,UAAA,GAAa;AAAA,GACf,GAAI,IAAA;AAEJ,EAAA,IAAI,CAAC,cAAA,IAAkB,CAAC,YAAA,EAAc;AACpC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,MAAM,MAAA,GACJ,kBAAkB,IAAIC,MAAAA,CAAO,cAAwB,EAAE,UAAA,EAAY,MAAM,CAAA;AAE3E,EAAA,OAAO,OAAO,GAAA,KAAwC;AACpD,IAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,kBAAkB,CAAA;AAE9C,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,qBAAoB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IACtE;AAEA,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AACF,MAAA,KAAA,GAAQ,MAAA,CAAO,QAAA,CAAS,cAAA,CAAe,IAAA,EAAM,KAAK,aAAa,CAAA;AAAA,IACjE,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,qBAAoB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IACtE;AAEA,IAAA,IAAI,KAAA,CAAM,SAAS,4BAAA,EAA8B;AAC/C,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,MAAA;AAK3B,IAAA,IAAI,OAAA,CAAQ,QAAA,EAAU,IAAA,KAAS,IAAA,EAAM;AACnC,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,yBAAA,CAA0B,QAAQ,EAAE,CAAA;AACnE,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,UAAA,GAAa,cAAA,CAAe,OAAA,CAAQ,QAAA,EAAU,WAAW,CAAA;AAC/D,IAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,CAAQ,QAAA,EAAU,SAAS,CAAA;AAE3D,IAAA,IAAI,UAAA,CAAW,MAAA,KAAW,CAAA,IAAK,QAAA,CAAS,WAAW,CAAA,EAAG;AACpD,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,IACzC;AAIA,IAAA,MAAM,WAAA,GAAc,MAAM,OAAA,CAAQ,qBAAA,EAAsB;AACxD,IAAA,MAAM,WAAA,GAAc,IAAI,GAAA,CAAI,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAC7D,IAAA,MAAM,YAAA,uBAAmB,GAAA,EAGvB;AACF,IAAA,KAAA,MAAW,WAAW,WAAA,EAAa;AACjC,MAAA,KAAA,MAAW,KAAA,IAAS,QAAQ,MAAA,EAAQ;AAClC,QAAA,YAAA,CAAa,IAAI,KAAA,CAAM,EAAA,EAAI,EAAE,KAAA,EAAO,SAAS,CAAA;AAAA,MAC/C;AAAA,IACF;AAEA,IAAA,MAAM,eAAA,GAAkB,UAAA,CACrB,GAAA,CAAI,CAAC,OAAO,WAAA,CAAY,GAAA,CAAI,EAAE,CAAC,CAAA,CAC/B,MAAA,CAAO,CAAC,CAAA,KAA8B,MAAM,MAAS,CAAA;AACxD,IAAA,MAAM,aAAA,GAAgB,SACnB,GAAA,CAAI,CAAC,OAAO,YAAA,CAAa,GAAA,CAAI,EAAE,CAAC,CAAA,CAChC,MAAA;AAAA,MACC,CAAC,MACC,CAAA,KAAM;AAAA,KACV;AAEF,IAAA,MAAM,UAAA,GAAa;AAAA,MACjB,GAAG,eAAA,CAAgB,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,SAAA,EAAW,CAAA,CAAE,EAAA,EAAI,KAAA,EAAO,CAAA,CAAE,KAAA,EAAM,CAAE,CAAA;AAAA,MACnE,GAAG,aAAA,CAAc,GAAA,CAAI,CAAC,EAAE,KAAA,EAAM,MAAO,EAAE,OAAA,EAAS,KAAA,CAAM,EAAA,EAAI,KAAA,EAAO,KAAA,CAAM,OAAM,CAAE;AAAA,KACjF;AAEA,IAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,gBAAA,EAAkB,KAAA,IAAS,EAAA;AAEzD,IAAA,IAAI,aAAA;AACJ,IAAA,IAAI;AACF,MAAA,aAAA,GAAgB,MAAM,QAAQ,oBAAA,CAAqB;AAAA,QACjD,iBAAiB,OAAA,CAAQ,EAAA;AAAA,QACzB,iBACE,OAAO,OAAA,CAAQ,cAAA,KAAmB,QAAA,GAAW,QAAQ,cAAA,GAAiB,IAAA;AAAA,QACxE,KAAA,EAAO,aAAA;AAAA,QACP,WAAA,EAAa,QAAQ,YAAA,IAAgB,CAAA;AAAA,QACrC,KAAA,EAAO;AAAA,OACR,CAAA;AAAA,IACH,SAAS,GAAA,EAAK;AAKZ,MAAA,IAAI,uBAAA,CAAwB,GAAG,CAAA,EAAG;AAChC,QAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,MACzC;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AAEA,IAAA,IAAI,CAAC,aAAA,EAAe;AAIlB,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN,CAAA,qDAAA,EAAwD,QAAQ,EAAE,CAAA,6FAAA;AAAA,OACpE;AACA,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,SAAA,GAAY;AAAA,MAChB,GAAG,eAAA,CAAgB,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,MACpC,GAAG,aAAA,CAAc,GAAA,CAAI,CAAC,EAAE,KAAA,EAAO,OAAA,EAAQ,KAAM,CAAA,EAAG,OAAA,CAAQ,IAAI,CAAA,QAAA,EAAM,KAAA,CAAM,IAAI,CAAA,CAAE;AAAA,KAChF;AAEA,IAAA,KAAA,CAAM,YAAY;AAChB,MAAA,IAAI;AACF,QAAA,MAAM,WAAA,GAAc,MAAM,cAAA,CAAe;AAAA,UACvC,OAAA,EAAS,MAAA,CAAO,aAAA,CAAc,EAAE,CAAA;AAAA,UAChC,KAAA,EAAO,aAAA;AAAA,UACP,MAAA,EAAQ;AAAA,SACT,CAAA;AACD,QAAA,MAAM,YAAY,CAAA,EAAG,OAAO,CAAA,EAAG,UAAU,UAAU,WAAW,CAAA,CAAA;AAC9D,QAAA,MAAM,EAAE,OAAA,EAAS,IAAA,EAAK,GAAI,0BAAA,CAA2B;AAAA,UACnD,QAAA;AAAA,UACA,SAAA;AAAA,UACA,SAAA;AAAA,UACA,UAAA,EAAY,QAAQ,YAAA,IAAgB;AAAA,SACrC,CAAA;AACD,QAAA,MAAM,MAAM,IAAA,CAAK;AAAA,UACf,IAAA,EAAM,SAAA;AAAA,UACN,EAAA,EAAI,aAAA;AAAA,UACJ,OAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACH,SAAS,GAAA,EAAK;AACZ,QAAA,OAAA,CAAQ,KAAA;AAAA,UACN,CAAA,yDAAA,EAA4D,aAAa,CAAA,aAAA,EAAgB,OAAA,CAAQ,EAAE,CAAA,CAAA,CAAA;AAAA,UACnG;AAAA,SACF;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,EACzC,CAAA;AACF;AAGA,SAAS,eAAe,KAAA,EAA4C;AAClE,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAC;AACpB,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AACxC,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,SAAU,EAAC;AACpC,IAAA,OAAO,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,OAAO,CAAC,CAAC,CAAA,CAAE,MAAA,CAAO,CAAC,CAAA,KAAM,MAAA,CAAO,QAAA,CAAS,CAAC,CAAC,CAAA;AAAA,EACtE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AACF;;;AChMO,SAAS,sBACd,IAAA,EAC4D;AAC5D,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,EAAQ,GAAI,IAAA;AAE7B,EAAA,OAAO,OAAO,KAAkB,GAAA,KAAyC;AACvE,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,GAAA,CAAI,MAAA;AAC5B,IAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAClC,IAAA,MAAM,UAAA,GAAa,UAAA,CAAW,YAAA,CAAa,GAAA,CAAI,SAAS,CAAA;AACxD,IAAA,MAAM,MAAA,GAAS,UAAA,CAAW,YAAA,CAAa,GAAA,CAAI,QAAQ,CAAA,IAAK,KAAA;AAExD,IAAA,IAAI,CAAC,UAAA,EAAY;AACf,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,mBAAkB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IACpE;AACA,IAAA,MAAM,OAAA,GAAU,OAAO,UAAU,CAAA;AACjC,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AAC7B,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,mBAAkB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IACpE;AAEA,IAAA,MAAM,aAAA,GAAgB,MAAM,OAAA,CAAQ,gBAAA,CAAiB,KAAK,CAAA;AAC1D,IAAA,IAAI,CAAC,aAAA,EAAe;AAClB,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,yBAAwB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IAC1E;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,sBAAA,CAAuB,OAAA,EAAS,SAAS,MAAM,CAAA;AAClE,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,kBAAiB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,EAAA,GAAK,MAAM,OAAA,CAAQ,gBAAA,CAAiB,OAAO,OAAO,CAAA;AACxD,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,OAAO,QAAA,CAAS,KAAK,EAAE,KAAA,EAAO,sBAAqB,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,IACvE;AAEA,IAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,uBAAA,CAAwB,KAAK,UAAA,EAAY;AAAA,MACjE,QAAA,EAAU,qBAAA,CAAsB,IAAA,CAAK,QAAQ,CAAA;AAAA,MAC7C,WAAA,EAAa,MAAA,KAAW,KAAA,GAAQ,WAAA,GAAc;AAAA,KAC/C,CAAA;AACD,IAAA,OAAO,QAAA,CAAS,QAAA,CAAS,GAAA,EAAK,GAAG,CAAA;AAAA,EACnC,CAAA;AACF;AAGA,eAAe,sBAAA,CACb,OAAA,EACA,OAAA,EACA,MAAA,EAC2B;AAC3B,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,qBAAA,EAAsB;AACrD,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,MAAM,KAAA,GAAQ,QAAQ,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,OAAO,CAAA;AACzD,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,IAAA,GAAO,MAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,MAAM,CAAA;AACxD,IAAA,OAAO,IAAA,IAAQ,IAAA;AAAA,EACjB;AACA,EAAA,OAAO,IAAA;AACT;AAGO,SAAS,sBAAsB,IAAA,EAAsB;AAC1D,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,WAAA,CAAY,GAAG,CAAA;AAChC,EAAA,IAAI,GAAA,IAAO,CAAA,EAAG,OAAO,IAAA,CAAK,IAAA,EAAK;AAC/B,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,CAAE,IAAA,EAAM,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,MAAM,CAAA,CAAA;AAC9D;;;AC/BA,SAAS,kBAAA,CAAmB,QAA4B,IAAA,EAAsB;AAC5E,EAAA,MAAM,OAAA,GAAU,QAAQ,IAAA,EAAK;AAC7B,EAAA,OAAO,OAAA,GAAU,CAAA,EAAG,OAAO,CAAA,GAAA,EAAM,IAAI,CAAA,CAAA,GAAK,IAAA;AAC5C;AASA,eAAsB,iBAAiB,IAAA,EAAiD;AACtF,EAAA,MAAM,EAAE,OAAA,EAAS,KAAA,EAAM,GAAI,IAAA;AAC3B,EAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,KAAA;AAC9B,EAAA,MAAM,gBAAA,GAAmB,MAAA,KAAW,KAAA,GAAQ,WAAA,GAAc,YAAA;AAC1D,EAAA,MAAM,eAAe,IAAA,CAAK,SAAA,IAAa,OAAO,IAAA,GAAO,MAAA,CAAO,KAAK,SAAS,CAAA;AAC1E,EAAA,MAAM,aAAA,GAAgB,KAAK,aAAA,IAAiB,IAAA;AAE5C,EAAA,MAAM,aAAA,GAAgB,MAAM,OAAA,CAAQ,gBAAA,CAAiB,KAAK,CAAA;AAC1D,EAAA,IAAI,CAAC,aAAA,EAAe;AAClB,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAO,uBAAA,EAAwB;AAAA,EAClE;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAM,OAAA,CAAQ,YAAA,CAAa,cAAc,OAAO,CAAA;AAC9D,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAO,uBAAA,EAAwB;AAAA,EAClE;AAEA,EAAA,OAAO,aAAA,CAAc;AAAA,IACnB,KAAA;AAAA,IACA,SAAA,EAAW,YAAA;AAAA,IACX,aAAA;AAAA,IACA,MAAA;AAAA,IACA,gBAAA;AAAA,IACA,eAAe,IAAA,CAAK;AAAA,GACrB,CAAA;AACH;AAcO,SAAS,yBACd,IAAA,EAC4D;AAC5D,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,EAAQ,GAAI,IAAA;AAE7B,EAAA,OAAO,OAAO,KAAkB,GAAA,KAAyC;AACvE,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,GAAA,CAAI,MAAA;AAC5B,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAC3B,IAAA,MAAM,UAAA,GAAa,MAAM,gBAAA,CAAiB;AAAA,MACxC,OAAA;AAAA,MACA,KAAA;AAAA,MACA,SAAA,EAAW,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,WAAW,CAAA;AAAA,MAC3C,aAAA,EAAe,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,UAAU,CAAA;AAAA,MAC9C,MAAA,EAAQ,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,QAAQ,CAAA,IAAK,KAAA;AAAA,MAC1C,eAAe,IAAA,CAAK;AAAA,KACrB,CAAA;AACD,IAAA,IAAI,CAAC,WAAW,EAAA,EAAI;AAClB,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,KAAA,EAAO,UAAA,CAAW,KAAA,EAAM,EAAG,EAAE,MAAA,EAAQ,UAAA,CAAW,MAAA,EAAQ,CAAA;AAAA,IACjF;AAEA,IAAA,MAAM,QAAA,GAAwB;AAAA,MAC5B,SAAS,UAAA,CAAW,OAAA;AAAA,MACpB,KAAA,EAAO,MAAM,eAAA,CAAgB,OAAA,EAAS,WAAW,KAAK;AAAA,KACxD;AACA,IAAA,OAAO,QAAA,CAAS,KAAK,QAAQ,CAAA;AAAA,EAC/B,CAAA;AACF;AAeA,SAAS,cAAc,IAAA,EAA+B;AACpD,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAW,eAAe,MAAA,EAAQ,gBAAA,EAAkB,eAAc,GAAI,IAAA;AACrF,EAAA,MAAM,GAAA,GAAM,OAAO,WAAA,EAAY;AAE/B,EAAA,IAAI,aAAA,EAAe;AACjB,IAAA,OAAO,iBAAiB,KAAA,EAAO,aAAA,EAAe,MAAA,EAAQ,gBAAA,EAAkB,KAAK,aAAa,CAAA;AAAA,EAC5F;AACA,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,MAAM,EAAA,GAAK,OAAO,SAAS,CAAA;AAC3B,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,EAAE,CAAA,EAAG;AACxB,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAO,mBAAA,EAAoB;AAAA,IAC9D;AACA,IAAA,OAAO,qBAAqB,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,gBAAA,EAAkB,KAAK,aAAa,CAAA;AAAA,EACrF;AACA,EAAA,OAAO,iBAAA,CAAkB,KAAA,EAAO,MAAA,EAAQ,gBAAA,EAAkB,KAAK,aAAa,CAAA;AAC9E;AAEA,SAAS,iBACP,KAAA,EACA,aAAA,EACA,MAAA,EACA,gBAAA,EACA,KACA,aAAA,EACY;AACZ,EAAA,MAAM,iBAAA,GAAoB,cACvB,KAAA,CAAM,GAAG,EACT,GAAA,CAAI,CAAC,MAAM,MAAA,CAAO,CAAA,CAAE,MAAM,CAAC,EAC3B,MAAA,CAAO,CAAC,MAAM,MAAA,CAAO,QAAA,CAAS,CAAC,CAAC,CAAA;AACnC,EAAA,MAAM,SAAA,GAAY,eAAe,KAAK,CAAA;AACtC,EAAA,MAAM,QAAA,GAAW,kBAAkB,KAAA,CAAM,CAAC,OAAO,SAAA,CAAU,IAAA,CAAK,EAAE,CAAC,CAAA;AACnE,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAO,oBAAA,EAAqB;AAAA,EAC/D;AAEA,EAAA,MAAM,QAAmB,EAAC;AAC1B,EAAA,KAAA,MAAW,MAAM,iBAAA,EAAmB;AAClC,IAAA,MAAM,GAAA,GAAM,SAAA,CAAU,MAAA,CAAO,EAAE,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAA,EAAK;AACV,IAAA,MAAM,IAAA,GAAO,IAAI,KAAA,CAAM,KAAA,CAAM,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,KAAW,MAAM,CAAA;AAC5D,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,KAAA,CAAM,IAAA,CAAK;AAAA,MACT,UAAU,YAAA,CAAa,GAAA,CAAI,SAAS,IAAA,IAAQ,IAAA,EAAM,KAAK,QAAQ,CAAA;AAAA,MAC/D,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,WAAA,EAAa;AAAA,KACd,CAAA;AAAA,EACH;AACA,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,GAAA;AAAA,MACR,KAAA,EAAO;AAAA,KACT;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,IAAA;AAAA,IACJ,OAAA,EAAS,kBAAA,CAAmB,aAAA,EAAe,CAAA,QAAA,EAAW,GAAG,CAAA,KAAA,CAAO,CAAA;AAAA,IAChE;AAAA,GACF;AACF;AAEA,SAAS,qBACP,KAAA,EACA,SAAA,EACA,MAAA,EACA,gBAAA,EACA,KACA,aAAA,EACY;AACZ,EAAA,MAAM,eAAA,GAAkB,MAAM,KAAA,CAAM,IAAA,CAAK,CAAC,IAAA,KAAS,IAAA,CAAK,cAAc,SAAS,CAAA;AAC/E,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAO,sBAAA,EAAuB;AAAA,EACjE;AAEA,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,KAAA,CAAM,IAAA,CAAK,CAAC,SAAS,IAAA,CAAK,OAAA,EAAS,EAAA,KAAO,SAAS,CAAA,EAAG,OAAA;AAC5E,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,OAAO,mBAAA,EAAoB;AAAA,EAC9D;AAEA,EAAA,MAAM,KAAA,GAAmB,OAAA,CAAQ,MAAA,CAC9B,GAAA,CAAI,CAAC,KAAA,KAAU;AACd,IAAA,MAAM,IAAA,GAAO,MAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,MAAM,CAAA;AACxD,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,YAAA,CAAa,IAAA,EAAM,IAAA,CAAK,QAAQ,CAAA;AAAA,MAC1C,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,WAAA,EAAa;AAAA,KACf;AAAA,EACF,CAAC,CAAA,CACA,MAAA,CAAO,CAAC,CAAA,KAAoB,MAAM,IAAI,CAAA;AAEzC,EAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,OAAA,CAAQ,aAAA,EAAe;AAC7C,IAAA,KAAA,CAAM,IAAA,CAAK,GAAG,eAAA,CAAgB,OAAA,CAAQ,MAAM,OAAA,CAAQ,aAAA,EAAe,EAAA,EAAI,KAAK,CAAC,CAAA;AAAA,EAC/E;AAEA,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,GAAA;AAAA,MACR,KAAA,EAAO;AAAA,KACT;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,IAAA;AAAA,IACJ,OAAA,EAAS,mBAAmB,aAAA,EAAe,CAAA,EAAG,QAAQ,IAAI,CAAA,EAAA,EAAK,GAAG,CAAA,KAAA,CAAO,CAAA;AAAA,IACzE;AAAA,GACF;AACF;AAEA,SAAS,iBAAA,CACP,KAAA,EACA,MAAA,EACA,gBAAA,EACA,KACA,aAAA,EACY;AACZ,EAAA,MAAM,eAA0B,EAAC;AACjC,EAAA,MAAM,gBAA2B,EAAC;AAElC,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,KAAA,EAAO;AAC9B,IAAA,IAAI,KAAK,OAAA,EAAS;AAChB,MAAA,MAAM,UAAU,IAAA,CAAK,OAAA;AACrB,MAAA,MAAM,UAAqB,EAAC;AAC5B,MAAA,KAAA,MAAW,KAAA,IAAS,QAAQ,MAAA,EAAQ;AAClC,QAAA,MAAM,IAAA,GAAO,MAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,MAAM,CAAA;AACxD,QAAA,IAAI,CAAC,IAAA,EAAM;AACX,QAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,UACX,QAAA,EAAU,YAAA,CAAa,OAAA,CAAQ,IAAA,EAAM,KAAK,QAAQ,CAAA;AAAA,UAClD,YAAY,IAAA,CAAK,UAAA;AAAA,UACjB,WAAA,EAAa;AAAA,SACd,CAAA;AAAA,MACH;AACA,MAAA,IAAI,OAAA,CAAQ,MAAA,GAAS,CAAA,IAAK,OAAA,CAAQ,aAAA,EAAe;AAC/C,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN,GAAG,eAAA;AAAA,YACD,OAAA,CAAQ,IAAA;AAAA,YACR,OAAA,CAAQ,aAAA;AAAA,YACR,CAAA,EAAG,eAAA,CAAgB,OAAA,CAAQ,IAAI,CAAC,CAAA,CAAA,CAAA;AAAA,YAChC;AAAA;AACF,SACF;AAAA,MACF;AACA,MAAA,YAAA,CAAa,IAAA,CAAK,GAAG,OAAO,CAAA;AAAA,IAC9B,CAAA,MAAA,IAAW,KAAK,KAAA,EAAO;AACrB,MAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AACnB,MAAA,MAAM,OAAA,GAAU,MAAM,OAAA,IAAW,IAAA;AACjC,MAAA,MAAM,IAAA,GAAO,MAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,MAAM,CAAA;AACxD,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,aAAA,CAAc,IAAA,CAAK;AAAA,QACjB,UAAU,YAAA,CAAa,OAAA,EAAS,IAAA,IAAQ,IAAA,EAAM,KAAK,QAAQ,CAAA;AAAA,QAC3D,YAAY,IAAA,CAAK,UAAA;AAAA,QACjB,WAAA,EAAa;AAAA,OACd,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,GAAG,YAAA,EAAc,GAAG,aAAa,CAAA;AAChD,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,GAAA;AAAA,MACR,KAAA,EAAO;AAAA,KACT;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,IAAA;AAAA,IACJ,OAAA,EAAS,mBAAmB,aAAA,EAAe,CAAA,MAAA,EAAS,MAAM,EAAE,CAAA,EAAA,EAAK,GAAG,CAAA,KAAA,CAAO,CAAA;AAAA,IAC3E;AAAA,GACF;AACF;AAQA,SAAS,eAAe,KAAA,EAAuB;AAC7C,EAAA,MAAM,IAAA,uBAAW,GAAA,EAA0B;AAC3C,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,KAAA,EAAO;AAC9B,IAAA,IAAI,KAAK,OAAA,EAAS;AAChB,MAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ;AACnC,QAAA,IAAA,CAAK,GAAA,CAAI,EAAE,EAAA,EAAI,EAAE,OAAO,CAAA,EAAG,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,CAAA;AAAA,MACpD;AAAA,IACF;AACA,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,EAAA,EAAI;AAAA,QACtB,OAAO,IAAA,CAAK,KAAA;AAAA,QACZ,OAAA,EAAU,IAAA,CAAK,KAAA,CAAM,OAAA,IAAoD;AAAA,OAC1E,CAAA;AAAA,IACH;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAC,EAAA,KAAe,IAAA,CAAK,IAAI,EAAE,CAAA;AAAA,IACjC,QAAQ,CAAC,EAAA,KAAe,IAAA,CAAK,GAAA,CAAI,EAAE,CAAA,IAAK;AAAA,GAC1C;AACF;AAGO,SAAS,gBAAgB,IAAA,EAAsB;AACpD,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,GAAG,EAAE,IAAA,EAAK;AAC3C;AAOO,SAAS,cAAc,QAAA,EAA2B;AACvD,EAAA,OAAO,eAAA,CAAgB,KAAK,QAAQ,CAAA;AACtC;AAQO,SAAS,YAAA,CAAa,aAA4B,QAAA,EAA0B;AACjF,EAAA,MAAM,KAAA,GAAQ,sBAAsB,QAAQ,CAAA;AAC5C,EAAA,MAAM,WAAqB,EAAC;AAC5B,EAAA,IAAI,WAAA,EAAa,QAAA,CAAS,IAAA,CAAK,eAAA,CAAgB,WAAW,CAAC,CAAA;AAC3D,EAAA,IAAI,aAAA,CAAc,KAAK,CAAA,EAAG,QAAA,CAAS,KAAK,UAAU,CAAA;AAClD,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,QAAA,CAAS,KAAK,GAAG,CAAA;AAC1B;AAGO,SAAS,iBAAiB,WAAA,EAA6B;AAC5D,EAAA,OAAO,CAAA,EAAG,eAAA,CAAgB,WAAW,CAAC,CAAA,gBAAA,CAAA;AACxC;AAQO,SAAS,eAAA,CACd,WAAA,EACA,aAAA,EACA,MAAA,EACA,YAAA,EACW;AACX,EAAA,MAAM,IAAA,GAAO,iBAAiB,WAAW,CAAA;AACzC,EAAA,MAAM,OAAA,GAAqB;AAAA,IACzB,EAAE,QAAA,EAAU,CAAA,EAAG,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,UAAA,EAAY,aAAA,EAAe,WAAA,EAAa,YAAA;AAAa,GACvF;AACA,EAAA,IAAI,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,QAAA,CAAS,UAAU,CAAC,CAAA,EAAG;AACxE,IAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,MACX,QAAA,EAAU,CAAA,EAAG,MAAM,CAAA,SAAA,EAAY,IAAI,CAAA,CAAA;AAAA,MACnC,UAAA,EAAY,aAAA;AAAA,MACZ,WAAA,EAAa;AAAA,KACd,CAAA;AAAA,EACH;AACA,EAAA,OAAO,OAAA;AACT;AAGA,eAAe,eAAA,CACb,SACA,KAAA,EAC+B;AAC/B,EAAA,OAAO,OAAA,CAAQ,GAAA;AAAA,IACb,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA,MAAO;AAAA,MACtB,UAAU,CAAA,CAAE,QAAA;AAAA,MACZ,GAAA,EAAK,MAAM,OAAA,CAAQ,uBAAA,CAAwB,EAAE,UAAA,EAAY;AAAA,QACvD,QAAA,EAAU,EAAE,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,MAAS,CAAA,CAAE,QAAA;AAAA,QAC3C,aAAa,CAAA,CAAE;AAAA,OAChB;AAAA,KACH,CAAE;AAAA,GACJ;AACF;AC/XO,SAAS,+BACd,IAAA,EAC4D;AAC5D,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,EAAQ,GAAI,IAAA;AAE7B,EAAA,OAAO,OAAO,KAAkB,GAAA,KAAyC;AACvE,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,GAAA,CAAI,MAAA;AAC5B,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAE3B,IAAA,MAAM,UAAA,GAAa,MAAM,gBAAA,CAAiB;AAAA,MACxC,OAAA;AAAA,MACA,KAAA;AAAA,MACA,SAAA,EAAW,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,WAAW,CAAA;AAAA,MAC3C,aAAA,EAAe,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,UAAU,CAAA;AAAA,MAC9C,MAAA,EAAQ,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,QAAQ,CAAA,IAAK,KAAA;AAAA,MAC1C,eAAe,IAAA,CAAK;AAAA,KACrB,CAAA;AACD,IAAA,IAAI,CAAC,WAAW,EAAA,EAAI;AAClB,MAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,KAAA,EAAO,UAAA,CAAW,KAAA,EAAM,EAAG,EAAE,MAAA,EAAQ,UAAA,CAAW,MAAA,EAAQ,CAAA;AAAA,IACjF;AAKA,IAAA,MAAM,UAAU,QAAA,CAAS,KAAA,EAAO,EAAE,KAAA,EAAO,MAAM,CAAA;AAE/C,IAAA,OAAA,CAAQ,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAQ;AAC3B,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,GAAG,CAAA;AAAA,IACnD,CAAC,CAAA;AAGD,IAAA,GAAA,CAAI,OAAO,gBAAA,CAAiB,OAAA,EAAS,MAAM,OAAA,CAAQ,OAAO,CAAA;AAU1D,IAAA,CAAC,YAAY;AACX,MAAA,KAAA,MAAW,IAAA,IAAQ,WAAW,KAAA,EAAO;AACnC,QAAA,IAAI,GAAA,CAAI,OAAO,OAAA,EAAS;AACxB,QAAA,MAAM,QAAA,GAAW,KAAK,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,MAAS,IAAA,CAAK,QAAA;AACxD,QAAA,IAAI;AACF,UAAA,MAAMC,IAAAA,GAAM,MAAM,OAAA,CAAQ,uBAAA,CAAwB,KAAK,UAAA,EAAY;AAAA,YACjE,QAAA,EAAU,QAAA;AAAA,YACV,aAAa,IAAA,CAAK;AAAA,WACnB,CAAA;AACD,UAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAMA,IAAAA,EAAK,EAAE,MAAA,EAAQ,GAAA,CAAI,QAAQ,CAAA;AACnD,UAAA,IAAI,CAAC,GAAA,CAAI,EAAA,IAAM,CAAC,GAAA,CAAI,IAAA,EAAM,MAAM,IAAI,KAAA,CAAM,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAG9D,UAAA,MAAM,OAAO,QAAA,CAAS,OAAA;AAAA,YACpB,GAAA,CAAI;AAAA,WACN;AAKA,UAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,UAAA,MAAM,OAAA,GAAU,IAAI,SAAA,CAAU;AAAA,YAC5B,SAAA,CAAU,KAAA,EAAe,IAAA,EAAM,EAAA,EAAI;AACjC,cAAA,aAAA,IAAiB,KAAA,CAAM,MAAA;AACvB,cAAA,EAAA,CAAG,MAAM,KAAK,CAAA;AAAA,YAChB;AAAA,WACD,CAAA;AAGD,UAAA,IAAA,CAAK,GAAG,OAAA,EAAS,CAAC,QAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAC,CAAA;AAC9C,UAAA,IAAA,CAAK,KAAK,OAAO,CAAA;AACjB,UAAA,OAAA,CAAQ,OAAO,OAAA,EAAS,EAAE,IAAA,EAAM,IAAA,CAAK,UAAU,CAAA;AAC/C,UAAA,IAAI;AAIF,YAAA,MAAM,SAAS,OAAO,CAAA;AAAA,UACxB,SAAS,GAAA,EAAK;AACZ,YAAA,IAAI,GAAA,CAAI,OAAO,OAAA,EAAS;AACxB,YAAA,MAAM,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC9D,YAAA,OAAA,CAAQ,KAAA;AAAA,cACN,CAAA,mCAAA,EAAsC,KAAK,QAAQ,CAAA,aAAA,EAClC,KAAK,UAAU,CAAA,gBAAA,EAAmB,aAAa,CAAA,GAAA,EAAM,MAAM,CAAA;AAAA,aAC9E;AACA,YAAA,IAAI;AACF,cAAA,OAAA,CAAQ,MAAA;AAAA,gBACN,cAAc,IAAA,CAAK,QAAQ,CAAA,4BAAA,EAA+B,aAAa,WAAW,MAAM,CAAA;AAAA;AAAA,CAAA;AAAA,gBAExF,EAAE,IAAA,EAAM,CAAA,QAAA,EAAW,QAAQ,CAAA,IAAA,CAAA;AAAO,eACpC;AAAA,YACF,SAAS,SAAA,EAAW;AAClB,cAAA,OAAA,CAAQ,IAAA;AAAA,gBACN,CAAA,sDAAA,EAAyD,KAAK,QAAQ,CAAA,CAAA,CAAA;AAAA,gBACtE;AAAA,eACF;AAAA,YACF;AAAA,UACF;AAAA,QACF,SAAS,GAAA,EAAK;AACZ,UAAA,IAAI,GAAA,CAAI,OAAO,OAAA,EAAS;AACxB,UAAA,MAAM,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC9D,UAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,8BAAA,EAAiC,IAAA,CAAK,QAAQ,KAAK,MAAM,CAAA;AACtE,UAAA,OAAA,CAAQ,MAAA;AAAA,YACN,CAAA,oBAAA,EAAuB,IAAA,CAAK,QAAQ,CAAA,gBAAA,EAAmB,MAAM,CAAA;AAAA;AAAA,CAAA;AAAA,YAE7D,EAAE,IAAA,EAAM,CAAA,QAAA,EAAW,QAAQ,CAAA,IAAA,CAAA;AAAO,WACpC;AAAA,QACF;AAAA,MACF;AACA,MAAA,MAAM,QAAQ,QAAA,EAAS;AAAA,IACzB,CAAA,GAAG,CAAE,KAAA,CAAM,CAAC,GAAA,KAAQ;AAClB,MAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,GAAG,CAAA;AACjD,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB,CAAC,CAAA;AAED,IAAA,OAAO,IAAI,QAAA,CAAS,QAAA,CAAS,KAAA,CAAM,OAAO,CAAA,EAAiC;AAAA,MACzE,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,iBAAA;AAAA,QAChB,uBAAuB,CAAA,sBAAA,EAAyB,UAAA,CAAW,QAAQ,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,CAAA,CAAA;AAAA,QACpF,eAAA,EAAiB;AAAA;AACnB,KACD,CAAA;AAAA,EACH,CAAA;AACF;;;AC3JA,IAAM,UAAA,GAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;AA0CZ,SAAS,0BAAA,GAA6C;AAC3D,EAAA,OAAO,MAAM;AAGX,IAAA,OAAA,CAAQ,KAAK,qEAAgE,CAAA;AAC7E,IAAA,OAAO,IAAI,SAAS,UAAA,EAAY;AAAA,MAC9B,MAAA,EAAQ,GAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,0BAAA;AAAA,QAChB,eAAA,EAAiB;AAAA;AACnB,KACD,CAAA;AAAA,EACH,CAAA;AACF","file":"index.js","sourcesContent":["import type { NextRequest } from \"next/server\";\nimport Stripe from \"stripe\";\nimport type { ReleaseWithTracks, TrackWithFiles } from \"@gigamusic/db\";\nimport type { CheckoutCartItem, CheckoutDeps } from \"./types.js\";\n\n/**\n * Build a POST handler that translates `{ items }` from the cart into a Stripe\n * Checkout Session and replies with `{ url }`. Release/track metadata is\n * stashed on the session so the webhook handler can later record the order\n * without re-deriving cart contents.\n *\n * Stripe is hard-wired (per project policy). The Stripe SDK instance is\n * created once per factory call and captured in the closure — no module\n * globals, no `process.env`.\n */\nexport function createCheckoutHandler(\n deps: CheckoutDeps,\n): (req: NextRequest) => Promise<Response> {\n const {\n stripeSecret,\n queries,\n site,\n baseUrl,\n successPath = \"/checkout/success\",\n cancelPath = \"/cart\",\n currency = \"usd\",\n catalogPurchase,\n } = deps;\n\n const stripe = new Stripe(stripeSecret, { typescript: true });\n\n return async (req: NextRequest): Promise<Response> => {\n let payload: { items?: CheckoutCartItem[] };\n try {\n payload = (await req.json()) as { items?: CheckoutCartItem[] };\n } catch {\n return jsonError(\"Invalid JSON body\", 400);\n }\n\n const items = payload.items;\n if (!items || items.length === 0) {\n return jsonError(\"No items provided\", 400);\n }\n\n const successUrl = `${baseUrl}${successPath}?session_id={CHECKOUT_SESSION_ID}`;\n const cancelUrl = `${baseUrl}${cancelPath}`;\n\n const isCatalogPurchase = items.some((i) => i.kind === \"catalog\");\n\n try {\n if (isCatalogPurchase) {\n const resolved =\n typeof catalogPurchase === \"function\"\n ? await catalogPurchase()\n : catalogPurchase;\n if (!resolved) {\n return jsonError(\"Catalog purchase not configured\", 400);\n }\n // Snapshot the current published-release ids so the webhook records\n // exactly what the customer bought, even if a release is unpublished\n // between session-create and webhook-delivery. The consumer-supplied\n // `totalCents` is what Stripe charges — pricing math lives in the\n // consumer (it owns whatever discount/rounding/tax rules apply).\n const releases = await queries.listPublishedReleases();\n\n const session = await stripe.checkout.sessions.create({\n mode: \"payment\",\n line_items: [\n {\n price_data: {\n currency,\n product_data: { name: resolved.productName },\n unit_amount: resolved.totalCents,\n },\n quantity: 1,\n },\n ],\n metadata: {\n site,\n catalog_purchase: \"true\",\n release_ids: JSON.stringify(releases.map((r) => r.id)),\n track_ids: \"[]\",\n },\n success_url: successUrl,\n cancel_url: cancelUrl,\n });\n\n return Response.json({ url: session.url });\n }\n\n const releaseIds = items\n .filter((i) => i.kind === \"release\" && i.id != null)\n .map((i) => i.id as number);\n const trackIds = items\n .filter((i) => i.kind === \"track\" && i.id != null)\n .map((i) => i.id as number);\n\n if (releaseIds.length === 0 && trackIds.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n // Single `listPublishedReleases` call sources both release and track\n // line items. Tracks live inside their release, so one fetch keeps the\n // ID/price/cover lookups consistent without a second round-trip.\n const allReleases = await queries.listPublishedReleases();\n const releaseById = new Map(allReleases.map((r) => [r.id, r]));\n const trackContext = new Map<\n number,\n { track: TrackWithFiles; release: ReleaseWithTracks }\n >();\n for (const release of allReleases) {\n for (const track of release.tracks) {\n trackContext.set(track.id, { track, release });\n }\n }\n\n const releaseLineItems = releaseIds\n .map((id) => releaseById.get(id))\n .filter((r): r is ReleaseWithTracks => r !== undefined)\n .map((r) => ({\n price_data: {\n currency,\n product_data: {\n name: r.name,\n ...(r.coverImageUrl\n ? { images: [toAbsoluteUrl(r.coverImageUrl, baseUrl)] }\n : {}),\n },\n unit_amount: r.price,\n },\n quantity: 1 as const,\n }));\n\n const trackLineItems = trackIds\n .map((id) => trackContext.get(id))\n .filter(\n (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>\n c !== undefined,\n )\n .map(({ track, release }) => ({\n price_data: {\n currency,\n product_data: {\n name: `${release.name} — ${track.name}`,\n ...(release.coverImageUrl\n ? { images: [toAbsoluteUrl(release.coverImageUrl, baseUrl)] }\n : {}),\n },\n unit_amount: track.price,\n },\n quantity: 1 as const,\n }));\n\n const lineItems = [...releaseLineItems, ...trackLineItems];\n if (lineItems.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n const resolvedReleaseIds = releaseLineItems.length\n ? releaseIds.filter((id) => releaseById.has(id))\n : [];\n const resolvedTrackIds = trackLineItems.length\n ? trackIds.filter((id) => trackContext.has(id))\n : [];\n\n const session = await stripe.checkout.sessions.create({\n mode: \"payment\",\n line_items: lineItems,\n metadata: {\n site,\n release_ids: JSON.stringify(resolvedReleaseIds),\n track_ids: JSON.stringify(resolvedTrackIds),\n },\n success_url: successUrl,\n cancel_url: cancelUrl,\n });\n\n return Response.json({ url: session.url });\n } catch (err) {\n return jsonError(\"Failed to create checkout session\", 500, err);\n }\n };\n}\n\n/** Resolve a stored relative URL against `baseUrl` and percent-encode the path. Bare http(s) URLs pass through. */\nfunction toAbsoluteUrl(url: string, baseUrl: string): string {\n const raw = url.startsWith(\"http\")\n ? url\n : `${baseUrl}${url.startsWith(\"/\") ? \"\" : \"/\"}${url}`;\n try {\n const parsed = new URL(raw);\n parsed.pathname = encodeURI(decodeURI(parsed.pathname));\n return parsed.toString();\n } catch {\n return raw;\n }\n}\n\nfunction jsonError(message: string, status: number, cause?: unknown): Response {\n if (cause) {\n // Logged via console.error. Consumers can swap this for a structured\n // logger in a future iteration; the package contract doesn't expose a\n // logger slot yet.\n console.error(`[checkout] ${message}:`, cause);\n }\n return Response.json({ error: message }, { status });\n}\n","import { after, type NextRequest } from \"next/server\";\nimport Stripe from \"stripe\";\nimport { signOrderToken } from \"@gigamusic/core\";\nimport { isUniqueConstraintError, type ReleaseWithTracks, type TrackWithFiles } from \"@gigamusic/db\";\nimport { renderPurchaseConfirmation } from \"@gigamusic/email\";\nimport type { WebhookDeps } from \"./types.js\";\n\n/**\n * Build the POST handler for Stripe's webhook endpoint. Verifies the\n * signature, records the order on `checkout.session.completed`, then defers\n * the magic-link email via `after()` so Stripe sees a fast 200.\n *\n * Idempotency is enforced two ways: a pre-flight `getOrderByStripeSessionId`\n * lookup, plus a unique-constraint catch around `recordCompletedOrder` for\n * the case where two webhook deliveries race past the lookup. Both paths\n * short-circuit with `{ received: true }` so Stripe doesn't retry.\n *\n * ### Published-only resolution (gotcha)\n *\n * The handler resolves the release/track IDs from `session.metadata` against\n * `queries.listPublishedReleases()`. Anything not in the published catalog at\n * the moment the webhook fires is silently dropped from the persisted order's\n * line items — the customer is still charged, but the order row doesn't\n * include the unpublished item.\n *\n * In the canonical flow this can't happen: `createCheckoutHandler` filters on\n * the same `listPublishedReleases()` before it ever creates the Stripe\n * Session. The only ways an unpublished ID can reach this handler are:\n * - an admin unpublishes between session-create and webhook-delivery\n * (a several-second race window),\n * - a consumer bypasses `createCheckoutHandler` and creates Stripe sessions\n * directly with their own metadata.\n *\n * If either of those is a realistic concern, swap the resolution call here\n * to `listAllReleases()` (or wrap with a \"warn + fallback\" branch) — but be\n * aware that doing so will surface unpublished items in admin order views.\n */\nexport function createStripeWebhookHandler(\n deps: WebhookDeps,\n): (req: NextRequest) => Promise<Response> {\n const {\n stripe: stripeOverride,\n stripeSecret,\n webhookSecret,\n queries,\n site,\n email,\n branding,\n emailFrom,\n baseUrl,\n orderTokenSecret,\n verifyPath = \"/orders/verify\",\n } = deps;\n\n if (!stripeOverride && !stripeSecret) {\n throw new Error(\n \"createStripeWebhookHandler: must supply either `stripe` or `stripeSecret`\",\n );\n }\n const stripe =\n stripeOverride ?? new Stripe(stripeSecret as string, { typescript: true });\n\n return async (req: NextRequest): Promise<Response> => {\n const body = await req.text();\n const sig = req.headers.get(\"stripe-signature\");\n\n if (!sig) {\n return Response.json({ error: \"Missing signature\" }, { status: 400 });\n }\n\n let event: Stripe.Event;\n try {\n event = stripe.webhooks.constructEvent(body, sig, webhookSecret);\n } catch {\n return Response.json({ error: \"Invalid signature\" }, { status: 400 });\n }\n\n if (event.type !== \"checkout.session.completed\") {\n return Response.json({ received: true });\n }\n\n const session = event.data.object as Stripe.Checkout.Session;\n\n // One Stripe account fans every event out to every registered endpoint.\n // Ignore sessions that belong to a different site so we don't record a\n // foreign purchase (with zero matched line items) under this site.\n if (session.metadata?.site !== site) {\n return Response.json({ received: true });\n }\n\n const existing = await queries.getOrderByStripeSessionId(session.id);\n if (existing) {\n return Response.json({ received: true });\n }\n\n const releaseIds = safeJsonIdList(session.metadata?.release_ids);\n const trackIds = safeJsonIdList(session.metadata?.track_ids);\n\n if (releaseIds.length === 0 && trackIds.length === 0) {\n return Response.json({ received: true });\n }\n\n // Snapshot the published catalog once so we can resolve both release and\n // track prices + names off a single in-memory lookup.\n const allReleases = await queries.listPublishedReleases();\n const releaseById = new Map(allReleases.map((r) => [r.id, r]));\n const trackContext = new Map<\n number,\n { track: TrackWithFiles; release: ReleaseWithTracks }\n >();\n for (const release of allReleases) {\n for (const track of release.tracks) {\n trackContext.set(track.id, { track, release });\n }\n }\n\n const matchedReleases = releaseIds\n .map((id) => releaseById.get(id))\n .filter((r): r is ReleaseWithTracks => r !== undefined);\n const matchedTracks = trackIds\n .map((id) => trackContext.get(id))\n .filter(\n (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>\n c !== undefined,\n );\n\n const orderItems = [\n ...matchedReleases.map((r) => ({ releaseId: r.id, price: r.price })),\n ...matchedTracks.map(({ track }) => ({ trackId: track.id, price: track.price })),\n ];\n\n const customerEmail = session.customer_details?.email ?? \"\";\n\n let recordedOrder;\n try {\n recordedOrder = await queries.recordCompletedOrder({\n stripeSessionId: session.id,\n stripePaymentId:\n typeof session.payment_intent === \"string\" ? session.payment_intent : null,\n email: customerEmail,\n amountTotal: session.amount_total ?? 0,\n items: orderItems,\n });\n } catch (err) {\n // Two deliveries can both pass the pre-flight lookup and race here. The\n // unique constraint on `stripeSessionId` guarantees only one row lands;\n // the loser surfaces as a 23505 unique-violation. Treat that as the same\n // idempotent short-circuit — no email, no 500, no Stripe retry.\n if (isUniqueConstraintError(err)) {\n return Response.json({ received: true });\n }\n throw err;\n }\n\n if (!customerEmail) {\n // Stripe almost always supplies an email but doesn't guarantee it. The\n // order is safely recorded; manual intervention required to deliver the\n // magic link.\n console.warn(\n `[stripe-webhook] checkout.session.completed (session=${session.id}) has no customer_details.email — order recorded but customer cannot retrieve downloads.`,\n );\n return Response.json({ received: true });\n }\n\n const itemNames = [\n ...matchedReleases.map((r) => r.name),\n ...matchedTracks.map(({ track, release }) => `${release.name} — ${track.name}`),\n ];\n\n after(async () => {\n try {\n const verifyToken = await signOrderToken({\n orderId: String(recordedOrder.id),\n email: customerEmail,\n secret: orderTokenSecret,\n });\n const verifyUrl = `${baseUrl}${verifyPath}?token=${verifyToken}`;\n const { subject, html } = renderPurchaseConfirmation({\n branding,\n verifyUrl,\n itemNames,\n totalCents: session.amount_total ?? 0,\n });\n await email.send({\n from: emailFrom,\n to: customerEmail,\n subject,\n html,\n });\n } catch (err) {\n console.error(\n `[stripe-webhook] failed to send purchase confirmation to ${customerEmail} for session ${session.id}:`,\n err,\n );\n }\n });\n\n return Response.json({ received: true });\n };\n}\n\n/** Parse a JSON-stringified id array from Stripe metadata, returning `[]` on any failure. NaN entries are dropped. */\nfunction safeJsonIdList(value: string | undefined | null): number[] {\n if (!value) return [];\n try {\n const parsed: unknown = JSON.parse(value);\n if (!Array.isArray(parsed)) return [];\n return parsed.map((v) => Number(v)).filter((n) => Number.isFinite(n));\n } catch {\n return [];\n }\n}\n","import type { NextRequest } from \"next/server\";\nimport type { Queries, TrackFile } from \"@gigamusic/db\";\nimport type { DownloadDeps } from \"./types.js\";\n\ninterface RouteContext {\n params: Promise<{ token: string }>;\n}\n\n/**\n * Build the GET handler for `/download/[token]?trackId=…&format=…`. The token\n * is looked up via `queries.getDownloadToken`; the track grant is checked via\n * `queries.tokenGrantsTrack`. On success the handler 302s to a presigned R2\n * URL — the storage provider has `Content-Disposition: attachment` baked in,\n * so the browser triggers a same-tab download.\n *\n * Order of checks: locate the file in the catalog first (so a missing-file\n * is 404), *then* enforce the token-track grant (a wrong trackId for the\n * given token is 403, not 404).\n */\nexport function createDownloadHandler(\n deps: DownloadDeps,\n): (req: NextRequest, ctx: RouteContext) => Promise<Response> {\n const { queries, storage } = deps;\n\n return async (req: NextRequest, ctx: RouteContext): Promise<Response> => {\n const { token } = await ctx.params;\n const requestUrl = new URL(req.url);\n const trackIdRaw = requestUrl.searchParams.get(\"trackId\");\n const format = requestUrl.searchParams.get(\"format\") ?? \"mp3\";\n\n if (!trackIdRaw) {\n return Response.json({ error: \"Missing trackId\" }, { status: 400 });\n }\n const trackId = Number(trackIdRaw);\n if (!Number.isFinite(trackId)) {\n return Response.json({ error: \"Invalid trackId\" }, { status: 400 });\n }\n\n const downloadToken = await queries.getDownloadToken(token);\n if (!downloadToken) {\n return Response.json({ error: \"Invalid download link\" }, { status: 404 });\n }\n\n const file = await findTrackFileInCatalog(queries, trackId, format);\n if (!file) {\n return Response.json({ error: \"File not found\" }, { status: 404 });\n }\n\n const ok = await queries.tokenGrantsTrack(token, trackId);\n if (!ok) {\n return Response.json({ error: \"Track not in order\" }, { status: 403 });\n }\n\n const url = await storage.getPresignedDownloadUrl(file.storageKey, {\n filename: cleanDownloadFilename(file.fileName),\n contentType: format === \"wav\" ? \"audio/wav\" : \"audio/mpeg\",\n });\n return Response.redirect(url, 302);\n };\n}\n\n/** Walk the published catalog (released to all customers) to find the file matching `trackId` + `format`. */\nasync function findTrackFileInCatalog(\n queries: Queries,\n trackId: number,\n format: string,\n): Promise<TrackFile | null> {\n const releases = await queries.listPublishedReleases();\n for (const release of releases) {\n const track = release.tracks.find((t) => t.id === trackId);\n if (!track) continue;\n const file = track.files.find((f) => f.format === format);\n return file ?? null;\n }\n return null;\n}\n\n/** Trim stray leading/trailing whitespace from a download filename while keeping its extension intact. */\nexport function cleanDownloadFilename(name: string): string {\n const dot = name.lastIndexOf(\".\");\n if (dot <= 0) return name.trim();\n return `${name.slice(0, dot).trim()}${name.slice(dot).trim()}`;\n}\n\nexport type { Queries };\n","import type { NextRequest } from \"next/server\";\nimport type {\n OrderWithItems,\n Queries,\n ReleaseWithTracks,\n TrackWithFiles,\n} from \"@gigamusic/db\";\nimport type { StorageProvider } from \"@gigamusic/storage\";\nimport type { DownloadDeps } from \"./types.js\";\nimport { cleanDownloadFilename } from \"./download.js\";\n\ninterface RouteContext {\n params: Promise<{ token: string }>;\n}\n\n/** A file destined for the zip, paired with the storage object it streams from. */\nexport interface ZipFile {\n fileName: string;\n storageKey: string;\n contentType: string;\n}\n\ninterface ZipManifest {\n zipName: string;\n files: { fileName: string; url: string }[];\n}\n\n/**\n * Discriminated result of `resolveZipBundle` — the SW-manifest handler and\n * the server-side streaming handler both call this so they produce\n * byte-identical archives. `ok: false` carries the HTTP status the caller\n * should return.\n */\nexport type ResolveZipResult =\n | { ok: true; zipName: string; files: ZipFile[] }\n | { ok: false; status: number; error: string };\n\nexport interface ResolveZipArgs {\n queries: Queries;\n token: string;\n /** Single-release download; falsy means \"whole order or selected tracks\". */\n releaseId?: number | string | null;\n /** Comma-separated track IDs for partial-order downloads. */\n trackIdsParam?: string | null;\n /** \"mp3\" or \"wav\". */\n format?: string;\n /** Optional artist-name prefix; see `DownloadDeps.zipNamePrefix`. */\n zipNamePrefix?: string;\n}\n\n/** Prepend the artist prefix when configured; otherwise return `base` untouched. */\nfunction applyZipNamePrefix(prefix: string | undefined, base: string): string {\n const trimmed = prefix?.trim();\n return trimmed ? `${trimmed} - ${base}` : base;\n}\n\n/**\n * Auth + file-list resolution for a customer's bulk download. The\n * `createDownloadZipHandler` (SW manifest path) and\n * `createDownloadZipStreamHandler` (server-side streaming path) both call\n * this so the two routes stay in lockstep on auth and on what ends up\n * in the archive.\n */\nexport async function resolveZipBundle(args: ResolveZipArgs): Promise<ResolveZipResult> {\n const { queries, token } = args;\n const format = args.format ?? \"mp3\";\n const audioContentType = format === \"wav\" ? \"audio/wav\" : \"audio/mpeg\";\n const releaseIdRaw = args.releaseId == null ? null : String(args.releaseId);\n const trackIdsParam = args.trackIdsParam ?? null;\n\n const downloadToken = await queries.getDownloadToken(token);\n if (!downloadToken) {\n return { ok: false, status: 404, error: \"Invalid download link\" };\n }\n\n const order = await queries.getOrderById(downloadToken.orderId);\n if (!order) {\n return { ok: false, status: 404, error: \"Invalid download link\" };\n }\n\n return resolveBundle({\n order,\n releaseId: releaseIdRaw,\n trackIdsParam,\n format,\n audioContentType,\n zipNamePrefix: args.zipNamePrefix,\n });\n}\n\n/**\n * Build the GET handler for the zip-manifest endpoint. Returns the JSON\n * manifest the consumer-shipped service worker (`public/sw-zip.js`)\n * consumes — the SW pipes presigned R2 URLs through\n * `client-zip` and streams the archive straight from R2 to the browser, with\n * the Vercel function never touching audio bytes.\n *\n * Query-param branches:\n * - `trackIds` set → curated track list, flat layout\n * - `releaseId` set → single-release bundle with cover art\n * - neither set → whole order (releases + à-la-carte tracks)\n */\nexport function createDownloadZipHandler(\n deps: DownloadDeps,\n): (req: NextRequest, ctx: RouteContext) => Promise<Response> {\n const { queries, storage } = deps;\n\n return async (req: NextRequest, ctx: RouteContext): Promise<Response> => {\n const { token } = await ctx.params;\n const url = new URL(req.url);\n const resolution = await resolveZipBundle({\n queries,\n token,\n releaseId: url.searchParams.get(\"releaseId\"),\n trackIdsParam: url.searchParams.get(\"trackIds\"),\n format: url.searchParams.get(\"format\") ?? \"mp3\",\n zipNamePrefix: deps.zipNamePrefix,\n });\n if (!resolution.ok) {\n return Response.json({ error: resolution.error }, { status: resolution.status });\n }\n\n const manifest: ZipManifest = {\n zipName: resolution.zipName,\n files: await presignZipFiles(storage, resolution.files),\n };\n return Response.json(manifest);\n };\n}\n\ntype Resolution =\n | { ok: true; zipName: string; files: ZipFile[] }\n | { ok: false; status: number; error: string };\n\ninterface ResolveArgs {\n order: OrderWithItems;\n releaseId: string | null;\n trackIdsParam: string | null;\n format: string;\n audioContentType: string;\n zipNamePrefix?: string;\n}\n\nfunction resolveBundle(args: ResolveArgs): Resolution {\n const { order, releaseId, trackIdsParam, format, audioContentType, zipNamePrefix } = args;\n const fmt = format.toUpperCase();\n\n if (trackIdsParam) {\n return resolveTrackList(order, trackIdsParam, format, audioContentType, fmt, zipNamePrefix);\n }\n if (releaseId) {\n const id = Number(releaseId);\n if (!Number.isFinite(id)) {\n return { ok: false, status: 404, error: \"Release not found\" };\n }\n return resolveSingleRelease(order, id, format, audioContentType, fmt, zipNamePrefix);\n }\n return resolveWholeOrder(order, format, audioContentType, fmt, zipNamePrefix);\n}\n\nfunction resolveTrackList(\n order: OrderWithItems,\n trackIdsParam: string,\n format: string,\n audioContentType: string,\n fmt: string,\n zipNamePrefix: string | undefined,\n): Resolution {\n const requestedTrackIds = trackIdsParam\n .split(\",\")\n .map((s) => Number(s.trim()))\n .filter((n) => Number.isFinite(n));\n const ownership = trackOwnership(order);\n const allOwned = requestedTrackIds.every((id) => ownership.owns(id));\n if (!allOwned) {\n return { ok: false, status: 403, error: \"Track not in order\" };\n }\n\n const files: ZipFile[] = [];\n for (const id of requestedTrackIds) {\n const ctx = ownership.locate(id);\n if (!ctx) continue;\n const file = ctx.track.files.find((f) => f.format === format);\n if (!file) continue;\n files.push({\n fileName: zipEntryPath(ctx.release?.name ?? null, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n });\n }\n if (files.length === 0) {\n return {\n ok: false,\n status: 404,\n error: \"No audio files have been uploaded for these tracks yet.\",\n };\n }\n return {\n ok: true,\n zipName: applyZipNamePrefix(zipNamePrefix, `Tracks (${fmt}).zip`),\n files,\n };\n}\n\nfunction resolveSingleRelease(\n order: OrderWithItems,\n releaseId: number,\n format: string,\n audioContentType: string,\n fmt: string,\n zipNamePrefix: string | undefined,\n): Resolution {\n const orderHasRelease = order.items.some((item) => item.releaseId === releaseId);\n if (!orderHasRelease) {\n return { ok: false, status: 403, error: \"Release not in order\" };\n }\n\n const release = order.items.find((item) => item.release?.id === releaseId)?.release;\n if (!release) {\n return { ok: false, status: 404, error: \"Release not found\" };\n }\n\n const files: ZipFile[] = release.tracks\n .map((track) => {\n const file = track.files.find((f) => f.format === format);\n if (!file) return null;\n return {\n fileName: zipEntryPath(null, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n } satisfies ZipFile;\n })\n .filter((f): f is ZipFile => f !== null);\n\n if (files.length > 0 && release.coverImageUrl) {\n files.push(...coverArtEntries(release.name, release.coverImageUrl, \"\", files));\n }\n\n if (files.length === 0) {\n return {\n ok: false,\n status: 404,\n error: \"No audio files have been uploaded for these tracks yet.\",\n };\n }\n return {\n ok: true,\n zipName: applyZipNamePrefix(zipNamePrefix, `${release.name} (${fmt}).zip`),\n files,\n };\n}\n\nfunction resolveWholeOrder(\n order: OrderWithItems,\n format: string,\n audioContentType: string,\n fmt: string,\n zipNamePrefix: string | undefined,\n): Resolution {\n const releaseFiles: ZipFile[] = [];\n const aLaCarteFiles: ZipFile[] = [];\n\n for (const item of order.items) {\n if (item.release) {\n const release = item.release;\n const entries: ZipFile[] = [];\n for (const track of release.tracks) {\n const file = track.files.find((f) => f.format === format);\n if (!file) continue;\n entries.push({\n fileName: zipEntryPath(release.name, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n });\n }\n if (entries.length > 0 && release.coverImageUrl) {\n entries.push(\n ...coverArtEntries(\n release.name,\n release.coverImageUrl,\n `${sanitizeSegment(release.name)}/`,\n entries,\n ),\n );\n }\n releaseFiles.push(...entries);\n } else if (item.track) {\n const track = item.track;\n const release = track.release ?? null;\n const file = track.files.find((f) => f.format === format);\n if (!file) continue;\n aLaCarteFiles.push({\n fileName: zipEntryPath(release?.name ?? null, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n });\n }\n }\n\n const files = [...releaseFiles, ...aLaCarteFiles];\n if (files.length === 0) {\n return {\n ok: false,\n status: 404,\n error: \"No audio files have been uploaded for these tracks yet.\",\n };\n }\n return {\n ok: true,\n zipName: applyZipNamePrefix(zipNamePrefix, `Order ${order.id} (${fmt}).zip`),\n files,\n };\n}\n\ninterface TrackContext {\n track: TrackWithFiles;\n release?: ReleaseWithTracks | null;\n}\n\n/** Index every track an order grants access to so the manifest resolver can both check ownership and locate file rows. */\nfunction trackOwnership(order: OrderWithItems) {\n const byId = new Map<number, TrackContext>();\n for (const item of order.items) {\n if (item.release) {\n for (const t of item.release.tracks) {\n byId.set(t.id, { track: t, release: item.release });\n }\n }\n if (item.track) {\n byId.set(item.track.id, {\n track: item.track,\n release: (item.track.release as ReleaseWithTracks | null | undefined) ?? null,\n });\n }\n }\n return {\n owns: (id: number) => byId.has(id),\n locate: (id: number) => byId.get(id) ?? null,\n };\n}\n\n/** Strip path separators so a release/track name can't spawn unintended zip subfolders. */\nexport function sanitizeSegment(name: string): string {\n return name.replace(/[/\\\\]+/g, \"-\").trim();\n}\n\n/**\n * Detects extended-mix tracks from their filename. The catalog marks them\n * inconsistently (\"Extended\", \"(Extended)\", \"[EXTENDED MIX]\"), so a loose\n * whole-word match on \"extended\" catches every variant.\n */\nexport function isExtendedMix(fileName: string): boolean {\n return /\\bextended\\b/i.test(fileName);\n}\n\n/**\n * Build a zip entry path: `[Release Name/][Extended/]filename`. Extended mixes\n * are nested in their own `Extended/` subfolder so they don't clutter the main\n * release listing. `releaseName` is null for single-release zips (which stay\n * flat apart from the `Extended/` split).\n */\nexport function zipEntryPath(releaseName: string | null, fileName: string): string {\n const clean = cleanDownloadFilename(fileName);\n const segments: string[] = [];\n if (releaseName) segments.push(sanitizeSegment(releaseName));\n if (isExtendedMix(clean)) segments.push(\"Extended\");\n segments.push(clean);\n return segments.join(\"/\");\n}\n\n/** Zip filename for a release's cover art. */\nexport function coverArtFilename(releaseName: string): string {\n return `${sanitizeSegment(releaseName)} - COVER ART.jpg`;\n}\n\n/**\n * Cover-art zip entries for a release: the artwork alongside the tracks, plus\n * a duplicate inside the `Extended/` subfolder when any track entry uses one.\n * `folder` is the release's `Name/` prefix for multi-release zips, or \"\" for\n * flat single-release zips.\n */\nexport function coverArtEntries(\n releaseName: string,\n coverImageUrl: string,\n folder: string,\n trackEntries: ZipFile[],\n): ZipFile[] {\n const file = coverArtFilename(releaseName);\n const entries: ZipFile[] = [\n { fileName: `${folder}${file}`, storageKey: coverImageUrl, contentType: \"image/jpeg\" },\n ];\n if (trackEntries.some((e) => e.fileName.split(\"/\").includes(\"Extended\"))) {\n entries.push({\n fileName: `${folder}Extended/${file}`,\n storageKey: coverImageUrl,\n contentType: \"image/jpeg\",\n });\n }\n return entries;\n}\n\n/** Presign every zip entry into the `{ fileName, url }` manifest shape the service worker consumes. */\nasync function presignZipFiles(\n storage: StorageProvider,\n files: ZipFile[],\n): Promise<ZipManifest[\"files\"]> {\n return Promise.all(\n files.map(async (t) => ({\n fileName: t.fileName,\n url: await storage.getPresignedDownloadUrl(t.storageKey, {\n filename: t.fileName.split(\"/\").pop() ?? t.fileName,\n contentType: t.contentType,\n }),\n })),\n );\n}\n\nexport type { Queries };\n","import type { NextRequest } from \"next/server\";\nimport { Readable, Transform } from \"node:stream\";\nimport { finished } from \"node:stream/promises\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\nimport archiver from \"archiver\";\nimport type { DownloadDeps } from \"./types.js\";\nimport { resolveZipBundle } from \"./zip.js\";\n\ninterface RouteContext {\n params: Promise<{ token: string }>;\n}\n\n/**\n * Server-side bulk-download fallback. Mirrors `createDownloadZipHandler`'s\n * auth + file-list logic via the shared `resolveZipBundle()`, but streams\n * a real zip back to the browser through `archiver` instead of returning\n * a manifest for a service worker to assemble.\n *\n * When to wire this up: in addition to `createDownloadZipHandler`. Browsers\n * that can't run the service worker reliably (WebKit — Safari desktop and\n * every iOS browser, plus private/incognito on most engines) get truncated\n * archives from the SW path. Detect those browsers at click time on the\n * client and hard-navigate here instead.\n *\n * Cost trade-off: audio bytes proxy through the serverless function and\n * cost egress, which is why the SW path stays the default for browsers\n * that handle it correctly.\n *\n * Per-file failure policy matches the SW: any failed storage fetch becomes\n * a `_FAILED_<name>.txt` placeholder so one bad object doesn't taint the\n * whole archive.\n */\nexport function createDownloadZipStreamHandler(\n deps: DownloadDeps,\n): (req: NextRequest, ctx: RouteContext) => Promise<Response> {\n const { queries, storage } = deps;\n\n return async (req: NextRequest, ctx: RouteContext): Promise<Response> => {\n const { token } = await ctx.params;\n const url = new URL(req.url);\n\n const resolution = await resolveZipBundle({\n queries,\n token,\n releaseId: url.searchParams.get(\"releaseId\"),\n trackIdsParam: url.searchParams.get(\"trackIds\"),\n format: url.searchParams.get(\"format\") ?? \"mp3\",\n zipNamePrefix: deps.zipNamePrefix,\n });\n if (!resolution.ok) {\n return Response.json({ error: resolution.error }, { status: resolution.status });\n }\n\n // `store: true` = no DEFLATE compression. MP3/WAV barely compress, and\n // the SW path serves uncompressed entries — staying consistent means\n // identical byte counts across the two routes.\n const archive = archiver(\"zip\", { store: true });\n\n archive.on(\"error\", (err) => {\n console.error(\"[zip-stream] archiver error:\", err);\n });\n\n // Tear the pipeline down if the client disconnects mid-download.\n req.signal.addEventListener(\"abort\", () => archive.abort());\n\n // Serialise storage fetches: only open file N's socket after archiver\n // has fully consumed file N-1. The naive parallel approach (open every\n // socket up front, let archiver drain them in order) leaves the later\n // sockets idle behind backpressure long enough for upstream R2/S3 to\n // time them out, surfacing as `TypeError: terminated` mid-stream. A\n // serial pipeline costs one TTFB per file transition — small compared\n // to the per-file streaming time — and archiver is the serial\n // bottleneck anyway, so end-to-end throughput is unchanged.\n (async () => {\n for (const file of resolution.files) {\n if (req.signal.aborted) return;\n const baseName = file.fileName.split(\"/\").pop() || file.fileName;\n try {\n const url = await storage.getPresignedDownloadUrl(file.storageKey, {\n filename: baseName,\n contentType: file.contentType,\n });\n const res = await fetch(url, { signal: req.signal });\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n // DOM `ReadableStream` and Node's web `ReadableStream` are\n // runtime-compatible but typed separately — cast at the boundary.\n const body = Readable.fromWeb(\n res.body as unknown as NodeReadableStream<Uint8Array>,\n );\n // Pass-through counter for two reasons: we get a meaningful\n // \"bytesReceived\" in failure logs, and `archiver.append(counter)`\n // gives us a handle the `finished()` await below can resolve on\n // (archiver's internal entry isn't exposed).\n let bytesReceived = 0;\n const counter = new Transform({\n transform(chunk: Buffer, _enc, cb) {\n bytesReceived += chunk.length;\n cb(null, chunk);\n },\n });\n // Forward upstream errors onto the counter so the `finished`\n // await rejects and the loop can append a _FAILED_ placeholder.\n body.on(\"error\", (err) => counter.destroy(err));\n body.pipe(counter);\n archive.append(counter, { name: file.fileName });\n try {\n // Resolves once both sides of `counter` are done: body has\n // ended (upstream finished) AND archiver has drained the\n // readable side (entry fully written to the zip queue).\n await finished(counter);\n } catch (err) {\n if (req.signal.aborted) return;\n const detail = err instanceof Error ? err.message : String(err);\n console.error(\n `[zip-stream] body stream error for ${file.fileName} ` +\n `(storageKey=${file.storageKey}, bytesReceived=${bytesReceived}): ${detail}`,\n );\n try {\n archive.append(\n `Streaming \"${file.fileName}\" from storage failed after ${bytesReceived} bytes: ${detail}.\\n` +\n `Try downloading the track individually from your order page.\\n`,\n { name: `_FAILED_${baseName}.txt` },\n );\n } catch (appendErr) {\n console.warn(\n `[zip-stream] could not append failure placeholder for ${file.fileName}:`,\n appendErr,\n );\n }\n }\n } catch (err) {\n if (req.signal.aborted) return;\n const detail = err instanceof Error ? err.message : String(err);\n console.warn(`[zip-stream] fetch failed for ${file.fileName}:`, detail);\n archive.append(\n `Failed to download \"${file.fileName}\" from storage: ${detail}.\\n` +\n `Try downloading the track individually from your order page.\\n`,\n { name: `_FAILED_${baseName}.txt` },\n );\n }\n }\n await archive.finalize();\n })().catch((err) => {\n console.error(\"[zip-stream] pipeline error:\", err);\n archive.abort();\n });\n\n return new Response(Readable.toWeb(archive) as ReadableStream<Uint8Array>, {\n headers: {\n \"Content-Type\": \"application/zip\",\n \"Content-Disposition\": `attachment; filename=\"${resolution.zipName.replace(/\"/g, \"\")}\"`,\n \"Cache-Control\": \"no-store\",\n },\n });\n };\n}\n","const ERROR_HTML = `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"color-scheme\" content=\"light dark\">\n <title>Bulk download couldn't start</title>\n <style>\n body { font-family: system-ui, -apple-system, sans-serif; max-width: 560px; margin: 3rem auto; padding: 0 1.25rem; line-height: 1.55; }\n h1 { font-size: 1.35rem; margin-bottom: 0.75rem; }\n ul { padding-left: 1.25rem; }\n li { margin: 0.35rem 0; }\n a { color: #6366f1; }\n </style>\n</head>\n<body>\n <h1>Bulk download couldn't start</h1>\n <p>Your browser couldn't generate the zip in the background. This usually happens in private/incognito mode, or after the order page has been open for a long time.</p>\n <p>To get your music:</p>\n <ul>\n <li>Go back to your order page and use the individual track download buttons.</li>\n <li>Or reopen the order link in a fresh tab and try the bulk download again.</li>\n </ul>\n <p><a href=\"javascript:history.back()\">&larr; Back to your order</a></p>\n</body>\n</html>`;\n\n/**\n * Fallback for `/sw-zip/[...path]` reached only when the consumer-shipped\n * service worker (`public/sw-zip.js`) fails to intercept the\n * navigation. Without this route the browser would save Next.js's 404\n * HTML under the URL's `.zip` filename — and macOS Archive Utility\n * surfaces that as the confusing \"Error 79 - Inappropriate file type or\n * format.\" Returning `text/html` (and no `Content-Disposition`) makes\n * the browser render the message instead of saving it.\n *\n * Wire up at `app/sw-zip/[...path]/route.ts`:\n *\n * ```ts\n * export const GET = createSwZipFallbackHandler();\n * ```\n */\nexport function createSwZipFallbackHandler(): () => Response {\n return () => {\n // Hitting this route is always a failure mode — the SW should have\n // intercepted. Log so the frequency shows up in serverless logs.\n console.warn(\"[sw-zip] fallback route hit — service worker did not intercept\");\n return new Response(ERROR_HTML, {\n status: 503,\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n },\n });\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/checkout.ts","../src/webhook.ts","../src/download.ts","../src/zip.ts","../src/zip-stream.ts","../src/sw-zip-fallback.ts"],"names":["session","Stripe","url"],"mappings":"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","file":"index.js","sourcesContent":["import type { NextRequest } from \"next/server\";\nimport Stripe from \"stripe\";\nimport type { ReleaseWithTracks, TrackWithFiles } from \"@gigamusic/db\";\nimport type { CheckoutCartItem, CheckoutDeps } from \"./types.js\";\n\n/**\n * Stripe rejects card charges below 50 cents (USD). A pay-what-you-want\n * override below this is clamped up rather than rejected, so a customer who\n * types a token amount still completes checkout instead of hitting an opaque\n * Stripe error. A `0` override never reaches here — the consumer routes free\n * downloads through its own gate, not the cart.\n */\nconst STRIPE_MIN_CHARGE_CENTS = 50;\n\n/**\n * Resolve the per-line `unit_amount`: a positive pay-what-you-want override\n * (clamped up to the Stripe minimum) when supplied, otherwise the catalog\n * price. Non-positive / non-finite overrides fall back to the catalog price.\n */\nfunction resolveUnitAmount(\n overrideCents: number | undefined,\n catalogCents: number,\n): number {\n if (\n typeof overrideCents === \"number\" &&\n Number.isFinite(overrideCents) &&\n overrideCents > 0\n ) {\n return Math.max(STRIPE_MIN_CHARGE_CENTS, Math.round(overrideCents));\n }\n return catalogCents;\n}\n\n/**\n * Build a POST handler that translates `{ items }` from the cart into a Stripe\n * Checkout Session and replies with `{ url }`. Release/track metadata is\n * stashed on the session so the webhook handler can later record the order\n * without re-deriving cart contents.\n *\n * Stripe is hard-wired (per project policy). The Stripe SDK instance is\n * created once per factory call and captured in the closure — no module\n * globals, no `process.env`.\n */\nexport function createCheckoutHandler(\n deps: CheckoutDeps,\n): (req: NextRequest) => Promise<Response> {\n const {\n stripeSecret,\n queries,\n site,\n baseUrl,\n successPath = \"/checkout/success\",\n cancelPath = \"/cart\",\n currency = \"usd\",\n catalogPurchase,\n } = deps;\n\n const stripe = new Stripe(stripeSecret, { typescript: true });\n\n return async (req: NextRequest): Promise<Response> => {\n let payload: { items?: CheckoutCartItem[] };\n try {\n payload = (await req.json()) as { items?: CheckoutCartItem[] };\n } catch {\n return jsonError(\"Invalid JSON body\", 400);\n }\n\n const items = payload.items;\n if (!items || items.length === 0) {\n return jsonError(\"No items provided\", 400);\n }\n\n const successUrl = `${baseUrl}${successPath}?session_id={CHECKOUT_SESSION_ID}`;\n const cancelUrl = `${baseUrl}${cancelPath}`;\n\n const isCatalogPurchase = items.some((i) => i.kind === \"catalog\");\n\n try {\n if (isCatalogPurchase) {\n const resolved =\n typeof catalogPurchase === \"function\"\n ? await catalogPurchase()\n : catalogPurchase;\n if (!resolved) {\n return jsonError(\"Catalog purchase not configured\", 400);\n }\n // Snapshot the current published-release ids so the webhook records\n // exactly what the customer bought, even if a release is unpublished\n // between session-create and webhook-delivery. The consumer-supplied\n // `totalCents` is what Stripe charges — pricing math lives in the\n // consumer (it owns whatever discount/rounding/tax rules apply).\n const releases = await queries.listPublishedReleases();\n\n const session = await stripe.checkout.sessions.create({\n mode: \"payment\",\n line_items: [\n {\n price_data: {\n currency,\n product_data: { name: resolved.productName },\n unit_amount: resolved.totalCents,\n },\n quantity: 1,\n },\n ],\n metadata: {\n site,\n catalog_purchase: \"true\",\n release_ids: JSON.stringify(releases.map((r) => r.id)),\n track_ids: \"[]\",\n },\n success_url: successUrl,\n cancel_url: cancelUrl,\n });\n\n return Response.json({ url: session.url });\n }\n\n const releaseIds = items\n .filter((i) => i.kind === \"release\" && i.id != null)\n .map((i) => i.id as number);\n const trackIds = items\n .filter((i) => i.kind === \"track\" && i.id != null)\n .map((i) => i.id as number);\n\n if (releaseIds.length === 0 && trackIds.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n // Pay-what-you-want overrides, keyed by id. Items without `priceCents`\n // (the fixed-price default — every thomas-adagio cart) leave the map\n // empty and fall back to the catalog price below.\n const releaseAmountOverride = new Map<number, number>(\n items\n .filter(\n (i) =>\n i.kind === \"release\" &&\n i.id != null &&\n typeof i.priceCents === \"number\",\n )\n .map((i) => [i.id as number, i.priceCents as number]),\n );\n const trackAmountOverride = new Map<number, number>(\n items\n .filter(\n (i) =>\n i.kind === \"track\" &&\n i.id != null &&\n typeof i.priceCents === \"number\",\n )\n .map((i) => [i.id as number, i.priceCents as number]),\n );\n\n // Charged `unit_amount` per line, keyed `r<id>` / `t<id>`. Stamped onto\n // the session so the webhook records the true price paid rather than\n // re-deriving the (suggested) catalog price.\n const chargedAmounts: Record<string, number> = {};\n\n // Single `listPublishedReleases` call sources both release and track\n // line items. Tracks live inside their release, so one fetch keeps the\n // ID/price/cover lookups consistent without a second round-trip.\n const allReleases = await queries.listPublishedReleases();\n const releaseById = new Map(allReleases.map((r) => [r.id, r]));\n const trackContext = new Map<\n number,\n { track: TrackWithFiles; release: ReleaseWithTracks }\n >();\n for (const release of allReleases) {\n for (const track of release.tracks) {\n trackContext.set(track.id, { track, release });\n }\n }\n\n const releaseLineItems = releaseIds\n .map((id) => releaseById.get(id))\n .filter((r): r is ReleaseWithTracks => r !== undefined)\n .map((r) => {\n const amount = resolveUnitAmount(releaseAmountOverride.get(r.id), r.price);\n chargedAmounts[`r${r.id}`] = amount;\n return {\n price_data: {\n currency,\n product_data: {\n name: r.name,\n ...(r.coverImageUrl\n ? { images: [toAbsoluteUrl(r.coverImageUrl, baseUrl)] }\n : {}),\n },\n unit_amount: amount,\n },\n quantity: 1 as const,\n };\n });\n\n const trackLineItems = trackIds\n .map((id) => trackContext.get(id))\n .filter(\n (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>\n c !== undefined,\n )\n .map(({ track, release }) => {\n const amount = resolveUnitAmount(\n trackAmountOverride.get(track.id),\n track.price,\n );\n chargedAmounts[`t${track.id}`] = amount;\n return {\n price_data: {\n currency,\n product_data: {\n name: `${release.name} — ${track.name}`,\n ...(release.coverImageUrl\n ? { images: [toAbsoluteUrl(release.coverImageUrl, baseUrl)] }\n : {}),\n },\n unit_amount: amount,\n },\n quantity: 1 as const,\n };\n });\n\n const lineItems = [...releaseLineItems, ...trackLineItems];\n if (lineItems.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n const resolvedReleaseIds = releaseLineItems.length\n ? releaseIds.filter((id) => releaseById.has(id))\n : [];\n const resolvedTrackIds = trackLineItems.length\n ? trackIds.filter((id) => trackContext.has(id))\n : [];\n\n const session = await stripe.checkout.sessions.create({\n mode: \"payment\",\n line_items: lineItems,\n metadata: {\n site,\n release_ids: JSON.stringify(resolvedReleaseIds),\n track_ids: JSON.stringify(resolvedTrackIds),\n amounts: JSON.stringify(chargedAmounts),\n },\n success_url: successUrl,\n cancel_url: cancelUrl,\n });\n\n return Response.json({ url: session.url });\n } catch (err) {\n return jsonError(\"Failed to create checkout session\", 500, err);\n }\n };\n}\n\n/** Resolve a stored relative URL against `baseUrl` and percent-encode the path. Bare http(s) URLs pass through. */\nfunction toAbsoluteUrl(url: string, baseUrl: string): string {\n const raw = url.startsWith(\"http\")\n ? url\n : `${baseUrl}${url.startsWith(\"/\") ? \"\" : \"/\"}${url}`;\n try {\n const parsed = new URL(raw);\n parsed.pathname = encodeURI(decodeURI(parsed.pathname));\n return parsed.toString();\n } catch {\n return raw;\n }\n}\n\nfunction jsonError(message: string, status: number, cause?: unknown): Response {\n if (cause) {\n // Logged via console.error. Consumers can swap this for a structured\n // logger in a future iteration; the package contract doesn't expose a\n // logger slot yet.\n console.error(`[checkout] ${message}:`, cause);\n }\n return Response.json({ error: message }, { status });\n}\n","import { after, type NextRequest } from \"next/server\";\nimport Stripe from \"stripe\";\nimport { signOrderToken } from \"@gigamusic/core\";\nimport { isUniqueConstraintError, type ReleaseWithTracks, type TrackWithFiles } from \"@gigamusic/db\";\nimport { renderPurchaseConfirmation } from \"@gigamusic/email\";\nimport type { WebhookDeps } from \"./types.js\";\n\n/**\n * Build the POST handler for Stripe's webhook endpoint. Verifies the\n * signature, records the order on `checkout.session.completed`, then defers\n * the magic-link email via `after()` so Stripe sees a fast 200.\n *\n * Idempotency is enforced two ways: a pre-flight `getOrderByStripeSessionId`\n * lookup, plus a unique-constraint catch around `recordCompletedOrder` for\n * the case where two webhook deliveries race past the lookup. Both paths\n * short-circuit with `{ received: true }` so Stripe doesn't retry.\n *\n * ### Published-only resolution (gotcha)\n *\n * The handler resolves the release/track IDs from `session.metadata` against\n * `queries.listPublishedReleases()`. Anything not in the published catalog at\n * the moment the webhook fires is silently dropped from the persisted order's\n * line items — the customer is still charged, but the order row doesn't\n * include the unpublished item.\n *\n * In the canonical flow this can't happen: `createCheckoutHandler` filters on\n * the same `listPublishedReleases()` before it ever creates the Stripe\n * Session. The only ways an unpublished ID can reach this handler are:\n * - an admin unpublishes between session-create and webhook-delivery\n * (a several-second race window),\n * - a consumer bypasses `createCheckoutHandler` and creates Stripe sessions\n * directly with their own metadata.\n *\n * If either of those is a realistic concern, swap the resolution call here\n * to `listAllReleases()` (or wrap with a \"warn + fallback\" branch) — but be\n * aware that doing so will surface unpublished items in admin order views.\n */\nexport function createStripeWebhookHandler(\n deps: WebhookDeps,\n): (req: NextRequest) => Promise<Response> {\n const {\n stripe: stripeOverride,\n stripeSecret,\n webhookSecret,\n queries,\n site,\n email,\n branding,\n emailFrom,\n baseUrl,\n orderTokenSecret,\n orderTokenExpiry = null,\n verifyPath = \"/orders/verify\",\n renderConfirmation = renderPurchaseConfirmation,\n } = deps;\n\n if (!stripeOverride && !stripeSecret) {\n throw new Error(\n \"createStripeWebhookHandler: must supply either `stripe` or `stripeSecret`\",\n );\n }\n const stripe =\n stripeOverride ?? new Stripe(stripeSecret as string, { typescript: true });\n\n return async (req: NextRequest): Promise<Response> => {\n const body = await req.text();\n const sig = req.headers.get(\"stripe-signature\");\n\n if (!sig) {\n return Response.json({ error: \"Missing signature\" }, { status: 400 });\n }\n\n let event: Stripe.Event;\n try {\n event = stripe.webhooks.constructEvent(body, sig, webhookSecret);\n } catch {\n return Response.json({ error: \"Invalid signature\" }, { status: 400 });\n }\n\n if (event.type !== \"checkout.session.completed\") {\n return Response.json({ received: true });\n }\n\n const session = event.data.object as Stripe.Checkout.Session;\n\n // One Stripe account fans every event out to every registered endpoint.\n // Ignore sessions that belong to a different site so we don't record a\n // foreign purchase (with zero matched line items) under this site.\n if (session.metadata?.site !== site) {\n return Response.json({ received: true });\n }\n\n const existing = await queries.getOrderByStripeSessionId(session.id);\n if (existing) {\n return Response.json({ received: true });\n }\n\n const releaseIds = safeJsonIdList(session.metadata?.release_ids);\n const trackIds = safeJsonIdList(session.metadata?.track_ids);\n // Per-line amounts the customer actually paid, keyed `r<id>` / `t<id>`,\n // stamped by `createCheckoutHandler`. Absent on pre-upgrade sessions and\n // on catalog purchases — those fall back to the catalog price below.\n const chargedAmounts = safeAmountMap(session.metadata?.amounts);\n\n if (releaseIds.length === 0 && trackIds.length === 0) {\n return Response.json({ received: true });\n }\n\n // Snapshot the published catalog once so we can resolve both release and\n // track prices + names off a single in-memory lookup.\n const allReleases = await queries.listPublishedReleases();\n const releaseById = new Map(allReleases.map((r) => [r.id, r]));\n const trackContext = new Map<\n number,\n { track: TrackWithFiles; release: ReleaseWithTracks }\n >();\n for (const release of allReleases) {\n for (const track of release.tracks) {\n trackContext.set(track.id, { track, release });\n }\n }\n\n const matchedReleases = releaseIds\n .map((id) => releaseById.get(id))\n .filter((r): r is ReleaseWithTracks => r !== undefined);\n const matchedTracks = trackIds\n .map((id) => trackContext.get(id))\n .filter(\n (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>\n c !== undefined,\n );\n\n const orderItems = [\n ...matchedReleases.map((r) => ({\n releaseId: r.id,\n price: chargedAmounts[`r${r.id}`] ?? r.price,\n })),\n ...matchedTracks.map(({ track }) => ({\n trackId: track.id,\n price: chargedAmounts[`t${track.id}`] ?? track.price,\n })),\n ];\n\n const customerEmail = session.customer_details?.email ?? \"\";\n\n let recordedOrder;\n try {\n recordedOrder = await queries.recordCompletedOrder({\n stripeSessionId: session.id,\n stripePaymentId:\n typeof session.payment_intent === \"string\" ? session.payment_intent : null,\n email: customerEmail,\n amountTotal: session.amount_total ?? 0,\n items: orderItems,\n });\n } catch (err) {\n // Two deliveries can both pass the pre-flight lookup and race here. The\n // unique constraint on `stripeSessionId` guarantees only one row lands;\n // the loser surfaces as a 23505 unique-violation. Treat that as the same\n // idempotent short-circuit — no email, no 500, no Stripe retry.\n if (isUniqueConstraintError(err)) {\n return Response.json({ received: true });\n }\n throw err;\n }\n\n if (!customerEmail) {\n // Stripe almost always supplies an email but doesn't guarantee it. The\n // order is safely recorded; manual intervention required to deliver the\n // magic link.\n console.warn(\n `[stripe-webhook] checkout.session.completed (session=${session.id}) has no customer_details.email — order recorded but customer cannot retrieve downloads.`,\n );\n return Response.json({ received: true });\n }\n\n const itemNames = [\n ...matchedReleases.map((r) => r.name),\n ...matchedTracks.map(({ track, release }) => `${release.name} — ${track.name}`),\n ];\n\n after(async () => {\n try {\n const verifyToken = await signOrderToken({\n orderId: String(recordedOrder.id),\n email: customerEmail,\n secret: orderTokenSecret,\n expiresIn: orderTokenExpiry,\n });\n const verifyUrl = `${baseUrl}${verifyPath}?token=${verifyToken}`;\n const { subject, html } = await renderConfirmation({\n branding,\n verifyUrl,\n itemNames,\n totalCents: session.amount_total ?? 0,\n });\n await email.send({\n from: emailFrom,\n to: customerEmail,\n subject,\n html,\n });\n } catch (err) {\n console.error(\n `[stripe-webhook] failed to send purchase confirmation to ${customerEmail} for session ${session.id}:`,\n err,\n );\n }\n });\n\n return Response.json({ received: true });\n };\n}\n\n/**\n * Parse the JSON-stringified `{ \"r<id>\" | \"t<id>\": cents }` amount map from\n * Stripe metadata, returning `{}` on any failure. Non-finite values are\n * dropped so a malformed entry falls back to the catalog price at the call site.\n */\nfunction safeAmountMap(value: string | undefined | null): Record<string, number> {\n if (!value) return {};\n try {\n const parsed: unknown = JSON.parse(value);\n if (typeof parsed !== \"object\" || parsed === null) return {};\n const out: Record<string, number> = {};\n for (const [key, raw] of Object.entries(parsed as Record<string, unknown>)) {\n const n = Number(raw);\n if (Number.isFinite(n)) out[key] = n;\n }\n return out;\n } catch {\n return {};\n }\n}\n\n/** Parse a JSON-stringified id array from Stripe metadata, returning `[]` on any failure. NaN entries are dropped. */\nfunction safeJsonIdList(value: string | undefined | null): number[] {\n if (!value) return [];\n try {\n const parsed: unknown = JSON.parse(value);\n if (!Array.isArray(parsed)) return [];\n return parsed.map((v) => Number(v)).filter((n) => Number.isFinite(n));\n } catch {\n return [];\n }\n}\n","import type { NextRequest } from \"next/server\";\nimport type { Queries, TrackFile } from \"@gigamusic/db\";\nimport type { DownloadDeps } from \"./types.js\";\n\ninterface RouteContext {\n params: Promise<{ token: string }>;\n}\n\n/**\n * Build the GET handler for `/download/[token]?trackId=…&format=…`. The token\n * is looked up via `queries.getDownloadToken`; the track grant is checked via\n * `queries.tokenGrantsTrack`. On success the handler 302s to a presigned R2\n * URL — the storage provider has `Content-Disposition: attachment` baked in,\n * so the browser triggers a same-tab download.\n *\n * Order of checks: locate the file in the catalog first (so a missing-file\n * is 404), *then* enforce the token-track grant (a wrong trackId for the\n * given token is 403, not 404).\n */\nexport function createDownloadHandler(\n deps: DownloadDeps,\n): (req: NextRequest, ctx: RouteContext) => Promise<Response> {\n const { queries, storage } = deps;\n\n return async (req: NextRequest, ctx: RouteContext): Promise<Response> => {\n const { token } = await ctx.params;\n const requestUrl = new URL(req.url);\n const trackIdRaw = requestUrl.searchParams.get(\"trackId\");\n const format = requestUrl.searchParams.get(\"format\") ?? \"mp3\";\n\n if (!trackIdRaw) {\n return Response.json({ error: \"Missing trackId\" }, { status: 400 });\n }\n const trackId = Number(trackIdRaw);\n if (!Number.isFinite(trackId)) {\n return Response.json({ error: \"Invalid trackId\" }, { status: 400 });\n }\n\n const downloadToken = await queries.getDownloadToken(token);\n if (!downloadToken) {\n return Response.json({ error: \"Invalid download link\" }, { status: 404 });\n }\n\n const file = await findTrackFileInCatalog(queries, trackId, format);\n if (!file) {\n return Response.json({ error: \"File not found\" }, { status: 404 });\n }\n\n const ok = await queries.tokenGrantsTrack(token, trackId);\n if (!ok) {\n return Response.json({ error: \"Track not in order\" }, { status: 403 });\n }\n\n const url = await storage.getPresignedDownloadUrl(file.storageKey, {\n filename: cleanDownloadFilename(file.fileName),\n contentType: format === \"wav\" ? \"audio/wav\" : \"audio/mpeg\",\n });\n return Response.redirect(url, 302);\n };\n}\n\n/** Walk the published catalog (released to all customers) to find the file matching `trackId` + `format`. */\nasync function findTrackFileInCatalog(\n queries: Queries,\n trackId: number,\n format: string,\n): Promise<TrackFile | null> {\n const releases = await queries.listPublishedReleases();\n for (const release of releases) {\n const track = release.tracks.find((t) => t.id === trackId);\n if (!track) continue;\n const file = track.files.find((f) => f.format === format);\n return file ?? null;\n }\n return null;\n}\n\n/** Trim stray leading/trailing whitespace from a download filename while keeping its extension intact. */\nexport function cleanDownloadFilename(name: string): string {\n const dot = name.lastIndexOf(\".\");\n if (dot <= 0) return name.trim();\n return `${name.slice(0, dot).trim()}${name.slice(dot).trim()}`;\n}\n\nexport type { Queries };\n","import type { NextRequest } from \"next/server\";\nimport type {\n OrderWithItems,\n Queries,\n ReleaseWithTracks,\n TrackWithFiles,\n} from \"@gigamusic/db\";\nimport type { StorageProvider } from \"@gigamusic/storage\";\nimport type { DownloadDeps } from \"./types.js\";\nimport { cleanDownloadFilename } from \"./download.js\";\n\ninterface RouteContext {\n params: Promise<{ token: string }>;\n}\n\n/** A file destined for the zip, paired with the storage object it streams from. */\nexport interface ZipFile {\n fileName: string;\n storageKey: string;\n contentType: string;\n}\n\ninterface ZipManifest {\n zipName: string;\n files: { fileName: string; url: string }[];\n}\n\n/**\n * Discriminated result of `resolveZipBundle` — the SW-manifest handler and\n * the server-side streaming handler both call this so they produce\n * byte-identical archives. `ok: false` carries the HTTP status the caller\n * should return.\n */\nexport type ResolveZipResult =\n | { ok: true; zipName: string; files: ZipFile[] }\n | { ok: false; status: number; error: string };\n\nexport interface ResolveZipArgs {\n queries: Queries;\n token: string;\n /** Single-release download; falsy means \"whole order or selected tracks\". */\n releaseId?: number | string | null;\n /** Comma-separated track IDs for partial-order downloads. */\n trackIdsParam?: string | null;\n /** \"mp3\" or \"wav\". */\n format?: string;\n /** Optional artist-name prefix; see `DownloadDeps.zipNamePrefix`. */\n zipNamePrefix?: string;\n}\n\n/** Prepend the artist prefix when configured; otherwise return `base` untouched. */\nfunction applyZipNamePrefix(prefix: string | undefined, base: string): string {\n const trimmed = prefix?.trim();\n return trimmed ? `${trimmed} - ${base}` : base;\n}\n\n/**\n * Auth + file-list resolution for a customer's bulk download. The\n * `createDownloadZipHandler` (SW manifest path) and\n * `createDownloadZipStreamHandler` (server-side streaming path) both call\n * this so the two routes stay in lockstep on auth and on what ends up\n * in the archive.\n */\nexport async function resolveZipBundle(args: ResolveZipArgs): Promise<ResolveZipResult> {\n const { queries, token } = args;\n const format = args.format ?? \"mp3\";\n const audioContentType = format === \"wav\" ? \"audio/wav\" : \"audio/mpeg\";\n const releaseIdRaw = args.releaseId == null ? null : String(args.releaseId);\n const trackIdsParam = args.trackIdsParam ?? null;\n\n const downloadToken = await queries.getDownloadToken(token);\n if (!downloadToken) {\n return { ok: false, status: 404, error: \"Invalid download link\" };\n }\n\n const order = await queries.getOrderById(downloadToken.orderId);\n if (!order) {\n return { ok: false, status: 404, error: \"Invalid download link\" };\n }\n\n return resolveBundle({\n order,\n releaseId: releaseIdRaw,\n trackIdsParam,\n format,\n audioContentType,\n zipNamePrefix: args.zipNamePrefix,\n });\n}\n\n/**\n * Build the GET handler for the zip-manifest endpoint. Returns the JSON\n * manifest the consumer-shipped service worker (`public/sw-zip.js`)\n * consumes — the SW pipes presigned R2 URLs through\n * `client-zip` and streams the archive straight from R2 to the browser, with\n * the Vercel function never touching audio bytes.\n *\n * Query-param branches:\n * - `trackIds` set → curated track list, flat layout\n * - `releaseId` set → single-release bundle with cover art\n * - neither set → whole order (releases + à-la-carte tracks)\n */\nexport function createDownloadZipHandler(\n deps: DownloadDeps,\n): (req: NextRequest, ctx: RouteContext) => Promise<Response> {\n const { queries, storage } = deps;\n\n return async (req: NextRequest, ctx: RouteContext): Promise<Response> => {\n const { token } = await ctx.params;\n const url = new URL(req.url);\n const resolution = await resolveZipBundle({\n queries,\n token,\n releaseId: url.searchParams.get(\"releaseId\"),\n trackIdsParam: url.searchParams.get(\"trackIds\"),\n format: url.searchParams.get(\"format\") ?? \"mp3\",\n zipNamePrefix: deps.zipNamePrefix,\n });\n if (!resolution.ok) {\n return Response.json({ error: resolution.error }, { status: resolution.status });\n }\n\n const manifest: ZipManifest = {\n zipName: resolution.zipName,\n files: await presignZipFiles(storage, resolution.files),\n };\n return Response.json(manifest);\n };\n}\n\ntype Resolution =\n | { ok: true; zipName: string; files: ZipFile[] }\n | { ok: false; status: number; error: string };\n\ninterface ResolveArgs {\n order: OrderWithItems;\n releaseId: string | null;\n trackIdsParam: string | null;\n format: string;\n audioContentType: string;\n zipNamePrefix?: string;\n}\n\nfunction resolveBundle(args: ResolveArgs): Resolution {\n const { order, releaseId, trackIdsParam, format, audioContentType, zipNamePrefix } = args;\n const fmt = format.toUpperCase();\n\n if (trackIdsParam) {\n return resolveTrackList(order, trackIdsParam, format, audioContentType, fmt, zipNamePrefix);\n }\n if (releaseId) {\n const id = Number(releaseId);\n if (!Number.isFinite(id)) {\n return { ok: false, status: 404, error: \"Release not found\" };\n }\n return resolveSingleRelease(order, id, format, audioContentType, fmt, zipNamePrefix);\n }\n return resolveWholeOrder(order, format, audioContentType, fmt, zipNamePrefix);\n}\n\nfunction resolveTrackList(\n order: OrderWithItems,\n trackIdsParam: string,\n format: string,\n audioContentType: string,\n fmt: string,\n zipNamePrefix: string | undefined,\n): Resolution {\n const requestedTrackIds = trackIdsParam\n .split(\",\")\n .map((s) => Number(s.trim()))\n .filter((n) => Number.isFinite(n));\n const ownership = trackOwnership(order);\n const allOwned = requestedTrackIds.every((id) => ownership.owns(id));\n if (!allOwned) {\n return { ok: false, status: 403, error: \"Track not in order\" };\n }\n\n const files: ZipFile[] = [];\n for (const id of requestedTrackIds) {\n const ctx = ownership.locate(id);\n if (!ctx) continue;\n const file = ctx.track.files.find((f) => f.format === format);\n if (!file) continue;\n files.push({\n fileName: zipEntryPath(ctx.release?.name ?? null, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n });\n }\n if (files.length === 0) {\n return {\n ok: false,\n status: 404,\n error: \"No audio files have been uploaded for these tracks yet.\",\n };\n }\n return {\n ok: true,\n zipName: applyZipNamePrefix(zipNamePrefix, `Tracks (${fmt}).zip`),\n files,\n };\n}\n\nfunction resolveSingleRelease(\n order: OrderWithItems,\n releaseId: number,\n format: string,\n audioContentType: string,\n fmt: string,\n zipNamePrefix: string | undefined,\n): Resolution {\n const orderHasRelease = order.items.some((item) => item.releaseId === releaseId);\n if (!orderHasRelease) {\n return { ok: false, status: 403, error: \"Release not in order\" };\n }\n\n const release = order.items.find((item) => item.release?.id === releaseId)?.release;\n if (!release) {\n return { ok: false, status: 404, error: \"Release not found\" };\n }\n\n const files: ZipFile[] = release.tracks\n .map((track) => {\n const file = track.files.find((f) => f.format === format);\n if (!file) return null;\n return {\n fileName: zipEntryPath(null, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n } satisfies ZipFile;\n })\n .filter((f): f is ZipFile => f !== null);\n\n if (files.length > 0 && release.coverImageUrl) {\n files.push(...coverArtEntries(release.name, release.coverImageUrl, \"\", files));\n }\n\n if (files.length === 0) {\n return {\n ok: false,\n status: 404,\n error: \"No audio files have been uploaded for these tracks yet.\",\n };\n }\n return {\n ok: true,\n zipName: applyZipNamePrefix(zipNamePrefix, `${release.name} (${fmt}).zip`),\n files,\n };\n}\n\nfunction resolveWholeOrder(\n order: OrderWithItems,\n format: string,\n audioContentType: string,\n fmt: string,\n zipNamePrefix: string | undefined,\n): Resolution {\n const releaseFiles: ZipFile[] = [];\n const aLaCarteFiles: ZipFile[] = [];\n\n for (const item of order.items) {\n if (item.release) {\n const release = item.release;\n const entries: ZipFile[] = [];\n for (const track of release.tracks) {\n const file = track.files.find((f) => f.format === format);\n if (!file) continue;\n entries.push({\n fileName: zipEntryPath(release.name, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n });\n }\n if (entries.length > 0 && release.coverImageUrl) {\n entries.push(\n ...coverArtEntries(\n release.name,\n release.coverImageUrl,\n `${sanitizeSegment(release.name)}/`,\n entries,\n ),\n );\n }\n releaseFiles.push(...entries);\n } else if (item.track) {\n const track = item.track;\n const release = track.release ?? null;\n const file = track.files.find((f) => f.format === format);\n if (!file) continue;\n aLaCarteFiles.push({\n fileName: zipEntryPath(release?.name ?? null, file.fileName),\n storageKey: file.storageKey,\n contentType: audioContentType,\n });\n }\n }\n\n const files = [...releaseFiles, ...aLaCarteFiles];\n if (files.length === 0) {\n return {\n ok: false,\n status: 404,\n error: \"No audio files have been uploaded for these tracks yet.\",\n };\n }\n return {\n ok: true,\n zipName: applyZipNamePrefix(zipNamePrefix, `Order ${order.id} (${fmt}).zip`),\n files,\n };\n}\n\ninterface TrackContext {\n track: TrackWithFiles;\n release?: ReleaseWithTracks | null;\n}\n\n/** Index every track an order grants access to so the manifest resolver can both check ownership and locate file rows. */\nfunction trackOwnership(order: OrderWithItems) {\n const byId = new Map<number, TrackContext>();\n for (const item of order.items) {\n if (item.release) {\n for (const t of item.release.tracks) {\n byId.set(t.id, { track: t, release: item.release });\n }\n }\n if (item.track) {\n byId.set(item.track.id, {\n track: item.track,\n release: (item.track.release as ReleaseWithTracks | null | undefined) ?? null,\n });\n }\n }\n return {\n owns: (id: number) => byId.has(id),\n locate: (id: number) => byId.get(id) ?? null,\n };\n}\n\n/** Strip path separators so a release/track name can't spawn unintended zip subfolders. */\nexport function sanitizeSegment(name: string): string {\n return name.replace(/[/\\\\]+/g, \"-\").trim();\n}\n\n/**\n * Detects extended-mix tracks from their filename. The catalog marks them\n * inconsistently (\"Extended\", \"(Extended)\", \"[EXTENDED MIX]\"), so a loose\n * whole-word match on \"extended\" catches every variant.\n */\nexport function isExtendedMix(fileName: string): boolean {\n return /\\bextended\\b/i.test(fileName);\n}\n\n/**\n * Build a zip entry path: `[Release Name/][Extended/]filename`. Extended mixes\n * are nested in their own `Extended/` subfolder so they don't clutter the main\n * release listing. `releaseName` is null for single-release zips (which stay\n * flat apart from the `Extended/` split).\n */\nexport function zipEntryPath(releaseName: string | null, fileName: string): string {\n const clean = cleanDownloadFilename(fileName);\n const segments: string[] = [];\n if (releaseName) segments.push(sanitizeSegment(releaseName));\n if (isExtendedMix(clean)) segments.push(\"Extended\");\n segments.push(clean);\n return segments.join(\"/\");\n}\n\n/** Zip filename for a release's cover art. */\nexport function coverArtFilename(releaseName: string): string {\n return `${sanitizeSegment(releaseName)} - COVER ART.jpg`;\n}\n\n/**\n * Cover-art zip entries for a release: the artwork alongside the tracks, plus\n * a duplicate inside the `Extended/` subfolder when any track entry uses one.\n * `folder` is the release's `Name/` prefix for multi-release zips, or \"\" for\n * flat single-release zips.\n */\nexport function coverArtEntries(\n releaseName: string,\n coverImageUrl: string,\n folder: string,\n trackEntries: ZipFile[],\n): ZipFile[] {\n const file = coverArtFilename(releaseName);\n const entries: ZipFile[] = [\n { fileName: `${folder}${file}`, storageKey: coverImageUrl, contentType: \"image/jpeg\" },\n ];\n if (trackEntries.some((e) => e.fileName.split(\"/\").includes(\"Extended\"))) {\n entries.push({\n fileName: `${folder}Extended/${file}`,\n storageKey: coverImageUrl,\n contentType: \"image/jpeg\",\n });\n }\n return entries;\n}\n\n/** Presign every zip entry into the `{ fileName, url }` manifest shape the service worker consumes. */\nasync function presignZipFiles(\n storage: StorageProvider,\n files: ZipFile[],\n): Promise<ZipManifest[\"files\"]> {\n return Promise.all(\n files.map(async (t) => ({\n fileName: t.fileName,\n url: await storage.getPresignedDownloadUrl(t.storageKey, {\n filename: t.fileName.split(\"/\").pop() ?? t.fileName,\n contentType: t.contentType,\n }),\n })),\n );\n}\n\nexport type { Queries };\n","import type { NextRequest } from \"next/server\";\nimport { Readable, Transform } from \"node:stream\";\nimport { finished } from \"node:stream/promises\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\nimport archiver from \"archiver\";\nimport type { DownloadDeps } from \"./types.js\";\nimport { resolveZipBundle } from \"./zip.js\";\n\ninterface RouteContext {\n params: Promise<{ token: string }>;\n}\n\n/**\n * Server-side bulk-download fallback. Mirrors `createDownloadZipHandler`'s\n * auth + file-list logic via the shared `resolveZipBundle()`, but streams\n * a real zip back to the browser through `archiver` instead of returning\n * a manifest for a service worker to assemble.\n *\n * When to wire this up: in addition to `createDownloadZipHandler`. Browsers\n * that can't run the service worker reliably (WebKit — Safari desktop and\n * every iOS browser, plus private/incognito on most engines) get truncated\n * archives from the SW path. Detect those browsers at click time on the\n * client and hard-navigate here instead.\n *\n * Cost trade-off: audio bytes proxy through the serverless function and\n * cost egress, which is why the SW path stays the default for browsers\n * that handle it correctly.\n *\n * Per-file failure policy matches the SW: any failed storage fetch becomes\n * a `_FAILED_<name>.txt` placeholder so one bad object doesn't taint the\n * whole archive.\n */\nexport function createDownloadZipStreamHandler(\n deps: DownloadDeps,\n): (req: NextRequest, ctx: RouteContext) => Promise<Response> {\n const { queries, storage } = deps;\n\n return async (req: NextRequest, ctx: RouteContext): Promise<Response> => {\n const { token } = await ctx.params;\n const url = new URL(req.url);\n\n const resolution = await resolveZipBundle({\n queries,\n token,\n releaseId: url.searchParams.get(\"releaseId\"),\n trackIdsParam: url.searchParams.get(\"trackIds\"),\n format: url.searchParams.get(\"format\") ?? \"mp3\",\n zipNamePrefix: deps.zipNamePrefix,\n });\n if (!resolution.ok) {\n return Response.json({ error: resolution.error }, { status: resolution.status });\n }\n\n // `store: true` = no DEFLATE compression. MP3/WAV barely compress, and\n // the SW path serves uncompressed entries — staying consistent means\n // identical byte counts across the two routes.\n const archive = archiver(\"zip\", { store: true });\n\n archive.on(\"error\", (err) => {\n console.error(\"[zip-stream] archiver error:\", err);\n });\n\n // Tear the pipeline down if the client disconnects mid-download.\n req.signal.addEventListener(\"abort\", () => archive.abort());\n\n // Serialise storage fetches: only open file N's socket after archiver\n // has fully consumed file N-1. The naive parallel approach (open every\n // socket up front, let archiver drain them in order) leaves the later\n // sockets idle behind backpressure long enough for upstream R2/S3 to\n // time them out, surfacing as `TypeError: terminated` mid-stream. A\n // serial pipeline costs one TTFB per file transition — small compared\n // to the per-file streaming time — and archiver is the serial\n // bottleneck anyway, so end-to-end throughput is unchanged.\n (async () => {\n for (const file of resolution.files) {\n if (req.signal.aborted) return;\n const baseName = file.fileName.split(\"/\").pop() || file.fileName;\n try {\n const url = await storage.getPresignedDownloadUrl(file.storageKey, {\n filename: baseName,\n contentType: file.contentType,\n });\n const res = await fetch(url, { signal: req.signal });\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n // DOM `ReadableStream` and Node's web `ReadableStream` are\n // runtime-compatible but typed separately — cast at the boundary.\n const body = Readable.fromWeb(\n res.body as unknown as NodeReadableStream<Uint8Array>,\n );\n // Pass-through counter for two reasons: we get a meaningful\n // \"bytesReceived\" in failure logs, and `archiver.append(counter)`\n // gives us a handle the `finished()` await below can resolve on\n // (archiver's internal entry isn't exposed).\n let bytesReceived = 0;\n const counter = new Transform({\n transform(chunk: Buffer, _enc, cb) {\n bytesReceived += chunk.length;\n cb(null, chunk);\n },\n });\n // Forward upstream errors onto the counter so the `finished`\n // await rejects and the loop can append a _FAILED_ placeholder.\n body.on(\"error\", (err) => counter.destroy(err));\n body.pipe(counter);\n archive.append(counter, { name: file.fileName });\n try {\n // Resolves once both sides of `counter` are done: body has\n // ended (upstream finished) AND archiver has drained the\n // readable side (entry fully written to the zip queue).\n await finished(counter);\n } catch (err) {\n if (req.signal.aborted) return;\n const detail = err instanceof Error ? err.message : String(err);\n console.error(\n `[zip-stream] body stream error for ${file.fileName} ` +\n `(storageKey=${file.storageKey}, bytesReceived=${bytesReceived}): ${detail}`,\n );\n try {\n archive.append(\n `Streaming \"${file.fileName}\" from storage failed after ${bytesReceived} bytes: ${detail}.\\n` +\n `Try downloading the track individually from your order page.\\n`,\n { name: `_FAILED_${baseName}.txt` },\n );\n } catch (appendErr) {\n console.warn(\n `[zip-stream] could not append failure placeholder for ${file.fileName}:`,\n appendErr,\n );\n }\n }\n } catch (err) {\n if (req.signal.aborted) return;\n const detail = err instanceof Error ? err.message : String(err);\n console.warn(`[zip-stream] fetch failed for ${file.fileName}:`, detail);\n archive.append(\n `Failed to download \"${file.fileName}\" from storage: ${detail}.\\n` +\n `Try downloading the track individually from your order page.\\n`,\n { name: `_FAILED_${baseName}.txt` },\n );\n }\n }\n await archive.finalize();\n })().catch((err) => {\n console.error(\"[zip-stream] pipeline error:\", err);\n archive.abort();\n });\n\n return new Response(Readable.toWeb(archive) as ReadableStream<Uint8Array>, {\n headers: {\n \"Content-Type\": \"application/zip\",\n \"Content-Disposition\": `attachment; filename=\"${resolution.zipName.replace(/\"/g, \"\")}\"`,\n \"Cache-Control\": \"no-store\",\n },\n });\n };\n}\n","const ERROR_HTML = `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"color-scheme\" content=\"light dark\">\n <title>Bulk download couldn't start</title>\n <style>\n body { font-family: system-ui, -apple-system, sans-serif; max-width: 560px; margin: 3rem auto; padding: 0 1.25rem; line-height: 1.55; }\n h1 { font-size: 1.35rem; margin-bottom: 0.75rem; }\n ul { padding-left: 1.25rem; }\n li { margin: 0.35rem 0; }\n a { color: #6366f1; }\n </style>\n</head>\n<body>\n <h1>Bulk download couldn't start</h1>\n <p>Your browser couldn't generate the zip in the background. This usually happens in private/incognito mode, or after the order page has been open for a long time.</p>\n <p>To get your music:</p>\n <ul>\n <li>Go back to your order page and use the individual track download buttons.</li>\n <li>Or reopen the order link in a fresh tab and try the bulk download again.</li>\n </ul>\n <p><a href=\"javascript:history.back()\">&larr; Back to your order</a></p>\n</body>\n</html>`;\n\n/**\n * Fallback for `/sw-zip/[...path]` reached only when the consumer-shipped\n * service worker (`public/sw-zip.js`) fails to intercept the\n * navigation. Without this route the browser would save Next.js's 404\n * HTML under the URL's `.zip` filename — and macOS Archive Utility\n * surfaces that as the confusing \"Error 79 - Inappropriate file type or\n * format.\" Returning `text/html` (and no `Content-Disposition`) makes\n * the browser render the message instead of saving it.\n *\n * Wire up at `app/sw-zip/[...path]/route.ts`:\n *\n * ```ts\n * export const GET = createSwZipFallbackHandler();\n * ```\n */\nexport function createSwZipFallbackHandler(): () => Response {\n return () => {\n // Hitting this route is always a failure mode — the SW should have\n // intercepted. Log so the frequency shows up in serverless logs.\n console.warn(\"[sw-zip] fallback route hit — service worker did not intercept\");\n return new Response(ERROR_HTML, {\n status: 503,\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n },\n });\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gigamusic/checkout",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Next.js route-handler factories for Stripe Checkout, Stripe webhooks, and presigned-URL downloads. Stripe is hard-wired; all secrets enter as factory args.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,10 +30,10 @@
30
30
  "dependencies": {
31
31
  "archiver": "^7.0.1",
32
32
  "stripe": "^22.0.0",
33
- "@gigamusic/core": "3.0.0",
34
- "@gigamusic/db": "4.0.0",
33
+ "@gigamusic/core": "4.2.0",
35
34
  "@gigamusic/storage": "3.0.0",
36
- "@gigamusic/email": "3.0.0"
35
+ "@gigamusic/db": "4.2.0",
36
+ "@gigamusic/email": "4.2.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "next": ">=15"
package/src/checkout.ts CHANGED
@@ -3,6 +3,34 @@ import Stripe from "stripe";
3
3
  import type { ReleaseWithTracks, TrackWithFiles } from "@gigamusic/db";
4
4
  import type { CheckoutCartItem, CheckoutDeps } from "./types.js";
5
5
 
6
+ /**
7
+ * Stripe rejects card charges below 50 cents (USD). A pay-what-you-want
8
+ * override below this is clamped up rather than rejected, so a customer who
9
+ * types a token amount still completes checkout instead of hitting an opaque
10
+ * Stripe error. A `0` override never reaches here — the consumer routes free
11
+ * downloads through its own gate, not the cart.
12
+ */
13
+ const STRIPE_MIN_CHARGE_CENTS = 50;
14
+
15
+ /**
16
+ * Resolve the per-line `unit_amount`: a positive pay-what-you-want override
17
+ * (clamped up to the Stripe minimum) when supplied, otherwise the catalog
18
+ * price. Non-positive / non-finite overrides fall back to the catalog price.
19
+ */
20
+ function resolveUnitAmount(
21
+ overrideCents: number | undefined,
22
+ catalogCents: number,
23
+ ): number {
24
+ if (
25
+ typeof overrideCents === "number" &&
26
+ Number.isFinite(overrideCents) &&
27
+ overrideCents > 0
28
+ ) {
29
+ return Math.max(STRIPE_MIN_CHARGE_CENTS, Math.round(overrideCents));
30
+ }
31
+ return catalogCents;
32
+ }
33
+
6
34
  /**
7
35
  * Build a POST handler that translates `{ items }` from the cart into a Stripe
8
36
  * Checkout Session and replies with `{ url }`. Release/track metadata is
@@ -99,6 +127,35 @@ export function createCheckoutHandler(
99
127
  return jsonError("No valid items found", 400);
100
128
  }
101
129
 
130
+ // Pay-what-you-want overrides, keyed by id. Items without `priceCents`
131
+ // (the fixed-price default — every thomas-adagio cart) leave the map
132
+ // empty and fall back to the catalog price below.
133
+ const releaseAmountOverride = new Map<number, number>(
134
+ items
135
+ .filter(
136
+ (i) =>
137
+ i.kind === "release" &&
138
+ i.id != null &&
139
+ typeof i.priceCents === "number",
140
+ )
141
+ .map((i) => [i.id as number, i.priceCents as number]),
142
+ );
143
+ const trackAmountOverride = new Map<number, number>(
144
+ items
145
+ .filter(
146
+ (i) =>
147
+ i.kind === "track" &&
148
+ i.id != null &&
149
+ typeof i.priceCents === "number",
150
+ )
151
+ .map((i) => [i.id as number, i.priceCents as number]),
152
+ );
153
+
154
+ // Charged `unit_amount` per line, keyed `r<id>` / `t<id>`. Stamped onto
155
+ // the session so the webhook records the true price paid rather than
156
+ // re-deriving the (suggested) catalog price.
157
+ const chargedAmounts: Record<string, number> = {};
158
+
102
159
  // Single `listPublishedReleases` call sources both release and track
103
160
  // line items. Tracks live inside their release, so one fetch keeps the
104
161
  // ID/price/cover lookups consistent without a second round-trip.
@@ -117,19 +174,23 @@ export function createCheckoutHandler(
117
174
  const releaseLineItems = releaseIds
118
175
  .map((id) => releaseById.get(id))
119
176
  .filter((r): r is ReleaseWithTracks => r !== undefined)
120
- .map((r) => ({
121
- price_data: {
122
- currency,
123
- product_data: {
124
- name: r.name,
125
- ...(r.coverImageUrl
126
- ? { images: [toAbsoluteUrl(r.coverImageUrl, baseUrl)] }
127
- : {}),
177
+ .map((r) => {
178
+ const amount = resolveUnitAmount(releaseAmountOverride.get(r.id), r.price);
179
+ chargedAmounts[`r${r.id}`] = amount;
180
+ return {
181
+ price_data: {
182
+ currency,
183
+ product_data: {
184
+ name: r.name,
185
+ ...(r.coverImageUrl
186
+ ? { images: [toAbsoluteUrl(r.coverImageUrl, baseUrl)] }
187
+ : {}),
188
+ },
189
+ unit_amount: amount,
128
190
  },
129
- unit_amount: r.price,
130
- },
131
- quantity: 1 as const,
132
- }));
191
+ quantity: 1 as const,
192
+ };
193
+ });
133
194
 
134
195
  const trackLineItems = trackIds
135
196
  .map((id) => trackContext.get(id))
@@ -137,19 +198,26 @@ export function createCheckoutHandler(
137
198
  (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>
138
199
  c !== undefined,
139
200
  )
140
- .map(({ track, release }) => ({
141
- price_data: {
142
- currency,
143
- product_data: {
144
- name: `${release.name} — ${track.name}`,
145
- ...(release.coverImageUrl
146
- ? { images: [toAbsoluteUrl(release.coverImageUrl, baseUrl)] }
147
- : {}),
201
+ .map(({ track, release }) => {
202
+ const amount = resolveUnitAmount(
203
+ trackAmountOverride.get(track.id),
204
+ track.price,
205
+ );
206
+ chargedAmounts[`t${track.id}`] = amount;
207
+ return {
208
+ price_data: {
209
+ currency,
210
+ product_data: {
211
+ name: `${release.name} — ${track.name}`,
212
+ ...(release.coverImageUrl
213
+ ? { images: [toAbsoluteUrl(release.coverImageUrl, baseUrl)] }
214
+ : {}),
215
+ },
216
+ unit_amount: amount,
148
217
  },
149
- unit_amount: track.price,
150
- },
151
- quantity: 1 as const,
152
- }));
218
+ quantity: 1 as const,
219
+ };
220
+ });
153
221
 
154
222
  const lineItems = [...releaseLineItems, ...trackLineItems];
155
223
  if (lineItems.length === 0) {
@@ -170,6 +238,7 @@ export function createCheckoutHandler(
170
238
  site,
171
239
  release_ids: JSON.stringify(resolvedReleaseIds),
172
240
  track_ids: JSON.stringify(resolvedTrackIds),
241
+ amounts: JSON.stringify(chargedAmounts),
173
242
  },
174
243
  success_url: successUrl,
175
244
  cancel_url: cancelUrl,
package/src/types.ts CHANGED
@@ -1,12 +1,30 @@
1
1
  import type Stripe from "stripe";
2
2
  import type { Queries } from "@gigamusic/db";
3
3
  import type { StorageProvider } from "@gigamusic/storage";
4
- import type { EmailProvider, EmailBranding } from "@gigamusic/email";
4
+ import type {
5
+ EmailProvider,
6
+ EmailBranding,
7
+ PurchaseConfirmationArgs,
8
+ } from "@gigamusic/email";
9
+
10
+ /** Rendered email: a subject line and an HTML body. */
11
+ export interface RenderedEmail {
12
+ subject: string;
13
+ html: string;
14
+ }
5
15
 
6
16
  /** Cart item shape accepted by `createCheckoutHandler`. `id` is required for "release" / "track"; ignored for "catalog". */
7
17
  export interface CheckoutCartItem {
8
18
  kind: "release" | "track" | "catalog";
9
19
  id?: number;
20
+ /**
21
+ * Pay-what-you-want override, in cents. When present and ≥ the Stripe card
22
+ * minimum, the customer is charged this instead of the catalog price — the
23
+ * handler clamps it up to the minimum and stamps the charged amount onto the
24
+ * session so the webhook can record the true per-line price. Omit it (the
25
+ * fixed-price default) and the catalog `price` is used. Ignored for "catalog".
26
+ */
27
+ priceCents?: number;
10
28
  }
11
29
 
12
30
  export interface CheckoutDeps {
@@ -65,8 +83,25 @@ export interface WebhookDeps {
65
83
  baseUrl: string;
66
84
  /** JWT secret for the magic-link order-verification token mailed to the customer. */
67
85
  orderTokenSecret: string;
86
+ /**
87
+ * Lifetime of the order magic-link token embedded in the confirmation email.
88
+ * Accepts any `jose` duration string (e.g. `"1h"`, `"30d"`). Pass `null` for
89
+ * a link that never expires. Defaults to `null` — the emailed link is the
90
+ * customer's durable path back to their downloads, so it should keep working.
91
+ */
92
+ orderTokenExpiry?: string | null;
68
93
  /** Defaults to "/orders/verify". */
69
94
  verifyPath?: string;
95
+ /**
96
+ * Override the purchase-confirmation email rendering. When provided, it's
97
+ * called instead of the built-in `renderPurchaseConfirmation`, giving the
98
+ * consumer full control over the email's subject and HTML — styling, layout,
99
+ * logo, copy — using the same data the default template receives. May be
100
+ * sync or async.
101
+ */
102
+ renderConfirmation?: (
103
+ args: PurchaseConfirmationArgs,
104
+ ) => RenderedEmail | Promise<RenderedEmail>;
70
105
  }
71
106
 
72
107
  export interface DownloadDeps {
package/src/webhook.ts CHANGED
@@ -49,7 +49,9 @@ export function createStripeWebhookHandler(
49
49
  emailFrom,
50
50
  baseUrl,
51
51
  orderTokenSecret,
52
+ orderTokenExpiry = null,
52
53
  verifyPath = "/orders/verify",
54
+ renderConfirmation = renderPurchaseConfirmation,
53
55
  } = deps;
54
56
 
55
57
  if (!stripeOverride && !stripeSecret) {
@@ -95,6 +97,10 @@ export function createStripeWebhookHandler(
95
97
 
96
98
  const releaseIds = safeJsonIdList(session.metadata?.release_ids);
97
99
  const trackIds = safeJsonIdList(session.metadata?.track_ids);
100
+ // Per-line amounts the customer actually paid, keyed `r<id>` / `t<id>`,
101
+ // stamped by `createCheckoutHandler`. Absent on pre-upgrade sessions and
102
+ // on catalog purchases — those fall back to the catalog price below.
103
+ const chargedAmounts = safeAmountMap(session.metadata?.amounts);
98
104
 
99
105
  if (releaseIds.length === 0 && trackIds.length === 0) {
100
106
  return Response.json({ received: true });
@@ -125,8 +131,14 @@ export function createStripeWebhookHandler(
125
131
  );
126
132
 
127
133
  const orderItems = [
128
- ...matchedReleases.map((r) => ({ releaseId: r.id, price: r.price })),
129
- ...matchedTracks.map(({ track }) => ({ trackId: track.id, price: track.price })),
134
+ ...matchedReleases.map((r) => ({
135
+ releaseId: r.id,
136
+ price: chargedAmounts[`r${r.id}`] ?? r.price,
137
+ })),
138
+ ...matchedTracks.map(({ track }) => ({
139
+ trackId: track.id,
140
+ price: chargedAmounts[`t${track.id}`] ?? track.price,
141
+ })),
130
142
  ];
131
143
 
132
144
  const customerEmail = session.customer_details?.email ?? "";
@@ -173,9 +185,10 @@ export function createStripeWebhookHandler(
173
185
  orderId: String(recordedOrder.id),
174
186
  email: customerEmail,
175
187
  secret: orderTokenSecret,
188
+ expiresIn: orderTokenExpiry,
176
189
  });
177
190
  const verifyUrl = `${baseUrl}${verifyPath}?token=${verifyToken}`;
178
- const { subject, html } = renderPurchaseConfirmation({
191
+ const { subject, html } = await renderConfirmation({
179
192
  branding,
180
193
  verifyUrl,
181
194
  itemNames,
@@ -199,6 +212,27 @@ export function createStripeWebhookHandler(
199
212
  };
200
213
  }
201
214
 
215
+ /**
216
+ * Parse the JSON-stringified `{ "r<id>" | "t<id>": cents }` amount map from
217
+ * Stripe metadata, returning `{}` on any failure. Non-finite values are
218
+ * dropped so a malformed entry falls back to the catalog price at the call site.
219
+ */
220
+ function safeAmountMap(value: string | undefined | null): Record<string, number> {
221
+ if (!value) return {};
222
+ try {
223
+ const parsed: unknown = JSON.parse(value);
224
+ if (typeof parsed !== "object" || parsed === null) return {};
225
+ const out: Record<string, number> = {};
226
+ for (const [key, raw] of Object.entries(parsed as Record<string, unknown>)) {
227
+ const n = Number(raw);
228
+ if (Number.isFinite(n)) out[key] = n;
229
+ }
230
+ return out;
231
+ } catch {
232
+ return {};
233
+ }
234
+ }
235
+
202
236
  /** Parse a JSON-stringified id array from Stripe metadata, returning `[]` on any failure. NaN entries are dropped. */
203
237
  function safeJsonIdList(value: string | undefined | null): number[] {
204
238
  if (!value) return [];