@gigamusic/checkout 4.6.0 → 4.7.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/README.md CHANGED
@@ -38,28 +38,45 @@ export const POST = createCheckoutHandler({
38
38
  });
39
39
  ```
40
40
 
41
+ A bundle's members can also be individual tracks — a "bundle of singles" —
42
+ via `trackIds`. Both member lists are optional; supply at least one, and mix
43
+ them freely if that's the product you're selling:
44
+
45
+ ```ts
46
+ return {
47
+ totalCents: bundle.discountedPrice,
48
+ productName: `${bundle.name} (${bundle.trackIds.length} songs)`,
49
+ trackIds: bundle.trackIds,
50
+ };
51
+ ```
52
+
41
53
  As with the catalog, the consumer owns pricing math. The handler:
42
54
 
43
55
  - clamps `totalCents` up to the Stripe card minimum;
44
- - resolves `releaseIds` against `listPublishedReleases()`, dropping any that
45
- aren't published;
56
+ - resolves `releaseIds` against `listPublishedReleases()` and `trackIds`
57
+ against those releases' tracks, dropping any that aren't published — a track
58
+ whose release is unpublished goes the same way as an unpublished release;
46
59
  - **apportions** the charged total across the surviving members, proportional
47
60
  to list price with largest-remainder rounding, so the `amounts` metadata sums
48
61
  to exactly what Stripe charged — and therefore so do the `order_items` rows
49
- `fulfillCheckoutSession` writes;
62
+ `fulfillCheckoutSession` writes. Releases and tracks apportion as one set, so
63
+ a mixed bundle is split once rather than rounded twice;
50
64
  - stamps `bundle_ids` onto the session for support. Fulfillment ignores it:
51
- bundles decompose into per-release order items, so downloads and order
52
- history need no special handling.
65
+ bundles decompose into per-release / per-track order items, so downloads and
66
+ order history need no special handling.
53
67
 
54
68
  Bundles compose with loose `release` / `track` items in a single session. A
55
- release charged by both a bundle and a loose line yields one deduped order item
56
- whose price is the sum.
69
+ release or track charged by both a bundle and a loose line yields one deduped
70
+ order item whose price is the sum.
71
+
72
+ A track has no cover of its own, so a track-only bundle's line item shows the
73
+ cover of the release its first member belongs to.
57
74
 
58
75
  Two rejections worth handling in the cart UI:
59
76
 
60
77
  | Case | Response |
61
78
  | --- | --- |
62
- | Resolver returns `null`, or every member is unpublished | `409 { error: "bundle-unavailable", bundleIds }` |
79
+ | Resolver returns `null`, or every member (of either kind) is unpublished | `409 { error: "bundle-unavailable", bundleIds }` |
63
80
  | Cart mixes `kind: "catalog"` with `kind: "bundle"` | `400` |
64
81
 
65
82
  The 409 is the expected path when an admin deletes or unpublishes a bundle still
package/dist/index.d.ts CHANGED
@@ -73,9 +73,10 @@ interface CheckoutDeps {
73
73
  *
74
74
  * Called once per distinct `bundleId` in the cart. As with the catalog, the
75
75
  * consumer owns pricing math: hand back the final `totalCents` and the
76
- * member `releaseIds`, and the handler apportions the total across those
77
- * members (proportional to list price, largest-remainder) so the recorded
78
- * order items sum to exactly what Stripe charged.
76
+ * members (`releaseIds`, `trackIds`, or both), and the handler apportions
77
+ * the total across those members (proportional to list price,
78
+ * largest-remainder) so the recorded order items sum to exactly what Stripe
79
+ * charged.
79
80
  *
80
81
  * Resolving to `null`/`undefined` means "no such bundle" — the handler
81
82
  * replies 409 `{ error: "bundle-unavailable", bundleIds }` so the cart can
@@ -84,7 +85,14 @@ interface CheckoutDeps {
84
85
  */
85
86
  bundlePurchase?: (bundleId: string) => Promise<ResolvedBundle | null | undefined> | ResolvedBundle | null | undefined;
86
87
  }
87
- /** What `bundlePurchase` returns for a bundle the consumer recognises. */
88
+ /**
89
+ * What `bundlePurchase` returns for a bundle the consumer recognises.
90
+ *
91
+ * A bundle's members are `releaseIds`, `trackIds`, or both — the package
92
+ * imposes no rule about mixing them, so a consumer is free to model bundles as
93
+ * releases-only, singles-only, or a combination. Supply at least one member
94
+ * list; a bundle that resolves to no surviving member is reported unavailable.
95
+ */
88
96
  interface ResolvedBundle {
89
97
  /** Final charged price for the whole bundle, in cents. Clamped up to the Stripe card minimum. */
90
98
  totalCents: number;
@@ -92,10 +100,16 @@ interface ResolvedBundle {
92
100
  productName: string;
93
101
  /**
94
102
  * Member release ids. Resolved against `listPublishedReleases()` — ids that
95
- * aren't published are dropped, and a bundle left with none is treated as
96
- * unavailable rather than charging for an order with no items.
103
+ * aren't published are dropped, and a bundle left with no members at all is
104
+ * treated as unavailable rather than charging for an order with no items.
105
+ */
106
+ releaseIds?: number[];
107
+ /**
108
+ * Member track ids — individual songs, for a "bundle of singles". Resolved
109
+ * against the tracks of `listPublishedReleases()`, so a track whose release
110
+ * is unpublished is dropped on the same terms as an unpublished release.
97
111
  */
98
- releaseIds: number[];
112
+ trackIds?: number[];
99
113
  }
100
114
  /** What `fulfillCheckoutSession` needs to turn a paid Stripe session into an order row. */
101
115
  interface FulfillSessionDeps {
package/dist/index.js CHANGED
@@ -24,13 +24,13 @@ function apportion(totalCents, members) {
24
24
  const exact = weights.map((w) => total * w);
25
25
  const base = exact.map((v) => Math.floor(v));
26
26
  let remainder = total - base.reduce((s, v) => s + v, 0);
27
- const order = members.map((m, i) => ({ i, frac: exact[i] - base[i], price: m.price, id: m.id })).sort((a, b) => b.frac - a.frac || b.price - a.price || a.id - b.id);
27
+ const order = members.map((m, i) => ({ i, frac: exact[i] - base[i], price: m.price, key: m.key })).sort((a, b) => b.frac - a.frac || b.price - a.price || a.key.localeCompare(b.key));
28
28
  for (const { i } of order) {
29
29
  if (remainder <= 0) break;
30
30
  base[i] += 1;
31
31
  remainder -= 1;
32
32
  }
33
- members.forEach((m, i) => result.set(m.id, base[i]));
33
+ members.forEach((m, i) => result.set(m.key, base[i]));
34
34
  return result;
35
35
  }
36
36
  function createCheckoutHandler(deps) {
@@ -161,12 +161,16 @@ function createCheckoutHandler(deps) {
161
161
  };
162
162
  });
163
163
  const bundleLineItems = [];
164
- const bundleMemberIds = [];
164
+ const bundleReleaseMemberIds = [];
165
+ const bundleTrackMemberIds = [];
165
166
  const unavailableBundleIds = [];
166
167
  for (const bundleId of bundleIds) {
167
168
  const resolved = await bundlePurchase(bundleId);
168
- const members = (resolved?.releaseIds ?? []).map((id) => releaseById.get(id)).filter((r) => r !== void 0);
169
- if (!resolved || members.length === 0) {
169
+ const releaseMembers = (resolved?.releaseIds ?? []).map((id) => releaseById.get(id)).filter((r) => r !== void 0);
170
+ const trackMembers = (resolved?.trackIds ?? []).map((id) => trackContext.get(id)).filter(
171
+ (c) => c !== void 0
172
+ );
173
+ if (!resolved || releaseMembers.length + trackMembers.length === 0) {
170
174
  unavailableBundleIds.push(bundleId);
171
175
  continue;
172
176
  }
@@ -174,7 +178,7 @@ function createCheckoutHandler(deps) {
174
178
  STRIPE_MIN_CHARGE_CENTS,
175
179
  Math.round(resolved.totalCents)
176
180
  );
177
- const cover = members.find((m) => m.coverImageUrl)?.coverImageUrl;
181
+ const cover = releaseMembers.find((m) => m.coverImageUrl)?.coverImageUrl ?? trackMembers.find((c) => c.release.coverImageUrl)?.release.coverImageUrl;
178
182
  bundleLineItems.push({
179
183
  price_data: {
180
184
  currency,
@@ -186,13 +190,14 @@ function createCheckoutHandler(deps) {
186
190
  },
187
191
  quantity: 1
188
192
  });
189
- for (const [id, cents] of apportion(
190
- total,
191
- members.map((m) => ({ id: m.id, price: m.price }))
192
- )) {
193
- chargedAmounts[`r${id}`] = (chargedAmounts[`r${id}`] ?? 0) + cents;
194
- bundleMemberIds.push(id);
193
+ for (const [key, cents] of apportion(total, [
194
+ ...releaseMembers.map((m) => ({ key: `r${m.id}`, price: m.price })),
195
+ ...trackMembers.map(({ track }) => ({ key: `t${track.id}`, price: track.price }))
196
+ ])) {
197
+ chargedAmounts[key] = (chargedAmounts[key] ?? 0) + cents;
195
198
  }
199
+ bundleReleaseMemberIds.push(...releaseMembers.map((m) => m.id));
200
+ bundleTrackMemberIds.push(...trackMembers.map(({ track }) => track.id));
196
201
  }
197
202
  if (unavailableBundleIds.length > 0) {
198
203
  return Response.json(
@@ -207,10 +212,15 @@ function createCheckoutHandler(deps) {
207
212
  const resolvedReleaseIds = [
208
213
  .../* @__PURE__ */ new Set([
209
214
  ...releaseLineItems.length ? releaseIds.filter((id) => releaseById.has(id)) : [],
210
- ...bundleMemberIds
215
+ ...bundleReleaseMemberIds
216
+ ])
217
+ ];
218
+ const resolvedTrackIds = [
219
+ .../* @__PURE__ */ new Set([
220
+ ...trackLineItems.length ? trackIds.filter((id) => trackContext.has(id)) : [],
221
+ ...bundleTrackMemberIds
211
222
  ])
212
223
  ];
213
- const resolvedTrackIds = trackLineItems.length ? [...new Set(trackIds.filter((id) => trackContext.has(id)))] : [];
214
224
  const session = await stripe.checkout.sessions.create({
215
225
  mode: "payment",
216
226
  line_items: lineItems,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/checkout.ts","../src/fulfill.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 * Split a bundle's charged total across its member releases, proportional to\n * list price, using largest-remainder allocation.\n *\n * The returned values sum to `totalCents` exactly. That's load-bearing: they\n * become the `amounts` metadata `fulfillCheckoutSession` turns into\n * `order_items.price` rows, and `sum(order_items) === orders.amountTotal` is\n * what refunds and admin order views rely on. Ties break deterministically\n * (remainder desc, price desc, id asc) so a retry apportions identically.\n */\nfunction apportion(\n totalCents: number,\n members: { id: number; price: number }[],\n): Map<number, number> {\n const result = new Map<number, number>();\n if (members.length === 0) return result;\n\n const total = Math.max(0, Math.round(totalCents));\n const sum = members.reduce((s, m) => s + Math.max(0, m.price), 0);\n // All-free members carry no weight to divide by, so split evenly.\n const weights =\n sum > 0\n ? members.map((m) => Math.max(0, m.price) / sum)\n : members.map(() => 1 / members.length);\n\n const exact = weights.map((w) => total * w);\n const base = exact.map((v) => Math.floor(v));\n let remainder = total - base.reduce((s, v) => s + v, 0);\n\n const order = members\n .map((m, i) => ({ i, frac: exact[i]! - base[i]!, price: m.price, id: m.id }))\n .sort((a, b) => b.frac - a.frac || b.price - a.price || a.id - b.id);\n\n for (const { i } of order) {\n if (remainder <= 0) break;\n base[i]! += 1;\n remainder -= 1;\n }\n\n members.forEach((m, i) => result.set(m.id, base[i]!));\n return result;\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 bundlePurchase,\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 const bundleIds = [\n ...new Set(\n items\n .filter((i) => i.kind === \"bundle\" && typeof i.bundleId === \"string\")\n .map((i) => i.bundleId as string),\n ),\n ];\n\n // The catalog already contains every bundle, so charging for both would\n // bill the same releases twice.\n if (isCatalogPurchase && bundleIds.length > 0) {\n return jsonError(\"Cannot mix a catalog purchase with a bundle\", 400);\n }\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 && bundleIds.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n if (bundleIds.length > 0 && typeof bundlePurchase !== \"function\") {\n return jsonError(\"Bundle purchases are not configured\", 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 // Accumulate: the same release can be charged by more than one line\n // (a bundle plus a loose copy in a stale cart), and the recorded\n // order item must carry the total actually paid for it.\n chargedAmounts[`r${r.id}`] = (chargedAmounts[`r${r.id}`] ?? 0) + 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}`] = (chargedAmounts[`t${track.id}`] ?? 0) + 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 // Bundles resolve against the same published-release snapshot as the\n // loose lines, so member prices, names, and covers stay consistent.\n const bundleLineItems: typeof releaseLineItems = [];\n const bundleMemberIds: number[] = [];\n const unavailableBundleIds: string[] = [];\n\n for (const bundleId of bundleIds) {\n const resolved = await bundlePurchase!(bundleId);\n const members = (resolved?.releaseIds ?? [])\n .map((id) => releaseById.get(id))\n .filter((r): r is ReleaseWithTracks => r !== undefined);\n\n // No such bundle, or nothing in it is published any more. Charging for\n // the latter would produce a paid order with zero items.\n if (!resolved || members.length === 0) {\n unavailableBundleIds.push(bundleId);\n continue;\n }\n\n const total = Math.max(\n STRIPE_MIN_CHARGE_CENTS,\n Math.round(resolved.totalCents),\n );\n const cover = members.find((m) => m.coverImageUrl)?.coverImageUrl;\n bundleLineItems.push({\n price_data: {\n currency,\n product_data: {\n name: resolved.productName,\n ...(cover ? { images: [toAbsoluteUrl(cover, baseUrl)] } : {}),\n },\n unit_amount: total,\n },\n quantity: 1 as const,\n });\n\n for (const [id, cents] of apportion(\n total,\n members.map((m) => ({ id: m.id, price: m.price })),\n )) {\n chargedAmounts[`r${id}`] = (chargedAmounts[`r${id}`] ?? 0) + cents;\n bundleMemberIds.push(id);\n }\n }\n\n if (unavailableBundleIds.length > 0) {\n return Response.json(\n { error: \"bundle-unavailable\", bundleIds: unavailableBundleIds },\n { status: 409 },\n );\n }\n\n const lineItems = [...bundleLineItems, ...releaseLineItems, ...trackLineItems];\n if (lineItems.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n // Deduped: a release reachable from both a bundle and a loose line still\n // yields one order item, whose price is the summed `amounts` entry.\n const resolvedReleaseIds = [\n ...new Set([\n ...(releaseLineItems.length ? releaseIds.filter((id) => releaseById.has(id)) : []),\n ...bundleMemberIds,\n ]),\n ];\n const resolvedTrackIds = trackLineItems.length\n ? [...new Set(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 // Ignored by fulfillment — bundles decompose into per-release order\n // items — but invaluable when supporting \"what did I actually buy?\".\n ...(bundleIds.length > 0 ? { bundle_ids: JSON.stringify(bundleIds) } : {}),\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 type Stripe from \"stripe\";\nimport { signOrderToken } from \"@gigamusic/core\";\nimport {\n isUniqueConstraintError,\n type OrderWithItems,\n type ReleaseWithTracks,\n type TrackWithFiles,\n} from \"@gigamusic/db\";\nimport { renderPurchaseConfirmation } from \"@gigamusic/email\";\nimport type { FulfillSessionDeps, PurchaseConfirmationDeps } from \"./types.js\";\n\nexport type FulfillSessionResult =\n /** This call wrote the order row. The caller owns sending the confirmation email. */\n | {\n status: \"created\";\n order: OrderWithItems;\n itemNames: string[];\n customerEmail: string;\n totalCents: number;\n }\n /**\n * The order was already on disk — either the pre-flight lookup found it or a\n * concurrent writer won the unique-constraint race. `order` is null only if\n * the re-read after a lost race somehow came back empty.\n */\n | { status: \"already-recorded\"; order: OrderWithItems | null }\n /** Nothing was written and nothing is owed. */\n | { status: \"skipped\"; reason: \"site-mismatch\" | \"no-items\" | \"unpaid\" };\n\n/**\n * Turn a completed Stripe Checkout Session into a persisted order, idempotently.\n *\n * This is the single fulfillment path shared by the Stripe webhook and any\n * consumer that wants to fulfill synchronously — e.g. a post-checkout success\n * page that would otherwise have to wait on webhook delivery to show download\n * links. Whoever calls first writes the row; everyone after gets\n * `already-recorded`. That guarantee rests on the unique constraint over\n * `stripeSessionId` inside `recordCompletedOrder`, not on call ordering, so\n * concurrent callers are safe.\n *\n * Only `status: \"created\"` should trigger side effects (the confirmation\n * email) — that status is returned to exactly one caller per session.\n *\n * ### Published-only resolution (gotcha)\n *\n * Release/track IDs from `session.metadata` are resolved against\n * `queries.listPublishedReleases()`. Anything not in the published catalog at\n * fulfillment time is silently dropped from the persisted order's line items —\n * the customer is still charged, but the order row doesn't include the\n * 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 here are:\n * - an admin unpublishes between session-create and fulfillment\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 is a realistic concern, swap the resolution call here to\n * `listAllReleases()` (or wrap with a \"warn + fallback\" branch) — but be aware\n * that doing so will surface unpublished items in admin order views.\n */\nexport async function fulfillCheckoutSession(\n session: Stripe.Checkout.Session,\n deps: FulfillSessionDeps,\n): Promise<FulfillSessionResult> {\n const { queries, site } = deps;\n\n // One Stripe account fans every event out to every registered endpoint, and\n // a success URL can be hand-forged. Ignore sessions belonging to a different\n // site so we never record a foreign purchase (with zero matched line items)\n // under this one.\n if (session.metadata?.site !== site) {\n return { status: \"skipped\", reason: \"site-mismatch\" };\n }\n\n // Delayed-notification methods (bank debits, vouchers) complete checkout with\n // the money still in flight; the session only flips to `paid` later, via\n // `checkout.session.async_payment_succeeded`. Handing over downloads at that\n // point would be giving away unpaid goods. `no_payment_required` (fully\n // discounted) and legacy sessions with no `payment_status` are fulfillable.\n if (session.payment_status === \"unpaid\") {\n return { status: \"skipped\", reason: \"unpaid\" };\n }\n\n const existing = await queries.getOrderByStripeSessionId(session.id);\n if (existing) {\n return { status: \"already-recorded\", order: existing };\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 on\n // 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 { status: \"skipped\", reason: \"no-items\" };\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 } => 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 const totalCents = session.amount_total ?? 0;\n\n let order: OrderWithItems;\n try {\n order = await queries.recordCompletedOrder({\n stripeSessionId: session.id,\n stripePaymentId:\n typeof session.payment_intent === \"string\" ? session.payment_intent : null,\n email: customerEmail,\n amountTotal: totalCents,\n items: orderItems,\n });\n } catch (err) {\n // Two callers can both pass the pre-flight lookup and race here — a webhook\n // retry, or the success page rendering while the webhook is mid-flight. The\n // unique constraint on `stripeSessionId` guarantees only one row lands; the\n // loser surfaces as a 23505 unique-violation. Re-read so the loser still\n // gets the order to render, but without claiming the \"created\" side effects.\n if (isUniqueConstraintError(err)) {\n return {\n status: \"already-recorded\",\n order: (await queries.getOrderByStripeSessionId(session.id)) ?? null,\n };\n }\n throw err;\n }\n\n const itemNames = [\n ...matchedReleases.map((r) => r.name),\n ...matchedTracks.map(({ track, release }) => `${release.name} — ${track.name}`),\n ];\n\n return { status: \"created\", order, itemNames, customerEmail, totalCents };\n}\n\n/**\n * Render and send the post-purchase confirmation email carrying the durable\n * magic link back to the customer's downloads.\n *\n * Errors are logged, never thrown: the order is already recorded and paid for,\n * so a mail-provider hiccup must not fail the caller (a webhook returning 500\n * would make Stripe retry a fulfillment that already succeeded). Callers\n * typically wrap this in `after()` so the response isn't blocked on it.\n */\nexport async function sendPurchaseConfirmation(\n args: {\n orderId: number;\n customerEmail: string;\n itemNames: string[];\n totalCents: number;\n },\n deps: PurchaseConfirmationDeps,\n): Promise<void> {\n const {\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 (!args.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 `[gigamusic/checkout] order ${args.orderId} has no customer email — recorded, but the customer cannot retrieve downloads.`,\n );\n return;\n }\n\n try {\n const verifyToken = await signOrderToken({\n orderId: String(args.orderId),\n email: args.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: args.itemNames,\n totalCents: args.totalCents,\n });\n await email.send({ from: emailFrom, to: args.customerEmail, subject, html });\n } catch (err) {\n console.error(\n `[gigamusic/checkout] failed to send purchase confirmation to ${args.customerEmail} for order ${args.orderId}:`,\n err,\n );\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 { after, type NextRequest } from \"next/server\";\nimport Stripe from \"stripe\";\nimport { fulfillCheckoutSession, sendPurchaseConfirmation } from \"./fulfill.js\";\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` via the shared\n * `fulfillCheckoutSession`, then defers the magic-link email via `after()` so\n * Stripe sees a fast 200.\n *\n * The handler is not the only fulfillment path — a consumer's success page can\n * call `fulfillCheckoutSession` directly rather than waiting on webhook\n * delivery. Whichever runs first writes the row; the other gets\n * `already-recorded` and skips the email, so the customer is mailed exactly\n * once no matter who wins. Every non-`created` outcome answers Stripe with\n * `{ received: true }` so it doesn't retry.\n */\nexport function createStripeWebhookHandler(\n deps: WebhookDeps,\n): (req: NextRequest) => Promise<Response> {\n const { stripe: stripeOverride, stripeSecret, webhookSecret, queries, site } = 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 const result = await fulfillCheckoutSession(session, { queries, site });\n if (result.status !== \"created\") {\n return Response.json({ received: true });\n }\n\n after(() =>\n sendPurchaseConfirmation(\n {\n orderId: result.order.id,\n customerEmail: result.customerEmail,\n itemNames: result.itemNames,\n totalCents: result.totalCents,\n },\n deps,\n ),\n );\n\n return Response.json({ received: true });\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/fulfill.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 * Split a bundle's charged total across its members, proportional to list\n * price, using largest-remainder allocation.\n *\n * Members are keyed by their `amounts`-metadata key (`r<id>` / `t<id>`) rather\n * than a bare numeric id, so a bundle holding tracks can't have a track id\n * collide with a release id.\n *\n * The returned values sum to `totalCents` exactly. That's load-bearing: they\n * become the `amounts` metadata `fulfillCheckoutSession` turns into\n * `order_items.price` rows, and `sum(order_items) === orders.amountTotal` is\n * what refunds and admin order views rely on. Ties break deterministically\n * (remainder desc, price desc, key asc) so a retry apportions identically.\n */\nfunction apportion(\n totalCents: number,\n members: { key: string; price: number }[],\n): Map<string, number> {\n const result = new Map<string, number>();\n if (members.length === 0) return result;\n\n const total = Math.max(0, Math.round(totalCents));\n const sum = members.reduce((s, m) => s + Math.max(0, m.price), 0);\n // All-free members carry no weight to divide by, so split evenly.\n const weights =\n sum > 0\n ? members.map((m) => Math.max(0, m.price) / sum)\n : members.map(() => 1 / members.length);\n\n const exact = weights.map((w) => total * w);\n const base = exact.map((v) => Math.floor(v));\n let remainder = total - base.reduce((s, v) => s + v, 0);\n\n const order = members\n .map((m, i) => ({ i, frac: exact[i]! - base[i]!, price: m.price, key: m.key }))\n .sort((a, b) => b.frac - a.frac || b.price - a.price || a.key.localeCompare(b.key));\n\n for (const { i } of order) {\n if (remainder <= 0) break;\n base[i]! += 1;\n remainder -= 1;\n }\n\n members.forEach((m, i) => result.set(m.key, base[i]!));\n return result;\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 bundlePurchase,\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 const bundleIds = [\n ...new Set(\n items\n .filter((i) => i.kind === \"bundle\" && typeof i.bundleId === \"string\")\n .map((i) => i.bundleId as string),\n ),\n ];\n\n // The catalog already contains every bundle, so charging for both would\n // bill the same releases twice.\n if (isCatalogPurchase && bundleIds.length > 0) {\n return jsonError(\"Cannot mix a catalog purchase with a bundle\", 400);\n }\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 && bundleIds.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n if (bundleIds.length > 0 && typeof bundlePurchase !== \"function\") {\n return jsonError(\"Bundle purchases are not configured\", 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 // Accumulate: the same release can be charged by more than one line\n // (a bundle plus a loose copy in a stale cart), and the recorded\n // order item must carry the total actually paid for it.\n chargedAmounts[`r${r.id}`] = (chargedAmounts[`r${r.id}`] ?? 0) + 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}`] = (chargedAmounts[`t${track.id}`] ?? 0) + 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 // Bundles resolve against the same published-release snapshot as the\n // loose lines, so member prices, names, and covers stay consistent. A\n // bundle may hold releases, tracks, or both — the package doesn't care\n // which; the consumer decides what a bundle is allowed to contain.\n const bundleLineItems: typeof releaseLineItems = [];\n const bundleReleaseMemberIds: number[] = [];\n const bundleTrackMemberIds: number[] = [];\n const unavailableBundleIds: string[] = [];\n\n for (const bundleId of bundleIds) {\n const resolved = await bundlePurchase!(bundleId);\n const releaseMembers = (resolved?.releaseIds ?? [])\n .map((id) => releaseById.get(id))\n .filter((r): r is ReleaseWithTracks => r !== undefined);\n const trackMembers = (resolved?.trackIds ?? [])\n .map((id) => trackContext.get(id))\n .filter(\n (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>\n c !== undefined,\n );\n\n // No such bundle, or nothing in it is published any more. Charging for\n // the latter would produce a paid order with zero items.\n if (!resolved || releaseMembers.length + trackMembers.length === 0) {\n unavailableBundleIds.push(bundleId);\n continue;\n }\n\n const total = Math.max(\n STRIPE_MIN_CHARGE_CENTS,\n Math.round(resolved.totalCents),\n );\n // A track carries no cover of its own, so a track-only bundle falls\n // back to the cover of the release the first member belongs to.\n const cover =\n releaseMembers.find((m) => m.coverImageUrl)?.coverImageUrl ??\n trackMembers.find((c) => c.release.coverImageUrl)?.release.coverImageUrl;\n bundleLineItems.push({\n price_data: {\n currency,\n product_data: {\n name: resolved.productName,\n ...(cover ? { images: [toAbsoluteUrl(cover, baseUrl)] } : {}),\n },\n unit_amount: total,\n },\n quantity: 1 as const,\n });\n\n // Apportioned jointly across both member kinds — the total has to land\n // on the members as one set, not split per kind and rounded twice.\n for (const [key, cents] of apportion(total, [\n ...releaseMembers.map((m) => ({ key: `r${m.id}`, price: m.price })),\n ...trackMembers.map(({ track }) => ({ key: `t${track.id}`, price: track.price })),\n ])) {\n chargedAmounts[key] = (chargedAmounts[key] ?? 0) + cents;\n }\n bundleReleaseMemberIds.push(...releaseMembers.map((m) => m.id));\n bundleTrackMemberIds.push(...trackMembers.map(({ track }) => track.id));\n }\n\n if (unavailableBundleIds.length > 0) {\n return Response.json(\n { error: \"bundle-unavailable\", bundleIds: unavailableBundleIds },\n { status: 409 },\n );\n }\n\n const lineItems = [...bundleLineItems, ...releaseLineItems, ...trackLineItems];\n if (lineItems.length === 0) {\n return jsonError(\"No valid items found\", 400);\n }\n\n // Deduped: a release or track reachable from both a bundle and a loose\n // line still yields one order item, whose price is the summed `amounts`\n // entry.\n const resolvedReleaseIds = [\n ...new Set([\n ...(releaseLineItems.length ? releaseIds.filter((id) => releaseById.has(id)) : []),\n ...bundleReleaseMemberIds,\n ]),\n ];\n const resolvedTrackIds = [\n ...new Set([\n ...(trackLineItems.length ? trackIds.filter((id) => trackContext.has(id)) : []),\n ...bundleTrackMemberIds,\n ]),\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 // Ignored by fulfillment — bundles decompose into per-release order\n // items — but invaluable when supporting \"what did I actually buy?\".\n ...(bundleIds.length > 0 ? { bundle_ids: JSON.stringify(bundleIds) } : {}),\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 type Stripe from \"stripe\";\nimport { signOrderToken } from \"@gigamusic/core\";\nimport {\n isUniqueConstraintError,\n type OrderWithItems,\n type ReleaseWithTracks,\n type TrackWithFiles,\n} from \"@gigamusic/db\";\nimport { renderPurchaseConfirmation } from \"@gigamusic/email\";\nimport type { FulfillSessionDeps, PurchaseConfirmationDeps } from \"./types.js\";\n\nexport type FulfillSessionResult =\n /** This call wrote the order row. The caller owns sending the confirmation email. */\n | {\n status: \"created\";\n order: OrderWithItems;\n itemNames: string[];\n customerEmail: string;\n totalCents: number;\n }\n /**\n * The order was already on disk — either the pre-flight lookup found it or a\n * concurrent writer won the unique-constraint race. `order` is null only if\n * the re-read after a lost race somehow came back empty.\n */\n | { status: \"already-recorded\"; order: OrderWithItems | null }\n /** Nothing was written and nothing is owed. */\n | { status: \"skipped\"; reason: \"site-mismatch\" | \"no-items\" | \"unpaid\" };\n\n/**\n * Turn a completed Stripe Checkout Session into a persisted order, idempotently.\n *\n * This is the single fulfillment path shared by the Stripe webhook and any\n * consumer that wants to fulfill synchronously — e.g. a post-checkout success\n * page that would otherwise have to wait on webhook delivery to show download\n * links. Whoever calls first writes the row; everyone after gets\n * `already-recorded`. That guarantee rests on the unique constraint over\n * `stripeSessionId` inside `recordCompletedOrder`, not on call ordering, so\n * concurrent callers are safe.\n *\n * Only `status: \"created\"` should trigger side effects (the confirmation\n * email) — that status is returned to exactly one caller per session.\n *\n * ### Published-only resolution (gotcha)\n *\n * Release/track IDs from `session.metadata` are resolved against\n * `queries.listPublishedReleases()`. Anything not in the published catalog at\n * fulfillment time is silently dropped from the persisted order's line items —\n * the customer is still charged, but the order row doesn't include the\n * 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 here are:\n * - an admin unpublishes between session-create and fulfillment\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 is a realistic concern, swap the resolution call here to\n * `listAllReleases()` (or wrap with a \"warn + fallback\" branch) — but be aware\n * that doing so will surface unpublished items in admin order views.\n */\nexport async function fulfillCheckoutSession(\n session: Stripe.Checkout.Session,\n deps: FulfillSessionDeps,\n): Promise<FulfillSessionResult> {\n const { queries, site } = deps;\n\n // One Stripe account fans every event out to every registered endpoint, and\n // a success URL can be hand-forged. Ignore sessions belonging to a different\n // site so we never record a foreign purchase (with zero matched line items)\n // under this one.\n if (session.metadata?.site !== site) {\n return { status: \"skipped\", reason: \"site-mismatch\" };\n }\n\n // Delayed-notification methods (bank debits, vouchers) complete checkout with\n // the money still in flight; the session only flips to `paid` later, via\n // `checkout.session.async_payment_succeeded`. Handing over downloads at that\n // point would be giving away unpaid goods. `no_payment_required` (fully\n // discounted) and legacy sessions with no `payment_status` are fulfillable.\n if (session.payment_status === \"unpaid\") {\n return { status: \"skipped\", reason: \"unpaid\" };\n }\n\n const existing = await queries.getOrderByStripeSessionId(session.id);\n if (existing) {\n return { status: \"already-recorded\", order: existing };\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 on\n // 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 { status: \"skipped\", reason: \"no-items\" };\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 } => 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 const totalCents = session.amount_total ?? 0;\n\n let order: OrderWithItems;\n try {\n order = await queries.recordCompletedOrder({\n stripeSessionId: session.id,\n stripePaymentId:\n typeof session.payment_intent === \"string\" ? session.payment_intent : null,\n email: customerEmail,\n amountTotal: totalCents,\n items: orderItems,\n });\n } catch (err) {\n // Two callers can both pass the pre-flight lookup and race here — a webhook\n // retry, or the success page rendering while the webhook is mid-flight. The\n // unique constraint on `stripeSessionId` guarantees only one row lands; the\n // loser surfaces as a 23505 unique-violation. Re-read so the loser still\n // gets the order to render, but without claiming the \"created\" side effects.\n if (isUniqueConstraintError(err)) {\n return {\n status: \"already-recorded\",\n order: (await queries.getOrderByStripeSessionId(session.id)) ?? null,\n };\n }\n throw err;\n }\n\n const itemNames = [\n ...matchedReleases.map((r) => r.name),\n ...matchedTracks.map(({ track, release }) => `${release.name} — ${track.name}`),\n ];\n\n return { status: \"created\", order, itemNames, customerEmail, totalCents };\n}\n\n/**\n * Render and send the post-purchase confirmation email carrying the durable\n * magic link back to the customer's downloads.\n *\n * Errors are logged, never thrown: the order is already recorded and paid for,\n * so a mail-provider hiccup must not fail the caller (a webhook returning 500\n * would make Stripe retry a fulfillment that already succeeded). Callers\n * typically wrap this in `after()` so the response isn't blocked on it.\n */\nexport async function sendPurchaseConfirmation(\n args: {\n orderId: number;\n customerEmail: string;\n itemNames: string[];\n totalCents: number;\n },\n deps: PurchaseConfirmationDeps,\n): Promise<void> {\n const {\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 (!args.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 `[gigamusic/checkout] order ${args.orderId} has no customer email — recorded, but the customer cannot retrieve downloads.`,\n );\n return;\n }\n\n try {\n const verifyToken = await signOrderToken({\n orderId: String(args.orderId),\n email: args.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: args.itemNames,\n totalCents: args.totalCents,\n });\n await email.send({ from: emailFrom, to: args.customerEmail, subject, html });\n } catch (err) {\n console.error(\n `[gigamusic/checkout] failed to send purchase confirmation to ${args.customerEmail} for order ${args.orderId}:`,\n err,\n );\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 { after, type NextRequest } from \"next/server\";\nimport Stripe from \"stripe\";\nimport { fulfillCheckoutSession, sendPurchaseConfirmation } from \"./fulfill.js\";\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` via the shared\n * `fulfillCheckoutSession`, then defers the magic-link email via `after()` so\n * Stripe sees a fast 200.\n *\n * The handler is not the only fulfillment path — a consumer's success page can\n * call `fulfillCheckoutSession` directly rather than waiting on webhook\n * delivery. Whichever runs first writes the row; the other gets\n * `already-recorded` and skips the email, so the customer is mailed exactly\n * once no matter who wins. Every non-`created` outcome answers Stripe with\n * `{ received: true }` so it doesn't retry.\n */\nexport function createStripeWebhookHandler(\n deps: WebhookDeps,\n): (req: NextRequest) => Promise<Response> {\n const { stripe: stripeOverride, stripeSecret, webhookSecret, queries, site } = 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 const result = await fulfillCheckoutSession(session, { queries, site });\n if (result.status !== \"created\") {\n return Response.json({ received: true });\n }\n\n after(() =>\n sendPurchaseConfirmation(\n {\n orderId: result.order.id,\n customerEmail: result.customerEmail,\n itemNames: result.itemNames,\n totalCents: result.totalCents,\n },\n deps,\n ),\n );\n\n return Response.json({ received: true });\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.6.0",
3
+ "version": "4.7.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": {
package/src/checkout.ts CHANGED
@@ -32,20 +32,24 @@ function resolveUnitAmount(
32
32
  }
33
33
 
34
34
  /**
35
- * Split a bundle's charged total across its member releases, proportional to
36
- * list price, using largest-remainder allocation.
35
+ * Split a bundle's charged total across its members, proportional to list
36
+ * price, using largest-remainder allocation.
37
+ *
38
+ * Members are keyed by their `amounts`-metadata key (`r<id>` / `t<id>`) rather
39
+ * than a bare numeric id, so a bundle holding tracks can't have a track id
40
+ * collide with a release id.
37
41
  *
38
42
  * The returned values sum to `totalCents` exactly. That's load-bearing: they
39
43
  * become the `amounts` metadata `fulfillCheckoutSession` turns into
40
44
  * `order_items.price` rows, and `sum(order_items) === orders.amountTotal` is
41
45
  * what refunds and admin order views rely on. Ties break deterministically
42
- * (remainder desc, price desc, id asc) so a retry apportions identically.
46
+ * (remainder desc, price desc, key asc) so a retry apportions identically.
43
47
  */
44
48
  function apportion(
45
49
  totalCents: number,
46
- members: { id: number; price: number }[],
47
- ): Map<number, number> {
48
- const result = new Map<number, number>();
50
+ members: { key: string; price: number }[],
51
+ ): Map<string, number> {
52
+ const result = new Map<string, number>();
49
53
  if (members.length === 0) return result;
50
54
 
51
55
  const total = Math.max(0, Math.round(totalCents));
@@ -61,8 +65,8 @@ function apportion(
61
65
  let remainder = total - base.reduce((s, v) => s + v, 0);
62
66
 
63
67
  const order = members
64
- .map((m, i) => ({ i, frac: exact[i]! - base[i]!, price: m.price, id: m.id }))
65
- .sort((a, b) => b.frac - a.frac || b.price - a.price || a.id - b.id);
68
+ .map((m, i) => ({ i, frac: exact[i]! - base[i]!, price: m.price, key: m.key }))
69
+ .sort((a, b) => b.frac - a.frac || b.price - a.price || a.key.localeCompare(b.key));
66
70
 
67
71
  for (const { i } of order) {
68
72
  if (remainder <= 0) break;
@@ -70,7 +74,7 @@ function apportion(
70
74
  remainder -= 1;
71
75
  }
72
76
 
73
- members.forEach((m, i) => result.set(m.id, base[i]!));
77
+ members.forEach((m, i) => result.set(m.key, base[i]!));
74
78
  return result;
75
79
  }
76
80
 
@@ -284,20 +288,29 @@ export function createCheckoutHandler(
284
288
  });
285
289
 
286
290
  // Bundles resolve against the same published-release snapshot as the
287
- // loose lines, so member prices, names, and covers stay consistent.
291
+ // loose lines, so member prices, names, and covers stay consistent. A
292
+ // bundle may hold releases, tracks, or both — the package doesn't care
293
+ // which; the consumer decides what a bundle is allowed to contain.
288
294
  const bundleLineItems: typeof releaseLineItems = [];
289
- const bundleMemberIds: number[] = [];
295
+ const bundleReleaseMemberIds: number[] = [];
296
+ const bundleTrackMemberIds: number[] = [];
290
297
  const unavailableBundleIds: string[] = [];
291
298
 
292
299
  for (const bundleId of bundleIds) {
293
300
  const resolved = await bundlePurchase!(bundleId);
294
- const members = (resolved?.releaseIds ?? [])
301
+ const releaseMembers = (resolved?.releaseIds ?? [])
295
302
  .map((id) => releaseById.get(id))
296
303
  .filter((r): r is ReleaseWithTracks => r !== undefined);
304
+ const trackMembers = (resolved?.trackIds ?? [])
305
+ .map((id) => trackContext.get(id))
306
+ .filter(
307
+ (c): c is { track: TrackWithFiles; release: ReleaseWithTracks } =>
308
+ c !== undefined,
309
+ );
297
310
 
298
311
  // No such bundle, or nothing in it is published any more. Charging for
299
312
  // the latter would produce a paid order with zero items.
300
- if (!resolved || members.length === 0) {
313
+ if (!resolved || releaseMembers.length + trackMembers.length === 0) {
301
314
  unavailableBundleIds.push(bundleId);
302
315
  continue;
303
316
  }
@@ -306,7 +319,11 @@ export function createCheckoutHandler(
306
319
  STRIPE_MIN_CHARGE_CENTS,
307
320
  Math.round(resolved.totalCents),
308
321
  );
309
- const cover = members.find((m) => m.coverImageUrl)?.coverImageUrl;
322
+ // A track carries no cover of its own, so a track-only bundle falls
323
+ // back to the cover of the release the first member belongs to.
324
+ const cover =
325
+ releaseMembers.find((m) => m.coverImageUrl)?.coverImageUrl ??
326
+ trackMembers.find((c) => c.release.coverImageUrl)?.release.coverImageUrl;
310
327
  bundleLineItems.push({
311
328
  price_data: {
312
329
  currency,
@@ -319,13 +336,16 @@ export function createCheckoutHandler(
319
336
  quantity: 1 as const,
320
337
  });
321
338
 
322
- for (const [id, cents] of apportion(
323
- total,
324
- members.map((m) => ({ id: m.id, price: m.price })),
325
- )) {
326
- chargedAmounts[`r${id}`] = (chargedAmounts[`r${id}`] ?? 0) + cents;
327
- bundleMemberIds.push(id);
339
+ // Apportioned jointly across both member kinds — the total has to land
340
+ // on the members as one set, not split per kind and rounded twice.
341
+ for (const [key, cents] of apportion(total, [
342
+ ...releaseMembers.map((m) => ({ key: `r${m.id}`, price: m.price })),
343
+ ...trackMembers.map(({ track }) => ({ key: `t${track.id}`, price: track.price })),
344
+ ])) {
345
+ chargedAmounts[key] = (chargedAmounts[key] ?? 0) + cents;
328
346
  }
347
+ bundleReleaseMemberIds.push(...releaseMembers.map((m) => m.id));
348
+ bundleTrackMemberIds.push(...trackMembers.map(({ track }) => track.id));
329
349
  }
330
350
 
331
351
  if (unavailableBundleIds.length > 0) {
@@ -340,17 +360,21 @@ export function createCheckoutHandler(
340
360
  return jsonError("No valid items found", 400);
341
361
  }
342
362
 
343
- // Deduped: a release reachable from both a bundle and a loose line still
344
- // yields one order item, whose price is the summed `amounts` entry.
363
+ // Deduped: a release or track reachable from both a bundle and a loose
364
+ // line still yields one order item, whose price is the summed `amounts`
365
+ // entry.
345
366
  const resolvedReleaseIds = [
346
367
  ...new Set([
347
368
  ...(releaseLineItems.length ? releaseIds.filter((id) => releaseById.has(id)) : []),
348
- ...bundleMemberIds,
369
+ ...bundleReleaseMemberIds,
370
+ ]),
371
+ ];
372
+ const resolvedTrackIds = [
373
+ ...new Set([
374
+ ...(trackLineItems.length ? trackIds.filter((id) => trackContext.has(id)) : []),
375
+ ...bundleTrackMemberIds,
349
376
  ]),
350
377
  ];
351
- const resolvedTrackIds = trackLineItems.length
352
- ? [...new Set(trackIds.filter((id) => trackContext.has(id)))]
353
- : [];
354
378
 
355
379
  const session = await stripe.checkout.sessions.create({
356
380
  mode: "payment",
package/src/types.ts CHANGED
@@ -74,9 +74,10 @@ export interface CheckoutDeps {
74
74
  *
75
75
  * Called once per distinct `bundleId` in the cart. As with the catalog, the
76
76
  * consumer owns pricing math: hand back the final `totalCents` and the
77
- * member `releaseIds`, and the handler apportions the total across those
78
- * members (proportional to list price, largest-remainder) so the recorded
79
- * order items sum to exactly what Stripe charged.
77
+ * members (`releaseIds`, `trackIds`, or both), and the handler apportions
78
+ * the total across those members (proportional to list price,
79
+ * largest-remainder) so the recorded order items sum to exactly what Stripe
80
+ * charged.
80
81
  *
81
82
  * Resolving to `null`/`undefined` means "no such bundle" — the handler
82
83
  * replies 409 `{ error: "bundle-unavailable", bundleIds }` so the cart can
@@ -88,7 +89,14 @@ export interface CheckoutDeps {
88
89
  ) => Promise<ResolvedBundle | null | undefined> | ResolvedBundle | null | undefined;
89
90
  }
90
91
 
91
- /** What `bundlePurchase` returns for a bundle the consumer recognises. */
92
+ /**
93
+ * What `bundlePurchase` returns for a bundle the consumer recognises.
94
+ *
95
+ * A bundle's members are `releaseIds`, `trackIds`, or both — the package
96
+ * imposes no rule about mixing them, so a consumer is free to model bundles as
97
+ * releases-only, singles-only, or a combination. Supply at least one member
98
+ * list; a bundle that resolves to no surviving member is reported unavailable.
99
+ */
92
100
  export interface ResolvedBundle {
93
101
  /** Final charged price for the whole bundle, in cents. Clamped up to the Stripe card minimum. */
94
102
  totalCents: number;
@@ -96,10 +104,16 @@ export interface ResolvedBundle {
96
104
  productName: string;
97
105
  /**
98
106
  * Member release ids. Resolved against `listPublishedReleases()` — ids that
99
- * aren't published are dropped, and a bundle left with none is treated as
100
- * unavailable rather than charging for an order with no items.
107
+ * aren't published are dropped, and a bundle left with no members at all is
108
+ * treated as unavailable rather than charging for an order with no items.
109
+ */
110
+ releaseIds?: number[];
111
+ /**
112
+ * Member track ids — individual songs, for a "bundle of singles". Resolved
113
+ * against the tracks of `listPublishedReleases()`, so a track whose release
114
+ * is unpublished is dropped on the same terms as an unpublished release.
101
115
  */
102
- releaseIds: number[];
116
+ trackIds?: number[];
103
117
  }
104
118
 
105
119
  /** What `fulfillCheckoutSession` needs to turn a paid Stripe session into an order row. */