@lunora/payment 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (40) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +3 -2
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/index.d.mts +20 -459
  5. package/dist/index.d.ts +20 -459
  6. package/dist/index.mjs +9 -11
  7. package/dist/packem_shared/{LunoraPaymentError-B3hEzXSs.mjs → LunoraPaymentError-BeQlhkZj.mjs} +4 -7
  8. package/dist/packem_shared/adapter.d-iFA55DzU.d.mts +312 -0
  9. package/dist/packem_shared/adapter.d-iFA55DzU.d.ts +312 -0
  10. package/dist/packem_shared/{addMoney-bCcs1nyw.mjs → addMoney-jSh_7TfF.mjs} +1 -1
  11. package/dist/packem_shared/{applyWebhookAction-DpAqf3Lw.mjs → applyWebhookAction-CLAx4svt.mjs} +1 -1
  12. package/dist/packem_shared/{constantTimeEqual-CfY0jYcL.mjs → constantTimeEqual-BQjoW85H.mjs} +14 -38
  13. package/dist/packem_shared/{createAdapterRegistry-BuDHFCBc.mjs → createAdapterRegistry-Ds7bx_TK.mjs} +1 -1
  14. package/dist/packem_shared/{createDatabasePaymentStore-bYB_HUE6.mjs → createDatabasePaymentStore-C0NUCj1H.mjs} +1 -1
  15. package/dist/packem_shared/{createPayment-BccfPGyw.mjs → createPayment-DzX-ji34.mjs} +17 -5
  16. package/dist/packem_shared/json-DhcPm8EO.mjs +11 -0
  17. package/dist/packem_shared/{lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs → lunoraDatabaseToPaymentDatabase-CL2vdAXq.mjs} +2 -2
  18. package/dist/packem_shared/not-supported-C0onRyia.mjs +7 -0
  19. package/dist/packem_shared/{reconcile-CI1ukJF9.mjs → reconcile-Dtn_ErbJ.mjs} +1 -1
  20. package/dist/packem_shared/subscription-event-CwEsWRCK.mjs +17 -0
  21. package/dist/providers/autumn-features.d.mts +94 -0
  22. package/dist/providers/autumn-features.d.ts +94 -0
  23. package/dist/providers/autumn-features.mjs +95 -0
  24. package/dist/providers/autumn.d.mts +19 -0
  25. package/dist/providers/autumn.d.ts +19 -0
  26. package/dist/providers/autumn.mjs +294 -0
  27. package/dist/providers/creem.d.mts +17 -0
  28. package/dist/providers/creem.d.ts +17 -0
  29. package/dist/providers/creem.mjs +205 -0
  30. package/dist/providers/dodopayments.d.mts +21 -0
  31. package/dist/providers/dodopayments.d.ts +21 -0
  32. package/dist/providers/dodopayments.mjs +259 -0
  33. package/dist/providers/polar.d.mts +22 -0
  34. package/dist/providers/polar.d.ts +22 -0
  35. package/dist/{packem_shared/createPolarAdapter-BJtVGSlF.mjs → providers/polar.mjs} +51 -47
  36. package/dist/providers/stripe.d.mts +23 -0
  37. package/dist/providers/stripe.d.ts +23 -0
  38. package/dist/{packem_shared/createStripeAdapter-D40MVBXg.mjs → providers/stripe.mjs} +56 -55
  39. package/package.json +46 -5
  40. package/dist/packem_shared/json-Db337f36.mjs +0 -6
@@ -1,7 +1,8 @@
1
- import idempotencyKey from './idempotencyKey-BFzDCA7g.mjs';
2
- import { a as asRecord, r as readString, c as readNumber, b as readBoolean } from './json-Db337f36.mjs';
3
- import { compareMoney, money, zeroMoney } from './addMoney-bCcs1nyw.mjs';
4
- import { verifyStripeSignature } from './constantTimeEqual-CfY0jYcL.mjs';
1
+ import { LunoraPaymentError } from '../packem_shared/LunoraPaymentError-BeQlhkZj.mjs';
2
+ import idempotencyKey from '../packem_shared/idempotencyKey-BFzDCA7g.mjs';
3
+ import { r as readString, c as readBoolean, b as readNumber, a as asRecord } from '../packem_shared/json-DhcPm8EO.mjs';
4
+ import { compareMoney, money, zeroMoney } from '../packem_shared/addMoney-jSh_7TfF.mjs';
5
+ import { s as stateToEventType } from '../packem_shared/subscription-event-CwEsWRCK.mjs';
5
6
 
6
7
  const PAYMENT_STATE_BY_STRIPE_STATUS = {
7
8
  canceled: "canceled",
@@ -15,7 +16,14 @@ const PAYMENT_STATE_BY_STRIPE_STATUS = {
15
16
  const SUBSCRIPTION_STATE_BY_STRIPE_STATUS = {
16
17
  active: "active",
17
18
  canceled: "canceled",
18
- incomplete: "trialing",
19
+ // SECURITY: `incomplete` means the FIRST payment has not succeeded (SCA
20
+ // `requires_action` or a failed initial charge). It must NOT map to an
21
+ // entitling state — Stripe's recommended `payment_behavior: "default_incomplete"`
22
+ // makes `incomplete` the initial status of every new subscription, so mapping
23
+ // it to `trialing` (which is in ACTIVE_STATES) would grant paid entitlements
24
+ // before any payment. Reserve `trialing` for a genuine Stripe trial (status
25
+ // `trialing`, mapped below); treat `incomplete` as non-entitling `past_due`.
26
+ incomplete: "past_due",
19
27
  incomplete_expired: "canceled",
20
28
  past_due: "past_due",
21
29
  paused: "paused",
@@ -31,61 +39,49 @@ const firstItem = (object) => {
31
39
  const firstPriceId = (object) => readString(asRecord(firstItem(object).price), "id");
32
40
  const firstQuantity = (object) => readNumber(firstItem(object), "quantity");
33
41
  const periodEndMs = (object) => {
34
- const seconds = readNumber(object, "current_period_end");
42
+ const seconds = readNumber(firstItem(object), "current_period_end") ?? readNumber(object, "current_period_end");
35
43
  return seconds === void 0 ? void 0 : seconds * 1e3;
36
44
  };
37
45
  const periodStartMs = (object) => {
38
- const seconds = readNumber(object, "current_period_start");
46
+ const seconds = readNumber(firstItem(object), "current_period_start") ?? readNumber(object, "current_period_start");
39
47
  return seconds === void 0 ? void 0 : seconds * 1e3;
40
48
  };
41
- const intentToSession = (intent) => {
42
- const amount = money(BigInt(intent.amount), intent.currency);
43
- const state = PAYMENT_STATE_BY_STRIPE_STATUS[intent.status] ?? "initiated";
49
+ const intentToSession = (input) => {
50
+ const intent = asRecord(input);
51
+ const currency = readString(intent, "currency") ?? "usd";
52
+ const amountValue = readNumber(intent, "amount") ?? 0;
53
+ const amount = money(BigInt(Math.round(amountValue)), currency);
54
+ const state = PAYMENT_STATE_BY_STRIPE_STATUS[readString(intent, "status") ?? ""] ?? "initiated";
44
55
  const now = Date.now();
45
56
  return {
46
57
  amount,
47
- capturedAmount: state === "captured" ? money(BigInt(intent.amount_received ?? intent.amount), intent.currency) : zeroMoney(intent.currency),
58
+ capturedAmount: state === "captured" ? money(BigInt(Math.round(readNumber(intent, "amount_received") ?? amountValue)), currency) : zeroMoney(currency),
48
59
  createdAt: now,
49
- id: intent.id,
60
+ id: readString(intent, "id") ?? "",
50
61
  provider: "stripe",
51
- referenceId: intent.metadata?.referenceId ?? "",
52
- refundedAmount: zeroMoney(intent.currency),
62
+ referenceId: readReferenceId(intent) ?? "",
63
+ refundedAmount: zeroMoney(currency),
53
64
  state,
54
65
  updatedAt: now
55
66
  };
56
67
  };
57
- const subscriptionFromStripe = (subscription) => {
68
+ const subscriptionFromStripe = (input) => {
69
+ const subscription = asRecord(input);
58
70
  const now = Date.now();
59
71
  return {
60
- cancelAtPeriodEnd: subscription.cancel_at_period_end ?? false,
72
+ cancelAtPeriodEnd: readBoolean(subscription, "cancel_at_period_end") ?? false,
61
73
  createdAt: now,
62
- currentPeriodEnd: subscription.current_period_end ? subscription.current_period_end * 1e3 : void 0,
63
- currentPeriodStart: subscription.current_period_start ? subscription.current_period_start * 1e3 : void 0,
64
- id: subscription.id,
65
- priceId: subscription.items?.data[0]?.price?.id ?? "",
74
+ currentPeriodEnd: periodEndMs(subscription),
75
+ currentPeriodStart: periodStartMs(subscription),
76
+ id: readString(subscription, "id") ?? "",
77
+ priceId: firstPriceId(subscription) ?? "",
66
78
  provider: "stripe",
67
- quantity: subscription.items?.data[0]?.quantity ?? 1,
68
- referenceId: subscription.metadata?.referenceId ?? "",
69
- state: SUBSCRIPTION_STATE_BY_STRIPE_STATUS[subscription.status] ?? "active",
79
+ quantity: firstQuantity(subscription) ?? 1,
80
+ referenceId: readReferenceId(subscription) ?? "",
81
+ state: SUBSCRIPTION_STATE_BY_STRIPE_STATUS[readString(subscription, "status") ?? ""] ?? "active",
70
82
  updatedAt: now
71
83
  };
72
84
  };
73
- const subscriptionEventType = (status) => {
74
- const state = status ? SUBSCRIPTION_STATE_BY_STRIPE_STATUS[status] : void 0;
75
- if (state === "canceled") {
76
- return "subscription.canceled";
77
- }
78
- if (state === "past_due") {
79
- return "subscription.past_due";
80
- }
81
- if (state === "paused") {
82
- return "subscription.paused";
83
- }
84
- if (state === "active" || state === "trialing") {
85
- return "subscription.active";
86
- }
87
- return "subscription.updated";
88
- };
89
85
  const mapEvent = (eventId, eventType, object) => {
90
86
  const base = { eventId, provider: "stripe", raw: { object, type: eventType } };
91
87
  const currency = readString(object, "currency") ?? "usd";
@@ -93,7 +89,7 @@ const mapEvent = (eventId, eventType, object) => {
93
89
  case "charge.refunded": {
94
90
  return {
95
91
  ...base,
96
- amount: money(BigInt(readNumber(object, "amount_refunded") ?? 0), currency),
92
+ amount: money(BigInt(Math.round(readNumber(object, "amount_refunded") ?? 0)), currency),
97
93
  amountKind: "absolute",
98
94
  referenceId: readReferenceId(object),
99
95
  sessionId: readString(object, "payment_intent") ?? readString(object, "id"),
@@ -102,18 +98,20 @@ const mapEvent = (eventId, eventType, object) => {
102
98
  }
103
99
  case "checkout.session.completed": {
104
100
  if (readString(object, "mode") === "subscription") {
101
+ const paymentStatus = readString(object, "payment_status");
102
+ const paid = paymentStatus === "paid" || paymentStatus === "no_payment_required";
105
103
  return {
106
104
  ...base,
107
105
  customerId: readString(object, "customer"),
108
106
  referenceId: readReferenceId(object),
109
107
  subscriptionId: readString(object, "subscription"),
110
- type: "subscription.active"
108
+ type: paid ? "subscription.active" : "subscription.updated"
111
109
  };
112
110
  }
113
111
  const amountTotal = readNumber(object, "amount_total");
114
112
  return {
115
113
  ...base,
116
- amount: amountTotal === void 0 ? void 0 : money(BigInt(amountTotal), currency),
114
+ amount: amountTotal === void 0 ? void 0 : money(BigInt(Math.round(amountTotal)), currency),
117
115
  customerId: readString(object, "customer"),
118
116
  referenceId: readReferenceId(object),
119
117
  sessionId: readString(object, "payment_intent") ?? readString(object, "id"),
@@ -132,7 +130,7 @@ const mapEvent = (eventId, eventType, object) => {
132
130
  quantity: firstQuantity(object),
133
131
  referenceId: readReferenceId(object),
134
132
  subscriptionId: readString(object, "id"),
135
- type: subscriptionEventType(readString(object, "status"))
133
+ type: stateToEventType(SUBSCRIPTION_STATE_BY_STRIPE_STATUS[readString(object, "status") ?? ""])
136
134
  };
137
135
  }
138
136
  case "customer.subscription.deleted": {
@@ -141,7 +139,7 @@ const mapEvent = (eventId, eventType, object) => {
141
139
  case "payment_intent.amount_capturable_updated": {
142
140
  return {
143
141
  ...base,
144
- amount: money(BigInt(readNumber(object, "amount") ?? 0), currency),
142
+ amount: money(BigInt(Math.round(readNumber(object, "amount") ?? 0)), currency),
145
143
  referenceId: readReferenceId(object),
146
144
  sessionId: readString(object, "id"),
147
145
  type: "payment.authorized"
@@ -153,7 +151,7 @@ const mapEvent = (eventId, eventType, object) => {
153
151
  case "payment_intent.succeeded": {
154
152
  return {
155
153
  ...base,
156
- amount: money(BigInt(readNumber(object, "amount_received") ?? readNumber(object, "amount") ?? 0), currency),
154
+ amount: money(BigInt(Math.round(readNumber(object, "amount_received") ?? readNumber(object, "amount") ?? 0)), currency),
157
155
  customerId: readString(object, "customer"),
158
156
  referenceId: readReferenceId(object),
159
157
  sessionId: readString(object, "id"),
@@ -166,7 +164,8 @@ const mapEvent = (eventId, eventType, object) => {
166
164
  }
167
165
  };
168
166
  const createStripeAdapter = (options) => {
169
- const { client, webhookSecret } = options;
167
+ const { webhookSecret } = options;
168
+ const client = options.client;
170
169
  return {
171
170
  cancelPayment: async (sessionId, paymentOptions) => {
172
171
  const intent = await client.paymentIntents.cancel(sessionId, void 0, { idempotencyKey: paymentOptions?.idempotencyKey });
@@ -218,10 +217,14 @@ const createStripeAdapter = (options) => {
218
217
  identifier: "stripe",
219
218
  parseWebhook: async ({ headers, payload }) => {
220
219
  const signatureHeader = headers.get("stripe-signature") ?? "";
221
- await verifyStripeSignature({ payload, secret: webhookSecret, signatureHeader, toleranceSeconds: options.webhookToleranceSeconds });
222
- const event = asRecord(JSON.parse(payload));
220
+ let event;
221
+ try {
222
+ event = await client.webhooks.constructEventAsync(payload, signatureHeader, webhookSecret, options.webhookToleranceSeconds);
223
+ } catch (error) {
224
+ throw new LunoraPaymentError("WEBHOOK_SIGNATURE_INVALID", error instanceof Error ? error.message : "invalid webhook signature");
225
+ }
223
226
  const object = asRecord(asRecord(event.data).object);
224
- return mapEvent(readString(event, "id") ?? "", readString(event, "type") ?? "", object);
227
+ return mapEvent(event.id, event.type, object);
225
228
  },
226
229
  refundPayment: async (input) => {
227
230
  await client.refunds.create(
@@ -243,7 +246,7 @@ const createStripeAdapter = (options) => {
243
246
  {
244
247
  event_name: input.featureId,
245
248
  identifier: input.idempotencyKey,
246
- payload: { stripe_customer_id: input.customerId, value: String(input.quantity) },
249
+ payload: { stripe_customer_id: input.customerId ?? "", value: String(input.quantity) },
247
250
  timestamp: input.timestamp === void 0 ? void 0 : Math.floor(input.timestamp / 1e3)
248
251
  },
249
252
  { idempotencyKey: input.idempotencyKey }
@@ -255,11 +258,9 @@ const createStripeAdapter = (options) => {
255
258
  },
256
259
  updateSubscription: async (subscriptionId, patch) => {
257
260
  const parameters = {};
258
- if (patch.quantity !== void 0) {
259
- parameters.quantity = patch.quantity;
260
- }
261
- if (patch.priceId) {
262
- parameters.items = [{ price: patch.priceId }];
261
+ if (patch.priceId !== void 0 || patch.quantity !== void 0) {
262
+ const current = await client.subscriptions.retrieve(subscriptionId);
263
+ parameters.items = [{ id: current.items.data[0]?.id, price: patch.priceId, quantity: patch.quantity }];
263
264
  }
264
265
  const subscription = await client.subscriptions.update(subscriptionId, parameters);
265
266
  return subscriptionFromStripe(subscription);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/payment",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.21",
4
4
  "description": "Provider-agnostic payments for Lunora: Stripe-first adapter, webhook sync, and subscription/payment state machine",
5
5
  "keywords": [
6
6
  "billing",
@@ -26,7 +26,7 @@
26
26
  "directory": "packages/payment"
27
27
  },
28
28
  "files": [
29
- "dist",
29
+ "./dist",
30
30
  "__assets__",
31
31
  "README.md",
32
32
  "LICENSE.md"
@@ -41,20 +41,61 @@
41
41
  "types": "./dist/index.d.ts",
42
42
  "import": "./dist/index.mjs"
43
43
  },
44
+ "./stripe": {
45
+ "types": "./dist/providers/stripe.d.ts",
46
+ "import": "./dist/providers/stripe.mjs"
47
+ },
48
+ "./polar": {
49
+ "types": "./dist/providers/polar.d.ts",
50
+ "import": "./dist/providers/polar.mjs"
51
+ },
52
+ "./autumn": {
53
+ "types": "./dist/providers/autumn.d.ts",
54
+ "import": "./dist/providers/autumn.mjs"
55
+ },
56
+ "./autumn-features": {
57
+ "types": "./dist/providers/autumn-features.d.ts",
58
+ "import": "./dist/providers/autumn-features.mjs"
59
+ },
60
+ "./dodopayments": {
61
+ "types": "./dist/providers/dodopayments.d.ts",
62
+ "import": "./dist/providers/dodopayments.mjs"
63
+ },
64
+ "./creem": {
65
+ "types": "./dist/providers/creem.d.ts",
66
+ "import": "./dist/providers/creem.mjs"
67
+ },
44
68
  "./package.json": "./package.json"
45
69
  },
46
70
  "publishConfig": {
47
71
  "access": "public"
48
72
  },
49
73
  "dependencies": {
50
- "@lunora/server": "1.0.0-alpha.2",
51
- "@lunora/values": "1.0.0-alpha.1",
74
+ "@lunora/errors": "1.0.0-alpha.3",
75
+ "@lunora/server": "1.0.0-alpha.21",
76
+ "@lunora/values": "1.0.0-alpha.6",
52
77
  "dinero.js": "2.0.2"
53
78
  },
54
79
  "peerDependencies": {
55
- "stripe": "^19.0.0"
80
+ "@polar-sh/sdk": "^0.48.0",
81
+ "autumn-js": "^1.2.0",
82
+ "creem": "^1.5.0",
83
+ "dodopayments": "^2.41.0",
84
+ "stripe": "^22.0.0"
56
85
  },
57
86
  "peerDependenciesMeta": {
87
+ "@polar-sh/sdk": {
88
+ "optional": true
89
+ },
90
+ "autumn-js": {
91
+ "optional": true
92
+ },
93
+ "creem": {
94
+ "optional": true
95
+ },
96
+ "dodopayments": {
97
+ "optional": true
98
+ },
58
99
  "stripe": {
59
100
  "optional": true
60
101
  }
@@ -1,6 +0,0 @@
1
- const asRecord = (value) => typeof value === "object" && value !== null ? value : {};
2
- const readString = (object, key) => typeof object[key] === "string" ? object[key] : void 0;
3
- const readNumber = (object, key) => typeof object[key] === "number" ? object[key] : void 0;
4
- const readBoolean = (object, key) => typeof object[key] === "boolean" ? object[key] : void 0;
5
-
6
- export { asRecord as a, readBoolean as b, readNumber as c, readString as r };