payweave 0.0.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +272 -0
  3. package/dist/_tsup-dts-rollup.d.ts +19470 -0
  4. package/dist/chunk-4R5OGOCC.js +94 -0
  5. package/dist/chunk-4R5OGOCC.js.map +1 -0
  6. package/dist/chunk-535Q3GUN.js +175 -0
  7. package/dist/chunk-535Q3GUN.js.map +1 -0
  8. package/dist/chunk-67NRYGRC.js +192 -0
  9. package/dist/chunk-67NRYGRC.js.map +1 -0
  10. package/dist/chunk-CGQ73I4I.js +568 -0
  11. package/dist/chunk-CGQ73I4I.js.map +1 -0
  12. package/dist/chunk-CJEYDHY6.js +18 -0
  13. package/dist/chunk-CJEYDHY6.js.map +1 -0
  14. package/dist/chunk-CR63JAAS.js +3 -0
  15. package/dist/chunk-CR63JAAS.js.map +1 -0
  16. package/dist/chunk-E633MKVX.js +1507 -0
  17. package/dist/chunk-E633MKVX.js.map +1 -0
  18. package/dist/chunk-FBPOCXRZ.js +1135 -0
  19. package/dist/chunk-FBPOCXRZ.js.map +1 -0
  20. package/dist/chunk-HTRGMZFH.js +267 -0
  21. package/dist/chunk-HTRGMZFH.js.map +1 -0
  22. package/dist/chunk-IUVGM3K7.js +765 -0
  23. package/dist/chunk-IUVGM3K7.js.map +1 -0
  24. package/dist/chunk-JD4OZHRI.js +326 -0
  25. package/dist/chunk-JD4OZHRI.js.map +1 -0
  26. package/dist/chunk-NV3MZF7L.js +139 -0
  27. package/dist/chunk-NV3MZF7L.js.map +1 -0
  28. package/dist/chunk-NXQBP35U.js +13 -0
  29. package/dist/chunk-NXQBP35U.js.map +1 -0
  30. package/dist/chunk-QJHXDZYC.js +412 -0
  31. package/dist/chunk-QJHXDZYC.js.map +1 -0
  32. package/dist/chunk-TWZK62TZ.js +83 -0
  33. package/dist/chunk-TWZK62TZ.js.map +1 -0
  34. package/dist/cli/index.js +35381 -0
  35. package/dist/core/index.d.ts +85 -0
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/db/drizzle/index.d.ts +6 -0
  39. package/dist/db/drizzle/index.js +1532 -0
  40. package/dist/db/drizzle/index.js.map +1 -0
  41. package/dist/db/index.d.ts +43 -0
  42. package/dist/db/index.js +3 -0
  43. package/dist/db/index.js.map +1 -0
  44. package/dist/db/mongodb/index.d.ts +3 -0
  45. package/dist/db/mongodb/index.js +934 -0
  46. package/dist/db/mongodb/index.js.map +1 -0
  47. package/dist/db/mysql/index.d.ts +1 -0
  48. package/dist/db/mysql/index.js +12 -0
  49. package/dist/db/mysql/index.js.map +1 -0
  50. package/dist/db/postgres/index.d.ts +5 -0
  51. package/dist/db/postgres/index.js +791 -0
  52. package/dist/db/postgres/index.js.map +1 -0
  53. package/dist/db/prisma/index.d.ts +1 -0
  54. package/dist/db/prisma/index.js +12 -0
  55. package/dist/db/prisma/index.js.map +1 -0
  56. package/dist/db/sqlite/index.d.ts +5 -0
  57. package/dist/db/sqlite/index.js +750 -0
  58. package/dist/db/sqlite/index.js.map +1 -0
  59. package/dist/express/index.d.ts +1 -0
  60. package/dist/express/index.js +3 -0
  61. package/dist/express/index.js.map +1 -0
  62. package/dist/fastify/index.d.ts +1 -0
  63. package/dist/fastify/index.js +3 -0
  64. package/dist/fastify/index.js.map +1 -0
  65. package/dist/flutterwave/index.d.ts +27 -0
  66. package/dist/flutterwave/index.js +9 -0
  67. package/dist/flutterwave/index.js.map +1 -0
  68. package/dist/index.d.ts +85 -0
  69. package/dist/index.js +3677 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/next/index.d.ts +1 -0
  72. package/dist/next/index.js +3 -0
  73. package/dist/next/index.js.map +1 -0
  74. package/dist/paystack/index.d.ts +33 -0
  75. package/dist/paystack/index.js +9 -0
  76. package/dist/paystack/index.js.map +1 -0
  77. package/dist/products/index.d.ts +22 -0
  78. package/dist/products/index.js +7 -0
  79. package/dist/products/index.js.map +1 -0
  80. package/dist/testing/index.d.ts +11 -0
  81. package/dist/testing/index.js +69 -0
  82. package/dist/testing/index.js.map +1 -0
  83. package/dist/unified/index.d.ts +41 -0
  84. package/dist/unified/index.js +6 -0
  85. package/dist/unified/index.js.map +1 -0
  86. package/dist/webhooks/index.d.ts +8 -0
  87. package/dist/webhooks/index.js +8 -0
  88. package/dist/webhooks/index.js.map +1 -0
  89. package/package.json +164 -0
@@ -0,0 +1,568 @@
1
+ import { advance } from './chunk-4R5OGOCC.js';
2
+ import { toUnifiedEventType, toUnifiedStatus } from './chunk-67NRYGRC.js';
3
+ import { PayweaveConfigError, PayweaveValidationError, PayweaveWebhookVerificationError, PayweaveProviderError } from './chunk-HTRGMZFH.js';
4
+ import { createHmac, timingSafeEqual, createHash } from 'crypto';
5
+
6
+ // src/products/subscribe.ts
7
+ var BILLING_CAPABLE_PROVIDERS = ["stripe", "paystack"];
8
+ function isBillingCapableProvider(provider) {
9
+ return BILLING_CAPABLE_PROVIDERS.includes(provider);
10
+ }
11
+ function resolvePlanGroup(plan) {
12
+ return plan.group ?? plan.id;
13
+ }
14
+ function nominalPeriod(interval, now) {
15
+ const startMs = now.getTime();
16
+ return { start: new Date(startMs), end: new Date(advance(startMs, interval ?? "month", 1)) };
17
+ }
18
+ function findPlan(products, planId) {
19
+ return products?.find((p) => p.id === planId);
20
+ }
21
+ function groupExclusivityError(group) {
22
+ return new PayweaveValidationError(`customer already has an active plan in group '${group}'`);
23
+ }
24
+ async function ensureProviderCustomer(ctx, database, provider, customerRow, externalId) {
25
+ const existing = customerRow.providerIds[provider];
26
+ if (existing !== void 0) return existing;
27
+ if (provider === "stripe") {
28
+ if (!ctx.stripe) throw new PayweaveConfigError("stripe is not configured on this client.");
29
+ const created2 = await ctx.stripe.customers.create({
30
+ ...customerRow.email !== null ? { email: customerRow.email } : {},
31
+ metadata: { pwv_reference: customerRow.id }
32
+ });
33
+ await database.customers.linkProviderRef(externalId, "stripe", created2.id);
34
+ return created2.id;
35
+ }
36
+ if (!ctx.paystack) throw new PayweaveConfigError("paystack is not configured on this client.");
37
+ if (customerRow.email === null) {
38
+ throw new PayweaveValidationError(
39
+ "paystack billing requires a customer email, but this customer has none on file \u2014 call `database.customers.upsert({ externalId, email })` against your own adapter reference before subscribing (`subscribe()`'s input doesn't carry an email, and Paystack's customer/transaction APIs require one)."
40
+ );
41
+ }
42
+ const created = await ctx.paystack.customers.create({
43
+ email: customerRow.email,
44
+ metadata: { pwv_reference: customerRow.id }
45
+ });
46
+ const customerCode = created.data.customer_code;
47
+ if (customerCode === void 0) {
48
+ throw new PayweaveProviderError("paystack created a customer without a customer_code", {
49
+ provider: "paystack"
50
+ });
51
+ }
52
+ await database.customers.linkProviderRef(externalId, "paystack", customerCode);
53
+ return customerCode;
54
+ }
55
+ async function createStripeCheckout(ctx, database, plan, providerRefs, customerRow, incompleteRow, input) {
56
+ const priceId = providerRefs.priceId;
57
+ if (priceId === void 0) {
58
+ throw new PayweaveValidationError(
59
+ `plan "${plan.id}" has no pushed stripe price \u2014 run \`payweave push\`.`
60
+ );
61
+ }
62
+ if (!ctx.stripe) throw new PayweaveConfigError("stripe is not configured on this client.");
63
+ const providerCustomerId = await ensureProviderCustomer(
64
+ ctx,
65
+ database,
66
+ "stripe",
67
+ customerRow,
68
+ input.customerId
69
+ );
70
+ const session = await ctx.stripe.checkout.sessions.create({
71
+ mode: "subscription",
72
+ customer: providerCustomerId,
73
+ line_items: [{ price: priceId, quantity: 1 }],
74
+ // Correlates the resulting `checkout.session.completed` event back to
75
+ // this local row (see the module doc comment's "seams for the webhook
76
+ // handler" note).
77
+ client_reference_id: incompleteRow.id,
78
+ metadata: { pwv_reference: incompleteRow.id, pwv_plan: plan.id, pwv_customer: customerRow.id },
79
+ ...input.successUrl !== void 0 ? { success_url: input.successUrl } : {},
80
+ ...input.cancelUrl !== void 0 ? { cancel_url: input.cancelUrl } : {}
81
+ });
82
+ if (!session.url) {
83
+ throw new PayweaveProviderError("stripe created a checkout session without a hosted URL", {
84
+ provider: "stripe",
85
+ raw: session
86
+ });
87
+ }
88
+ return { status: "checkout", checkoutUrl: session.url, reference: session.id };
89
+ }
90
+ async function createPaystackCheckout(ctx, database, plan, providerRefs, customerRow, incompleteRow, input) {
91
+ const planCode = providerRefs.planCode;
92
+ if (planCode === void 0) {
93
+ throw new PayweaveValidationError(
94
+ `plan "${plan.id}" has no pushed paystack plan code \u2014 run \`payweave push\`.`
95
+ );
96
+ }
97
+ if (!ctx.paystack) throw new PayweaveConfigError("paystack is not configured on this client.");
98
+ await ensureProviderCustomer(ctx, database, "paystack", customerRow, input.customerId);
99
+ if (customerRow.email === null) {
100
+ throw new PayweaveValidationError(
101
+ "paystack billing requires a customer email, but this customer has none on file \u2014 call `database.customers.upsert({ externalId, email })` against your own adapter reference before subscribing (`subscribe()`'s input doesn't carry an email, and Paystack's customer/transaction APIs require one)."
102
+ );
103
+ }
104
+ const amountMinor = plan.price?.amount;
105
+ if (amountMinor === void 0) {
106
+ throw new PayweaveValidationError(`plan "${plan.id}" has no price to charge.`);
107
+ }
108
+ const res = await ctx.paystack.transactions.initialize({
109
+ email: customerRow.email,
110
+ amount: amountMinor,
111
+ plan: planCode,
112
+ // Our OWN reference — Paystack echoes it back on every subsequent
113
+ // event/verify call, so it doubles as the local row's stable correlation
114
+ // key for the webhook handler (see the module doc comment).
115
+ reference: incompleteRow.id,
116
+ metadata: { pwv_reference: incompleteRow.id, pwv_plan: plan.id, pwv_customer: customerRow.id }
117
+ });
118
+ return { status: "checkout", checkoutUrl: res.data.authorization_url, reference: res.data.reference };
119
+ }
120
+ async function subscribe(ctx, input) {
121
+ const database = ctx.database;
122
+ if (!database) {
123
+ throw new PayweaveConfigError(
124
+ "payweave.subscribe() needs a database \u2014 pass a payweave/db/* adapter to createPayweave()."
125
+ );
126
+ }
127
+ const plan = findPlan(ctx.products, input.planId);
128
+ if (!plan) {
129
+ throw new PayweaveValidationError(
130
+ `unknown plan "${input.planId}" \u2014 configure it in \`products\` and run \`payweave push\`.`
131
+ );
132
+ }
133
+ const provider = input.provider ?? ctx.defaultProvider;
134
+ if (!ctx.providers.includes(provider)) {
135
+ throw new PayweaveConfigError(
136
+ `provider "${provider}" is not configured on this client \u2014 configured: ${ctx.providers.join(", ")}.`
137
+ );
138
+ }
139
+ if (!isBillingCapableProvider(provider)) {
140
+ throw new PayweaveConfigError(
141
+ `provider "${provider}" does not support billing (subscribe) \u2014 billing-capable providers: ${BILLING_CAPABLE_PROVIDERS.join(", ")}.`
142
+ );
143
+ }
144
+ const group = resolvePlanGroup(plan);
145
+ const customerRow = await database.customers.upsert({ externalId: input.customerId });
146
+ const existingActive = await database.subscriptions.getActive(customerRow.id, group);
147
+ if (plan.default) {
148
+ if (existingActive) throw groupExclusivityError(group);
149
+ return { status: "active", planId: plan.id, subscription: null };
150
+ }
151
+ if (existingActive) throw groupExclusivityError(group);
152
+ const pushedVersion = await database.plans.getActiveVersion(plan.id);
153
+ if (!pushedVersion) {
154
+ throw new PayweaveValidationError(
155
+ `plan "${plan.id}" has no pushed version \u2014 run \`payweave push\` to sync your products.`
156
+ );
157
+ }
158
+ const now = /* @__PURE__ */ new Date();
159
+ if (plan.price === void 0) {
160
+ const period2 = nominalPeriod(void 0, now);
161
+ const row = await database.subscriptions.create({
162
+ customerId: customerRow.id,
163
+ planId: plan.id,
164
+ planVersion: pushedVersion.version,
165
+ group,
166
+ status: "active",
167
+ provider: null,
168
+ providerSubscriptionRef: null,
169
+ currentPeriodStart: period2.start,
170
+ currentPeriodEnd: period2.end,
171
+ cancelAtPeriodEnd: false
172
+ });
173
+ return { status: "active", planId: plan.id, subscription: row };
174
+ }
175
+ const providerRefs = pushedVersion.providerRefs[provider];
176
+ if (!providerRefs) {
177
+ throw new PayweaveValidationError(
178
+ `plan "${plan.id}" has no pushed ${provider} provider refs \u2014 run \`payweave push\`.`
179
+ );
180
+ }
181
+ const period = nominalPeriod(plan.price.interval, now);
182
+ const incompleteRow = await database.subscriptions.create({
183
+ customerId: customerRow.id,
184
+ planId: plan.id,
185
+ planVersion: pushedVersion.version,
186
+ group,
187
+ status: "incomplete",
188
+ provider,
189
+ providerSubscriptionRef: null,
190
+ currentPeriodStart: period.start,
191
+ currentPeriodEnd: period.end,
192
+ cancelAtPeriodEnd: false
193
+ });
194
+ if (provider === "stripe") {
195
+ return createStripeCheckout(ctx, database, plan, providerRefs, customerRow, incompleteRow, input);
196
+ }
197
+ return createPaystackCheckout(ctx, database, plan, providerRefs, customerRow, incompleteRow, input);
198
+ }
199
+
200
+ // src/products/apply.ts
201
+ function asRecord(value) {
202
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
203
+ }
204
+ function stringField(obj, key) {
205
+ const v = obj?.[key];
206
+ return typeof v === "string" && v.length > 0 ? v : void 0;
207
+ }
208
+ function numberField(obj, key) {
209
+ const v = obj?.[key];
210
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
211
+ }
212
+ function boolField(obj, key) {
213
+ const v = obj?.[key];
214
+ return typeof v === "boolean" ? v : void 0;
215
+ }
216
+ function stripeObject(event) {
217
+ if (event.provider !== "stripe") return void 0;
218
+ return asRecord(asRecord(event.data)?.object);
219
+ }
220
+ function flatData(event) {
221
+ return event.provider === "stripe" ? void 0 : asRecord(event.data);
222
+ }
223
+ function resolveCorrelation(event) {
224
+ if (event.provider === "stripe") {
225
+ const obj = stripeObject(event);
226
+ const meta2 = asRecord(obj?.metadata);
227
+ const rowId = stringField(obj, "client_reference_id") ?? stringField(meta2, "pwv_reference");
228
+ const providerSubscriptionRef = stringField(obj, "subscription") ?? (stringField(obj, "object") === "subscription" ? stringField(obj, "id") : void 0);
229
+ return {
230
+ rowId,
231
+ customerId: stringField(meta2, "pwv_customer"),
232
+ planId: stringField(meta2, "pwv_plan"),
233
+ providerSubscriptionRef
234
+ };
235
+ }
236
+ const data = flatData(event);
237
+ const meta = asRecord(data?.metadata);
238
+ return {
239
+ rowId: stringField(data, "reference") ?? stringField(meta, "pwv_reference"),
240
+ customerId: stringField(meta, "pwv_customer"),
241
+ planId: stringField(meta, "pwv_plan"),
242
+ providerSubscriptionRef: stringField(data, "subscription_code")
243
+ };
244
+ }
245
+ function resolveGroupForPlanId(ctx, planId) {
246
+ return ctx.products?.find((p) => p.id === planId);
247
+ }
248
+ async function findActiveRow(tx, ctx, correlation) {
249
+ if (!correlation.customerId || !correlation.planId) return null;
250
+ const plan = resolveGroupForPlanId(ctx, correlation.planId);
251
+ if (!plan) return null;
252
+ return tx.subscriptions.getActive(correlation.customerId, resolvePlanGroup(plan));
253
+ }
254
+ function firstListItem(container, key) {
255
+ const wrapper = asRecord(container?.[key]);
256
+ const list = wrapper?.data;
257
+ return Array.isArray(list) && list.length > 0 ? asRecord(list[0]) : void 0;
258
+ }
259
+ function periodFromUnixSeconds(start, end) {
260
+ if (end === void 0) return void 0;
261
+ return { start: start !== void 0 ? new Date(start * 1e3) : void 0, end: new Date(end * 1e3) };
262
+ }
263
+ function extractPeriod(event) {
264
+ if (event.provider === "stripe") {
265
+ const obj = stripeObject(event);
266
+ const item = firstListItem(obj, "items");
267
+ if (item) {
268
+ const period = periodFromUnixSeconds(
269
+ numberField(item, "current_period_start"),
270
+ numberField(item, "current_period_end")
271
+ );
272
+ if (period) return period;
273
+ }
274
+ const line = firstListItem(obj, "lines");
275
+ if (line) {
276
+ const period = asRecord(line.period);
277
+ const derived = periodFromUnixSeconds(numberField(period, "start"), numberField(period, "end"));
278
+ if (derived) return derived;
279
+ }
280
+ return void 0;
281
+ }
282
+ const data = flatData(event);
283
+ const nextPaymentDate = stringField(data, "next_payment_date");
284
+ if (!nextPaymentDate) return void 0;
285
+ const end = new Date(nextPaymentDate);
286
+ if (Number.isNaN(end.getTime())) return void 0;
287
+ return { start: void 0, end };
288
+ }
289
+ function resolvePeriodStart(period, currentRow, now) {
290
+ if (period.start) return period.start;
291
+ if (currentRow && currentRow.currentPeriodEnd.getTime() <= period.end.getTime()) {
292
+ return currentRow.currentPeriodEnd;
293
+ }
294
+ return now;
295
+ }
296
+ function isStalePeriod(period, currentRow) {
297
+ return period !== void 0 && period.end.getTime() < currentRow.currentPeriodEnd.getTime();
298
+ }
299
+ var STRIPE_SUBSCRIPTION_STATUS_MAP = {
300
+ active: "active",
301
+ trialing: "trialing",
302
+ past_due: "past_due",
303
+ canceled: "canceled",
304
+ unpaid: "past_due",
305
+ paused: "past_due",
306
+ incomplete: "incomplete",
307
+ incomplete_expired: "canceled"
308
+ };
309
+ var PAYSTACK_SUBSCRIPTION_STATUS_MAP = {
310
+ active: "active",
311
+ "non-renewing": "active",
312
+ attention: "past_due",
313
+ completed: "canceled",
314
+ cancelled: "canceled"
315
+ };
316
+ function mapNativeStatus(event) {
317
+ if (event.provider === "stripe") {
318
+ return STRIPE_SUBSCRIPTION_STATUS_MAP[stringField(stripeObject(event), "status") ?? ""];
319
+ }
320
+ if (event.provider === "paystack") {
321
+ return PAYSTACK_SUBSCRIPTION_STATUS_MAP[stringField(flatData(event), "status") ?? ""];
322
+ }
323
+ return void 0;
324
+ }
325
+ function extractCancelAtPeriodEnd(event) {
326
+ if (event.provider === "stripe") return boolField(stripeObject(event), "cancel_at_period_end");
327
+ if (event.provider === "paystack" && event.type === "subscription.not_renew") return true;
328
+ return void 0;
329
+ }
330
+ async function resetBalancesForPlanChange(database, params) {
331
+ for (const inclusion of params.plan.includes) {
332
+ if (inclusion.type !== "metered") continue;
333
+ const init = {
334
+ limit: inclusion.limit,
335
+ resetInterval: inclusion.reset,
336
+ anchor: params.anchor,
337
+ planId: params.plan.id,
338
+ planVersion: params.planVersion
339
+ };
340
+ await database.balances.resetTo(params.customerId, inclusion.featureId, params.group, init);
341
+ }
342
+ }
343
+ async function applyActivation(tx, ctx, event, now) {
344
+ const correlation = resolveCorrelation(event);
345
+ const period = extractPeriod(event);
346
+ const activeRow = await findActiveRow(tx, ctx, correlation);
347
+ if (activeRow) {
348
+ if (isStalePeriod(period, activeRow)) return { applied: false, skipped: "stale" };
349
+ const patch2 = { status: "active" };
350
+ if (correlation.providerSubscriptionRef) patch2.providerSubscriptionRef = correlation.providerSubscriptionRef;
351
+ if (period) {
352
+ patch2.currentPeriodStart = resolvePeriodStart(period, activeRow, now);
353
+ patch2.currentPeriodEnd = period.end;
354
+ }
355
+ const updated2 = await tx.subscriptions.update(activeRow.id, patch2);
356
+ return { applied: true, subscription: updated2 };
357
+ }
358
+ if (!correlation.rowId) return { applied: false, skipped: "unresolved" };
359
+ const patch = { status: "active" };
360
+ if (correlation.providerSubscriptionRef) patch.providerSubscriptionRef = correlation.providerSubscriptionRef;
361
+ if (period) {
362
+ patch.currentPeriodStart = period.start ?? now;
363
+ patch.currentPeriodEnd = period.end;
364
+ }
365
+ const updated = await tx.subscriptions.update(correlation.rowId, patch);
366
+ return { applied: true, subscription: updated };
367
+ }
368
+ async function applyLifecycleUpdate(tx, ctx, event, now, forcedStatus, allowPlanChange, logger) {
369
+ const correlation = resolveCorrelation(event);
370
+ const row = await findActiveRow(tx, ctx, correlation);
371
+ if (!row) {
372
+ logger?.({
373
+ type: "schema_drift",
374
+ message: "billing apply: could not correlate a subscription lifecycle event to a local row \u2014 safe no-op.",
375
+ provider: event.provider,
376
+ nativeType: event.type,
377
+ unifiedType: event.unifiedType
378
+ });
379
+ return { applied: false, skipped: "unresolved" };
380
+ }
381
+ const period = extractPeriod(event);
382
+ if (isStalePeriod(period, row)) return { applied: false, skipped: "stale" };
383
+ const patch = {};
384
+ const status = forcedStatus ?? mapNativeStatus(event);
385
+ if (status) patch.status = status;
386
+ if (period) {
387
+ patch.currentPeriodStart = resolvePeriodStart(period, row, now);
388
+ patch.currentPeriodEnd = period.end;
389
+ }
390
+ const cancelAtPeriodEnd = extractCancelAtPeriodEnd(event);
391
+ if (cancelAtPeriodEnd !== void 0) patch.cancelAtPeriodEnd = cancelAtPeriodEnd;
392
+ if (allowPlanChange && correlation.planId && correlation.planId !== row.planId) {
393
+ const newPlan = resolveGroupForPlanId(ctx, correlation.planId);
394
+ if (newPlan) {
395
+ const pushed = await tx.plans.getActiveVersion(newPlan.id);
396
+ const planVersion = pushed?.version ?? row.planVersion;
397
+ patch.planId = newPlan.id;
398
+ patch.planVersion = planVersion;
399
+ await resetBalancesForPlanChange(tx, {
400
+ customerId: row.customerId,
401
+ group: row.group,
402
+ plan: newPlan,
403
+ planVersion,
404
+ anchor: patch.currentPeriodStart ?? row.currentPeriodStart
405
+ });
406
+ }
407
+ }
408
+ if (Object.keys(patch).length === 0) return { applied: false, skipped: "unmapped" };
409
+ const updated = await tx.subscriptions.update(row.id, patch);
410
+ return { applied: true, subscription: updated };
411
+ }
412
+ async function applyWebhookEvent(ctx, event, options = {}) {
413
+ const database = ctx.database;
414
+ if (!database) {
415
+ throw new PayweaveConfigError(
416
+ "event.apply() needs a database \u2014 pass a payweave/db/* adapter to createPayweave()."
417
+ );
418
+ }
419
+ const now = options.now ?? /* @__PURE__ */ new Date();
420
+ const logger = options.logger;
421
+ return database.transaction(async (tx) => {
422
+ const claimed = await tx.webhookEvents.claim(event.dedupeKey, {
423
+ provider: event.provider,
424
+ type: event.type,
425
+ now
426
+ });
427
+ if (!claimed) return { applied: false, skipped: "already-applied" };
428
+ let result;
429
+ switch (event.unifiedType) {
430
+ case "payment.succeeded":
431
+ case "invoice.paid":
432
+ result = await applyActivation(tx, ctx, event, now);
433
+ break;
434
+ case "subscription.created":
435
+ case "subscription.updated":
436
+ result = await applyLifecycleUpdate(tx, ctx, event, now, void 0, true, logger);
437
+ break;
438
+ case "subscription.canceled":
439
+ result = await applyLifecycleUpdate(tx, ctx, event, now, "canceled", false, logger);
440
+ break;
441
+ case "invoice.payment_failed":
442
+ result = await applyLifecycleUpdate(tx, ctx, event, now, "past_due", false, logger);
443
+ break;
444
+ default:
445
+ result = { applied: false, skipped: "unmapped" };
446
+ }
447
+ if (result.skipped !== "unresolved") {
448
+ await tx.webhookEvents.markApplied(event.dedupeKey);
449
+ }
450
+ return result;
451
+ });
452
+ }
453
+ var DEFAULT_TOLERANCE_SEC = 300;
454
+ function matchesExpected(expected, candidate) {
455
+ const received = Buffer.from(candidate, "utf8");
456
+ return expected.length === received.length && timingSafeEqual(expected, received);
457
+ }
458
+ function verifyStripe(rawBody, signatureHeader, webhookSecret, options) {
459
+ if (typeof webhookSecret !== "string" || webhookSecret.length === 0) return false;
460
+ if (typeof signatureHeader !== "string" || signatureHeader.length === 0) return false;
461
+ if (typeof rawBody !== "string" && !(rawBody instanceof Uint8Array)) return false;
462
+ let timestampRaw;
463
+ const candidates = [];
464
+ for (const rawItem of signatureHeader.split(",")) {
465
+ const item = rawItem.trim();
466
+ const eq = item.indexOf("=");
467
+ if (eq === -1) return false;
468
+ const scheme = item.slice(0, eq);
469
+ const value = item.slice(eq + 1);
470
+ if (scheme === "t") {
471
+ if (timestampRaw !== void 0) return false;
472
+ timestampRaw = value;
473
+ } else if (scheme === "v1") {
474
+ candidates.push(value);
475
+ }
476
+ }
477
+ if (timestampRaw === void 0 || !/^\d+$/.test(timestampRaw)) return false;
478
+ if (candidates.length === 0) return false;
479
+ const timestamp = Number(timestampRaw);
480
+ const nowSec = options?.now !== void 0 ? options.now() : Math.floor(Date.now() / 1e3);
481
+ const toleranceSec = options?.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
482
+ const withinTolerance = Math.abs(nowSec - timestamp) <= toleranceSec;
483
+ const hmac = createHmac("sha256", webhookSecret);
484
+ hmac.update(`${timestampRaw}.`);
485
+ hmac.update(rawBody);
486
+ const expected = Buffer.from(hmac.digest("hex"), "utf8");
487
+ let matched = false;
488
+ for (const candidate of candidates) {
489
+ matched = matchesExpected(expected, candidate) || matched;
490
+ }
491
+ return withinTolerance && matched;
492
+ }
493
+
494
+ // src/webhooks/index.ts
495
+ var NO_DATABASE_CONTEXT = { database: void 0, products: void 0 };
496
+ function decodeBody(rawBody) {
497
+ return typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody);
498
+ }
499
+ function asRecord2(value) {
500
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
501
+ }
502
+ function extractNativeType(root, provider) {
503
+ const candidate = provider === "flutterwave" ? root.type ?? root.event : provider === "stripe" ? root.type : root.event;
504
+ return typeof candidate === "string" ? candidate : "unknown";
505
+ }
506
+ function extractWebhookId(root) {
507
+ return typeof root.id === "string" && root.id.length > 0 ? root.id : void 0;
508
+ }
509
+ function computeDedupeKey(provider, nativeType, data, webhookId) {
510
+ const idCandidate = data?.id ?? data?.reference ?? data?.transfer_code ?? data?.subscription_code ?? data?.customer_code;
511
+ const dataId = idCandidate !== void 0 && idCandidate !== null ? String(idCandidate) : "";
512
+ const status = typeof data?.status === "string" ? data.status : "";
513
+ if (provider === "flutterwave") {
514
+ if (webhookId !== void 0) return webhookId;
515
+ return `${dataId}:${status}`;
516
+ }
517
+ if (provider === "stripe" && webhookId !== void 0) return webhookId;
518
+ return createHash("sha256").update(`${nativeType}:${dataId}:${status}`).digest("hex");
519
+ }
520
+ function constructEvent(params) {
521
+ const { rawBody, headers, provider, version, verify, logger, billing } = params;
522
+ if (!verify({ rawBody, headers })) {
523
+ throw new PayweaveWebhookVerificationError("Webhook signature verification failed.", {
524
+ provider,
525
+ isRetryable: false
526
+ });
527
+ }
528
+ let parsed;
529
+ try {
530
+ parsed = JSON.parse(decodeBody(rawBody));
531
+ } catch (cause) {
532
+ throw new PayweaveWebhookVerificationError("Webhook body is not valid JSON.", {
533
+ provider,
534
+ isRetryable: false,
535
+ cause
536
+ });
537
+ }
538
+ const root = asRecord2(parsed) ?? {};
539
+ const data = root.data;
540
+ const nativeType = extractNativeType(root, provider);
541
+ const unifiedType = toUnifiedEventType(provider, version, nativeType, data);
542
+ const dataRecord = asRecord2(data);
543
+ if (provider !== "stripe" && typeof dataRecord?.status === "string") {
544
+ toUnifiedStatus(provider, version, dataRecord.status, logger);
545
+ }
546
+ const webhookId = provider === "flutterwave" || provider === "stripe" ? extractWebhookId(root) : void 0;
547
+ const dedupeKey = computeDedupeKey(provider, nativeType, dataRecord, webhookId);
548
+ const base = {
549
+ provider,
550
+ type: nativeType,
551
+ unifiedType,
552
+ data,
553
+ raw: parsed,
554
+ dedupeKey,
555
+ ...webhookId !== void 0 ? { id: webhookId } : {}
556
+ };
557
+ return {
558
+ ...base,
559
+ // always present (see the `WebhookEvent.apply` doc comment);
560
+ // `billing` is `undefined` for a database-less client, in which case
561
+ // `applyWebhookEvent` itself throws `PayweaveConfigError` when called.
562
+ apply: (options) => applyWebhookEvent(billing ?? NO_DATABASE_CONTEXT, base, { logger, ...options })
563
+ };
564
+ }
565
+
566
+ export { BILLING_CAPABLE_PROVIDERS, constructEvent, resolvePlanGroup, subscribe, verifyStripe };
567
+ //# sourceMappingURL=chunk-CGQ73I4I.js.map
568
+ //# sourceMappingURL=chunk-CGQ73I4I.js.map