@lunora/payment 1.0.0-alpha.7 → 1.0.0-alpha.71
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/LICENSE.md +6 -0
- package/README.md +5 -2
- package/dist/index.d.mts +293 -526
- package/dist/index.d.ts +293 -526
- package/dist/index.mjs +1 -16
- package/dist/packem_shared/LunoraPaymentError-C8uAUIzQ.mjs +1 -0
- package/dist/packem_shared/MemoryPaymentStore-CkXqJEe2.mjs +1 -0
- package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DVNbZxBy.mjs +1 -0
- package/dist/packem_shared/adapter.d-xBfAhqVC.d.mts +445 -0
- package/dist/packem_shared/adapter.d-xBfAhqVC.d.ts +445 -0
- package/dist/packem_shared/addMoney-BKfsSf0H.mjs +1 -0
- package/dist/packem_shared/applyWebhookAction-Bwvycl00.mjs +1 -0
- package/dist/packem_shared/constantTimeEqual-B4DP75aY.mjs +1 -0
- package/dist/packem_shared/createAdapterRegistry-SoXdsiQd.mjs +1 -0
- package/dist/packem_shared/createDatabasePaymentStore-DxChfP5X.mjs +1 -0
- package/dist/packem_shared/createPayment-Cj0tRPRZ.mjs +1 -0
- package/dist/packem_shared/entitlementsForReference-BbEOKu5d.mjs +1 -0
- package/dist/packem_shared/idempotencyKey-CfJKqcE3.mjs +1 -0
- package/dist/packem_shared/json-D85Nbd92.mjs +1 -0
- package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-CW1VjW4D.mjs +1 -0
- package/dist/packem_shared/not-supported-BBbwyP_u.mjs +1 -0
- package/dist/packem_shared/observability-C9tz8mj_.mjs +1 -0
- package/dist/packem_shared/paymentTables-Be-_11ux.mjs +1 -0
- package/dist/packem_shared/reconcile-BjWV0mB0.mjs +1 -0
- package/dist/packem_shared/subscription-event-BosLshSM.mjs +1 -0
- package/dist/providers/autumn-features.d.mts +129 -0
- package/dist/providers/autumn-features.d.ts +129 -0
- package/dist/providers/autumn-features.mjs +1 -0
- package/dist/providers/autumn.d.mts +28 -0
- package/dist/providers/autumn.d.ts +28 -0
- package/dist/providers/autumn.mjs +1 -0
- package/dist/providers/creem.d.mts +26 -0
- package/dist/providers/creem.d.ts +26 -0
- package/dist/providers/creem.mjs +1 -0
- package/dist/providers/dodopayments.d.mts +30 -0
- package/dist/providers/dodopayments.d.ts +30 -0
- package/dist/providers/dodopayments.mjs +1 -0
- package/dist/providers/polar.d.mts +31 -0
- package/dist/providers/polar.d.ts +31 -0
- package/dist/providers/polar.mjs +1 -0
- package/dist/providers/stripe.d.mts +32 -0
- package/dist/providers/stripe.d.ts +32 -0
- package/dist/providers/stripe.mjs +1 -0
- package/package.json +45 -4
- package/dist/packem_shared/LunoraPaymentError-B3hEzXSs.mjs +0 -22
- package/dist/packem_shared/MemoryPaymentStore-DvgdWa3C.mjs +0 -72
- package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DrxV0clv.mjs +0 -26
- package/dist/packem_shared/addMoney-bCcs1nyw.mjs +0 -60
- package/dist/packem_shared/applyWebhookAction-DpAqf3Lw.mjs +0 -177
- package/dist/packem_shared/constantTimeEqual-CfY0jYcL.mjs +0 -95
- package/dist/packem_shared/createAdapterRegistry-BuDHFCBc.mjs +0 -24
- package/dist/packem_shared/createDatabasePaymentStore-bYB_HUE6.mjs +0 -172
- package/dist/packem_shared/createPayment-BccfPGyw.mjs +0 -190
- package/dist/packem_shared/createPolarAdapter-BJtVGSlF.mjs +0 -217
- package/dist/packem_shared/createStripeAdapter-D40MVBXg.mjs +0 -270
- package/dist/packem_shared/entitlementsForReference-CzZGXPoZ.mjs +0 -51
- package/dist/packem_shared/idempotencyKey-BFzDCA7g.mjs +0 -3
- package/dist/packem_shared/json-Db337f36.mjs +0 -6
- package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs +0 -29
- package/dist/packem_shared/observability-CvhJ205g.mjs +0 -11
- package/dist/packem_shared/paymentTables-DccHwWr_.mjs +0 -127
- package/dist/packem_shared/reconcile-CI1ukJF9.mjs +0 -73
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraPaymentError as A}from"../packem_shared/LunoraPaymentError-C8uAUIzQ.mjs";import{idempotencyKey as P}from"../packem_shared/idempotencyKey-CfJKqcE3.mjs";import{r as a,e as b,b as o,a as u}from"../packem_shared/json-D85Nbd92.mjs";import{compareMoney as S,money as m,zeroMoney as f}from"../packem_shared/addMoney-BKfsSf0H.mjs";import{i as q}from"../packem_shared/subscription-event-BosLshSM.mjs";const E={canceled:"canceled",processing:"authorized",requires_action:"authorized",requires_capture:"authorized",requires_confirmation:"initiated",requires_payment_method:"initiated",succeeded:"captured"},v={active:"active",canceled:"canceled",incomplete:"past_due",incomplete_expired:"canceled",past_due:"past_due",paused:"paused",trialing:"trialing",unpaid:"past_due"},s=i=>a(u(i.metadata),"referenceId")??a(i,"client_reference_id"),_=i=>{const n=u(i.items),t=Array.isArray(n.data)?n.data:[];return u(t[0])},h=i=>a(u(_(i).price),"id"),w=i=>o(_(i),"quantity"),g=i=>{const n=o(_(i),"current_period_end")??o(i,"current_period_end");return n===void 0?void 0:n*1e3},K=i=>{const n=o(_(i),"current_period_start")??o(i,"current_period_start");return n===void 0?void 0:n*1e3},y=i=>{const n=u(i),t=a(n,"currency")??"usd",e=o(n,"amount")??0,r=m(BigInt(Math.round(e)),t),c=E[a(n,"status")??""]??"initiated",d=Date.now();return{amount:r,capturedAmount:c==="captured"?m(BigInt(Math.round(o(n,"amount_received")??e)),t):f(t),createdAt:d,id:a(n,"id")??"",provider:"stripe",referenceId:s(n)??"",refundedAmount:f(t),state:c,updatedAt:d}},l=i=>{const n=u(i),t=Date.now();return{cancelAtPeriodEnd:b(n,"cancel_at_period_end")??!1,createdAt:t,currentPeriodEnd:g(n),currentPeriodStart:K(n),id:a(n,"id")??"",priceId:h(n)??"",provider:"stripe",quantity:w(n)??1,referenceId:s(n)??"",state:v[a(n,"status")??""]??"active",updatedAt:t}},M=(i,n,t)=>{const e={eventId:i,provider:"stripe",raw:{object:t,type:n}},r=a(t,"currency")??"usd";switch(n){case"charge.refunded":return{...e,amount:m(BigInt(Math.round(o(t,"amount_refunded")??0)),r),amountKind:"absolute",referenceId:s(t),sessionId:a(t,"payment_intent")??a(t,"id"),type:"payment.refunded"};case"checkout.session.completed":{if(a(t,"mode")==="subscription"){const d=a(t,"payment_status"),p=d==="paid"||d==="no_payment_required";return{...e,customerId:a(t,"customer"),referenceId:s(t),subscriptionId:a(t,"subscription"),type:p?"subscription.active":"subscription.updated"}}const c=o(t,"amount_total");return{...e,amount:c===void 0?void 0:m(BigInt(Math.round(c)),r),customerId:a(t,"customer"),referenceId:s(t),sessionId:a(t,"payment_intent")??a(t,"id"),type:"payment.captured"}}case"customer.subscription.created":case"customer.subscription.updated":return{...e,cancelAtPeriodEnd:b(t,"cancel_at_period_end"),currentPeriodEnd:g(t),currentPeriodStart:K(t),customerId:a(t,"customer"),priceId:h(t),quantity:w(t),referenceId:s(t),subscriptionId:a(t,"id"),type:q(v[a(t,"status")??""])};case"customer.subscription.deleted":return{...e,referenceId:s(t),subscriptionId:a(t,"id"),type:"subscription.canceled"};case"payment_intent.amount_capturable_updated":return{...e,amount:m(BigInt(Math.round(o(t,"amount")??0)),r),referenceId:s(t),sessionId:a(t,"id"),type:"payment.authorized"};case"payment_intent.payment_failed":return{...e,referenceId:s(t),sessionId:a(t,"id"),type:"payment.failed"};case"payment_intent.succeeded":return{...e,amount:m(BigInt(Math.round(o(t,"amount_received")??o(t,"amount")??0)),r),customerId:a(t,"customer"),referenceId:s(t),sessionId:a(t,"id"),type:"payment.captured"};default:return{...e,type:"unhandled"}}},D=i=>{const{webhookSecret:n}=i,t=i.client;return{cancelPayment:async(e,r)=>{const c=await t.paymentIntents.cancel(e,void 0,{idempotencyKey:r?.idempotencyKey});return y(c)},cancelSubscription:async(e,r)=>{const c=r?.atPeriodEnd?await t.subscriptions.update(e,{cancel_at_period_end:!0},{idempotencyKey:r.idempotencyKey}):await t.subscriptions.cancel(e,void 0,{idempotencyKey:r?.idempotencyKey});return l(c)},capabilities:{merchantOfRecord:!1,portal:!0,usageMetering:!0},capturePayment:async e=>{const r=await t.paymentIntents.capture(e.sessionId,e.amount?{amount_to_capture:Number(e.amount.minorUnits)}:void 0,{idempotencyKey:e.idempotencyKey});return y(r)},createCheckout:async e=>{const r=await t.checkout.sessions.create({cancel_url:e.cancelUrl,client_reference_id:e.referenceId,customer:e.customerId,line_items:[{price:e.priceId,quantity:e.quantity??1}],metadata:{...e.metadata,referenceId:e.referenceId},mode:e.mode,subscription_data:e.mode==="subscription"?{metadata:{referenceId:e.referenceId}}:void 0,success_url:e.successUrl},{idempotencyKey:e.idempotencyKey});return{id:r.id,provider:"stripe",url:r.url??""}},createPortalSession:async e=>({url:(await t.billingPortal.sessions.create({customer:e.customerId,return_url:e.returnUrl})).url}),getOrCreateCustomer:async e=>{const r=await t.customers.create({email:e.email,metadata:{...e.metadata,referenceId:e.referenceId}},{idempotencyKey:P("customer","stripe",e.referenceId)});return{createdAt:Date.now(),email:r.email??void 0,id:r.id,provider:"stripe",referenceId:e.referenceId}},getPaymentStatus:async e=>y(await t.paymentIntents.retrieve(e)),getSubscriptionStatus:async e=>l(await t.subscriptions.retrieve(e)),identifier:"stripe",parseWebhook:async({headers:e,payload:r})=>{const c=e.get("stripe-signature")??"";let d;try{d=await t.webhooks.constructEventAsync(r,c,n,i.webhookToleranceSeconds)}catch(I){throw new A("WEBHOOK_SIGNATURE_INVALID",I instanceof Error?I.message:"invalid webhook signature")}const p=u(u(d.data).object);return M(d.id,d.type,p)},refundPayment:async e=>{await t.refunds.create({amount:e.amount?Number(e.amount.minorUnits):void 0,payment_intent:e.sessionId,reason:e.reason},{idempotencyKey:e.idempotencyKey});const r=await t.paymentIntents.retrieve(e.sessionId),c=y(r),d=e.amount??c.capturedAmount,p=e.amount!==void 0&&S(e.amount,c.capturedAmount)<0;return{...c,refundedAmount:d,state:p?"partially_refunded":"refunded"}},reportUsage:async e=>{await t.billing.meterEvents.create({event_name:e.featureId,identifier:e.idempotencyKey,payload:{stripe_customer_id:e.customerId??"",value:String(e.quantity)},timestamp:e.timestamp===void 0?void 0:Math.floor(e.timestamp/1e3)},{idempotencyKey:e.idempotencyKey})},resumeSubscription:async e=>{const r=await t.subscriptions.update(e,{cancel_at_period_end:!1});return l(r)},updateSubscription:async(e,r)=>{const c={};if(r.priceId!==void 0||r.quantity!==void 0){const p=await t.subscriptions.retrieve(e);c.items=[{id:p.items.data[0]?.id,price:r.priceId,quantity:r.quantity}]}const d=await t.subscriptions.update(e,c);return l(d)}}};export{D as createStripeAdapter};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/payment",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.71",
|
|
4
4
|
"description": "Provider-agnostic payments for Lunora: Stripe-first adapter, webhook sync, and subscription/payment state machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"billing",
|
|
@@ -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/
|
|
51
|
-
"@lunora/
|
|
74
|
+
"@lunora/errors": "1.0.0-alpha.20",
|
|
75
|
+
"@lunora/server": "1.0.0-alpha.70",
|
|
76
|
+
"@lunora/values": "1.0.0-alpha.25",
|
|
52
77
|
"dinero.js": "2.0.2"
|
|
53
78
|
},
|
|
54
79
|
"peerDependencies": {
|
|
55
|
-
"
|
|
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,22 +0,0 @@
|
|
|
1
|
-
const STATUS_BY_CODE = {
|
|
2
|
-
CONFIG_INVALID: 500,
|
|
3
|
-
CURRENCY_MISMATCH: 400,
|
|
4
|
-
FORBIDDEN: 403,
|
|
5
|
-
INVALID_TRANSITION: 409,
|
|
6
|
-
NOT_FOUND: 404,
|
|
7
|
-
PROVIDER_ERROR: 502,
|
|
8
|
-
WEBHOOK_SIGNATURE_INVALID: 400,
|
|
9
|
-
WEBHOOK_TIMESTAMP_INVALID: 400
|
|
10
|
-
};
|
|
11
|
-
class LunoraPaymentError extends Error {
|
|
12
|
-
code;
|
|
13
|
-
status;
|
|
14
|
-
constructor(code, message) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = "LunoraPaymentError";
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.status = STATUS_BY_CODE[code];
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export { LunoraPaymentError };
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
const customerKey = (provider, referenceId) => `${provider}:${referenceId}`;
|
|
2
|
-
const recordKey = (provider, id) => `${provider}:${id}`;
|
|
3
|
-
class MemoryPaymentStore {
|
|
4
|
-
customers = /* @__PURE__ */ new Map();
|
|
5
|
-
processedEvents = /* @__PURE__ */ new Set();
|
|
6
|
-
sessions = /* @__PURE__ */ new Map();
|
|
7
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
8
|
-
usageEvents = /* @__PURE__ */ new Map();
|
|
9
|
-
getCustomerByReference(provider, referenceId) {
|
|
10
|
-
return Promise.resolve(this.customers.get(customerKey(provider, referenceId)));
|
|
11
|
-
}
|
|
12
|
-
getPaymentSession(provider, id) {
|
|
13
|
-
return Promise.resolve(this.sessions.get(recordKey(provider, id)));
|
|
14
|
-
}
|
|
15
|
-
getSubscription(provider, id) {
|
|
16
|
-
return Promise.resolve(this.subscriptions.get(recordKey(provider, id)));
|
|
17
|
-
}
|
|
18
|
-
listSubscriptionsByReference(referenceId) {
|
|
19
|
-
return Promise.resolve([...this.subscriptions.values()].filter((subscription) => subscription.referenceId === referenceId));
|
|
20
|
-
}
|
|
21
|
-
markEventProcessed(provider, eventId) {
|
|
22
|
-
const key = recordKey(provider, eventId);
|
|
23
|
-
if (this.processedEvents.has(key)) {
|
|
24
|
-
return Promise.resolve(false);
|
|
25
|
-
}
|
|
26
|
-
this.processedEvents.add(key);
|
|
27
|
-
return Promise.resolve(true);
|
|
28
|
-
}
|
|
29
|
-
releaseEvent(provider, eventId) {
|
|
30
|
-
this.processedEvents.delete(recordKey(provider, eventId));
|
|
31
|
-
return Promise.resolve();
|
|
32
|
-
}
|
|
33
|
-
markUsageReported(provider, idempotencyKey) {
|
|
34
|
-
const key = recordKey(provider, idempotencyKey);
|
|
35
|
-
const existing = this.usageEvents.get(key);
|
|
36
|
-
if (existing) {
|
|
37
|
-
this.usageEvents.set(key, { ...existing, reportedToProvider: true });
|
|
38
|
-
}
|
|
39
|
-
return Promise.resolve();
|
|
40
|
-
}
|
|
41
|
-
recordUsage(event) {
|
|
42
|
-
const key = recordKey(event.provider, event.idempotencyKey);
|
|
43
|
-
if (this.usageEvents.has(key)) {
|
|
44
|
-
return Promise.resolve(false);
|
|
45
|
-
}
|
|
46
|
-
this.usageEvents.set(key, event);
|
|
47
|
-
return Promise.resolve(true);
|
|
48
|
-
}
|
|
49
|
-
sumUsage(referenceId, featureId, since) {
|
|
50
|
-
let total = 0;
|
|
51
|
-
for (const event of this.usageEvents.values()) {
|
|
52
|
-
if (event.referenceId === referenceId && event.featureId === featureId && event.createdAt >= since) {
|
|
53
|
-
total += event.quantity;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return Promise.resolve(total);
|
|
57
|
-
}
|
|
58
|
-
upsertCustomer(customer) {
|
|
59
|
-
this.customers.set(customerKey(customer.provider, customer.referenceId), customer);
|
|
60
|
-
return Promise.resolve();
|
|
61
|
-
}
|
|
62
|
-
upsertPaymentSession(session) {
|
|
63
|
-
this.sessions.set(recordKey(session.provider, session.id), session);
|
|
64
|
-
return Promise.resolve();
|
|
65
|
-
}
|
|
66
|
-
upsertSubscription(subscription) {
|
|
67
|
-
this.subscriptions.set(recordKey(subscription.provider, subscription.id), subscription);
|
|
68
|
-
return Promise.resolve();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export { MemoryPaymentStore };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const PAYMENT_TRANSITIONS = {
|
|
2
|
-
authorized: { cancel: "canceled", capture: "captured", fail: "failed" },
|
|
3
|
-
canceled: {},
|
|
4
|
-
captured: { partial_refund: "partially_refunded", refund: "refunded" },
|
|
5
|
-
failed: {},
|
|
6
|
-
// A webhook can land before our local record exists, so "initiated" accepts the same
|
|
7
|
-
// outcomes a fresh intent could reach directly.
|
|
8
|
-
initiated: { authorize: "authorized", cancel: "canceled", capture: "captured", fail: "failed" },
|
|
9
|
-
partially_refunded: { partial_refund: "partially_refunded", refund: "refunded" },
|
|
10
|
-
refunded: {}
|
|
11
|
-
};
|
|
12
|
-
const SUBSCRIPTION_TRANSITIONS = {
|
|
13
|
-
active: { cancel: "canceled", mark_past_due: "past_due", pause: "paused", renew: "active" },
|
|
14
|
-
canceled: {},
|
|
15
|
-
past_due: { activate: "active", cancel: "canceled", pause: "paused", renew: "active" },
|
|
16
|
-
paused: { cancel: "canceled", resume: "active" },
|
|
17
|
-
trialing: { activate: "active", cancel: "canceled", mark_past_due: "past_due" }
|
|
18
|
-
};
|
|
19
|
-
const PAYMENT_TERMINAL_STATES = /* @__PURE__ */ new Set(["canceled", "failed", "refunded"]);
|
|
20
|
-
const SUBSCRIPTION_TERMINAL_STATES = /* @__PURE__ */ new Set(["canceled"]);
|
|
21
|
-
const nextPaymentState = (from, action) => PAYMENT_TRANSITIONS[from][action];
|
|
22
|
-
const canTransitionPayment = (from, action) => nextPaymentState(from, action) !== void 0;
|
|
23
|
-
const nextSubscriptionState = (from, action) => SUBSCRIPTION_TRANSITIONS[from][action];
|
|
24
|
-
const canTransitionSubscription = (from, action) => nextSubscriptionState(from, action) !== void 0;
|
|
25
|
-
|
|
26
|
-
export { PAYMENT_TERMINAL_STATES, SUBSCRIPTION_TERMINAL_STATES, canTransitionPayment, canTransitionSubscription, nextPaymentState, nextSubscriptionState };
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { toSnapshot } from 'dinero.js';
|
|
2
|
-
import { add, allocate, compare, subtract, dinero } from 'dinero.js/bigint';
|
|
3
|
-
import { LunoraPaymentError } from './LunoraPaymentError-B3hEzXSs.mjs';
|
|
4
|
-
|
|
5
|
-
const ZERO_DECIMAL = /* @__PURE__ */ new Set(["BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF"]);
|
|
6
|
-
const THREE_DECIMAL = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
|
|
7
|
-
const exponentFor = (code) => {
|
|
8
|
-
if (ZERO_DECIMAL.has(code)) {
|
|
9
|
-
return 0n;
|
|
10
|
-
}
|
|
11
|
-
return THREE_DECIMAL.has(code) ? 3n : 2n;
|
|
12
|
-
};
|
|
13
|
-
const currencyFor = (code) => {
|
|
14
|
-
return { base: 10n, code: code.toUpperCase(), exponent: exponentFor(code.toUpperCase()) };
|
|
15
|
-
};
|
|
16
|
-
const toDinero = (value) => dinero({ amount: value.minorUnits, currency: currencyFor(value.currency) });
|
|
17
|
-
const fromDinero = (value) => {
|
|
18
|
-
const snapshot = toSnapshot(value);
|
|
19
|
-
return { currency: snapshot.currency.code, minorUnits: snapshot.amount };
|
|
20
|
-
};
|
|
21
|
-
const assertSameCurrency = (a, b) => {
|
|
22
|
-
if (a.currency !== b.currency) {
|
|
23
|
-
throw new LunoraPaymentError("CURRENCY_MISMATCH", `cannot combine ${a.currency} with ${b.currency}`);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const isZeroDecimalCurrency = (currency) => exponentFor(currency.toUpperCase()) === 0n;
|
|
27
|
-
const money = (minorUnits, currency) => {
|
|
28
|
-
const units = typeof minorUnits === "bigint" ? minorUnits : BigInt(Math.trunc(minorUnits));
|
|
29
|
-
return { currency: currency.toUpperCase(), minorUnits: units };
|
|
30
|
-
};
|
|
31
|
-
const zeroMoney = (currency) => money(0n, currency);
|
|
32
|
-
const formatMoney = (value, locale = "en-US") => {
|
|
33
|
-
const exponent = Number(exponentFor(value.currency.toUpperCase()));
|
|
34
|
-
const amount = Number(value.minorUnits) / 10 ** exponent;
|
|
35
|
-
try {
|
|
36
|
-
return new Intl.NumberFormat(locale, { currency: value.currency, style: "currency" }).format(amount);
|
|
37
|
-
} catch {
|
|
38
|
-
return `${amount.toFixed(exponent)} ${value.currency}`;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
const addMoney = (a, b) => {
|
|
42
|
-
assertSameCurrency(a, b);
|
|
43
|
-
return fromDinero(add(toDinero(a), toDinero(b)));
|
|
44
|
-
};
|
|
45
|
-
const subtractMoney = (a, b) => {
|
|
46
|
-
assertSameCurrency(a, b);
|
|
47
|
-
return fromDinero(subtract(toDinero(a), toDinero(b)));
|
|
48
|
-
};
|
|
49
|
-
const compareMoney = (a, b) => {
|
|
50
|
-
assertSameCurrency(a, b);
|
|
51
|
-
return compare(toDinero(a), toDinero(b));
|
|
52
|
-
};
|
|
53
|
-
const allocateMoney = (amount, ratios) => allocate(toDinero(amount), [...ratios]).map((part) => fromDinero(part));
|
|
54
|
-
const isZeroMoney = (a) => a.minorUnits === 0n;
|
|
55
|
-
const toMoneyJSON = (m) => {
|
|
56
|
-
return { currency: m.currency, minorUnits: m.minorUnits.toString() };
|
|
57
|
-
};
|
|
58
|
-
const fromMoneyJSON = (json) => money(BigInt(json.minorUnits), json.currency);
|
|
59
|
-
|
|
60
|
-
export { addMoney, allocateMoney, compareMoney, formatMoney, fromMoneyJSON, isZeroDecimalCurrency, isZeroMoney, money, subtractMoney, toMoneyJSON, zeroMoney };
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { compareMoney, zeroMoney, addMoney } from './addMoney-bCcs1nyw.mjs';
|
|
2
|
-
import { n as notifyObserver } from './observability-CvhJ205g.mjs';
|
|
3
|
-
import { nextPaymentState, nextSubscriptionState } from './PAYMENT_TERMINAL_STATES-DrxV0clv.mjs';
|
|
4
|
-
|
|
5
|
-
const PAYMENT_ACTION_BY_TYPE = {
|
|
6
|
-
"payment.authorized": "authorize",
|
|
7
|
-
"payment.captured": "capture",
|
|
8
|
-
"payment.failed": "fail",
|
|
9
|
-
"payment.refunded": "refund"
|
|
10
|
-
};
|
|
11
|
-
const SUBSCRIPTION_STATE_BY_TYPE = {
|
|
12
|
-
"subscription.active": "active",
|
|
13
|
-
"subscription.canceled": "canceled",
|
|
14
|
-
"subscription.past_due": "past_due",
|
|
15
|
-
"subscription.paused": "paused"
|
|
16
|
-
};
|
|
17
|
-
const SUBSCRIPTION_ACTION_BY_TYPE = {
|
|
18
|
-
"subscription.active": "activate",
|
|
19
|
-
"subscription.canceled": "cancel",
|
|
20
|
-
"subscription.past_due": "mark_past_due",
|
|
21
|
-
"subscription.paused": "pause"
|
|
22
|
-
};
|
|
23
|
-
const maxMoney = (a, b) => compareMoney(a, b) > 0 ? a : b;
|
|
24
|
-
const refundedTotalFor = (base, action) => {
|
|
25
|
-
if (!action.amount) {
|
|
26
|
-
return void 0;
|
|
27
|
-
}
|
|
28
|
-
if (base.refundedAmount.currency !== action.amount.currency || base.capturedAmount.currency !== action.amount.currency) {
|
|
29
|
-
return void 0;
|
|
30
|
-
}
|
|
31
|
-
const prospective = action.amountKind === "absolute" ? maxMoney(action.amount, base.refundedAmount) : addMoney(base.refundedAmount, action.amount);
|
|
32
|
-
if (compareMoney(prospective, base.capturedAmount) > 0) {
|
|
33
|
-
return void 0;
|
|
34
|
-
}
|
|
35
|
-
return prospective;
|
|
36
|
-
};
|
|
37
|
-
const applyPayment = async (store, action, paymentAction) => {
|
|
38
|
-
if (!action.sessionId) {
|
|
39
|
-
return { applied: false, reason: "unhandled" };
|
|
40
|
-
}
|
|
41
|
-
const existing = await store.getPaymentSession(action.provider, action.sessionId);
|
|
42
|
-
const fromState = existing?.state ?? "initiated";
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
const currency = action.amount?.currency ?? existing?.amount.currency ?? "USD";
|
|
45
|
-
let resolvedAction = paymentAction;
|
|
46
|
-
if (paymentAction === "refund" && existing) {
|
|
47
|
-
const prospective = refundedTotalFor(existing, action);
|
|
48
|
-
if (prospective && compareMoney(prospective, existing.capturedAmount) < 0) {
|
|
49
|
-
resolvedAction = "partial_refund";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const toState = nextPaymentState(fromState, resolvedAction);
|
|
53
|
-
if (!toState) {
|
|
54
|
-
return { applied: false, reason: "illegal_transition" };
|
|
55
|
-
}
|
|
56
|
-
const base = existing ?? {
|
|
57
|
-
amount: action.amount ?? zeroMoney(currency),
|
|
58
|
-
capturedAmount: zeroMoney(currency),
|
|
59
|
-
createdAt: now,
|
|
60
|
-
id: action.sessionId,
|
|
61
|
-
provider: action.provider,
|
|
62
|
-
referenceId: action.referenceId ?? "",
|
|
63
|
-
refundedAmount: zeroMoney(currency),
|
|
64
|
-
state: fromState,
|
|
65
|
-
updatedAt: now
|
|
66
|
-
};
|
|
67
|
-
let { capturedAmount, refundedAmount } = base;
|
|
68
|
-
if (resolvedAction === "capture" && action.amount) {
|
|
69
|
-
capturedAmount = action.amount;
|
|
70
|
-
}
|
|
71
|
-
if ((resolvedAction === "partial_refund" || resolvedAction === "refund") && action.amount) {
|
|
72
|
-
const prospective = refundedTotalFor(base, action);
|
|
73
|
-
if (!prospective) {
|
|
74
|
-
return { applied: false, reason: "invalid_refund_amount" };
|
|
75
|
-
}
|
|
76
|
-
refundedAmount = prospective;
|
|
77
|
-
}
|
|
78
|
-
await store.upsertPaymentSession({
|
|
79
|
-
...base,
|
|
80
|
-
capturedAmount,
|
|
81
|
-
referenceId: action.referenceId ?? base.referenceId,
|
|
82
|
-
refundedAmount,
|
|
83
|
-
state: toState,
|
|
84
|
-
updatedAt: now
|
|
85
|
-
});
|
|
86
|
-
return { applied: true, reason: "ok" };
|
|
87
|
-
};
|
|
88
|
-
const applySubscription = async (store, action) => {
|
|
89
|
-
if (!action.subscriptionId) {
|
|
90
|
-
return { applied: false, reason: "unhandled" };
|
|
91
|
-
}
|
|
92
|
-
const existing = await store.getSubscription(action.provider, action.subscriptionId);
|
|
93
|
-
const now = Date.now();
|
|
94
|
-
if (action.type === "subscription.updated") {
|
|
95
|
-
if (!existing) {
|
|
96
|
-
return { applied: false, reason: "unhandled" };
|
|
97
|
-
}
|
|
98
|
-
await store.upsertSubscription({
|
|
99
|
-
...existing,
|
|
100
|
-
cancelAtPeriodEnd: action.cancelAtPeriodEnd ?? existing.cancelAtPeriodEnd,
|
|
101
|
-
currentPeriodEnd: action.currentPeriodEnd ?? existing.currentPeriodEnd,
|
|
102
|
-
currentPeriodStart: action.currentPeriodStart ?? existing.currentPeriodStart,
|
|
103
|
-
priceId: action.priceId ?? existing.priceId,
|
|
104
|
-
quantity: action.quantity ?? existing.quantity,
|
|
105
|
-
updatedAt: now
|
|
106
|
-
});
|
|
107
|
-
return { applied: true, reason: "ok" };
|
|
108
|
-
}
|
|
109
|
-
const targetState = SUBSCRIPTION_STATE_BY_TYPE[action.type];
|
|
110
|
-
if (!targetState) {
|
|
111
|
-
return { applied: false, reason: "unhandled" };
|
|
112
|
-
}
|
|
113
|
-
if (!existing) {
|
|
114
|
-
await store.upsertSubscription({
|
|
115
|
-
cancelAtPeriodEnd: action.cancelAtPeriodEnd ?? false,
|
|
116
|
-
createdAt: now,
|
|
117
|
-
currentPeriodEnd: action.currentPeriodEnd,
|
|
118
|
-
currentPeriodStart: action.currentPeriodStart ?? now,
|
|
119
|
-
id: action.subscriptionId,
|
|
120
|
-
priceId: action.priceId ?? "",
|
|
121
|
-
provider: action.provider,
|
|
122
|
-
quantity: action.quantity ?? 1,
|
|
123
|
-
referenceId: action.referenceId ?? "",
|
|
124
|
-
state: targetState,
|
|
125
|
-
updatedAt: now
|
|
126
|
-
});
|
|
127
|
-
return { applied: true, reason: "ok" };
|
|
128
|
-
}
|
|
129
|
-
const subscriptionAction = existing.state === "active" && targetState === "active" ? "renew" : SUBSCRIPTION_ACTION_BY_TYPE[action.type];
|
|
130
|
-
const nextState = subscriptionAction ? nextSubscriptionState(existing.state, subscriptionAction) : void 0;
|
|
131
|
-
if (!nextState) {
|
|
132
|
-
return { applied: false, reason: "illegal_transition" };
|
|
133
|
-
}
|
|
134
|
-
await store.upsertSubscription({
|
|
135
|
-
...existing,
|
|
136
|
-
cancelAtPeriodEnd: action.cancelAtPeriodEnd ?? existing.cancelAtPeriodEnd,
|
|
137
|
-
currentPeriodEnd: action.currentPeriodEnd ?? existing.currentPeriodEnd,
|
|
138
|
-
currentPeriodStart: action.currentPeriodStart ?? existing.currentPeriodStart,
|
|
139
|
-
priceId: action.priceId ?? existing.priceId,
|
|
140
|
-
quantity: action.quantity ?? existing.quantity,
|
|
141
|
-
state: nextState,
|
|
142
|
-
updatedAt: now
|
|
143
|
-
});
|
|
144
|
-
return { applied: true, reason: "ok" };
|
|
145
|
-
};
|
|
146
|
-
const applyWebhookAction = async (store, action, observer) => {
|
|
147
|
-
if (action.type === "unhandled") {
|
|
148
|
-
return { applied: false, reason: "unhandled" };
|
|
149
|
-
}
|
|
150
|
-
const fresh = await store.markEventProcessed(action.provider, action.eventId);
|
|
151
|
-
if (!fresh) {
|
|
152
|
-
notifyObserver(observer, { eventId: action.eventId, provider: action.provider, type: "webhook.duplicate" });
|
|
153
|
-
return { applied: false, reason: "duplicate" };
|
|
154
|
-
}
|
|
155
|
-
const paymentAction = PAYMENT_ACTION_BY_TYPE[action.type];
|
|
156
|
-
let result;
|
|
157
|
-
try {
|
|
158
|
-
result = paymentAction ? await applyPayment(store, action, paymentAction) : await applySubscription(store, action);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
await store.releaseEvent(action.provider, action.eventId);
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
notifyObserver(observer, { action: action.type, eventId: action.eventId, provider: action.provider, reason: result.reason, type: "webhook.applied" });
|
|
164
|
-
if (action.type === "payment.failed") {
|
|
165
|
-
notifyObserver(observer, { provider: action.provider, referenceId: action.referenceId, sessionId: action.sessionId, type: "payment.failed" });
|
|
166
|
-
} else if (action.type === "subscription.past_due") {
|
|
167
|
-
notifyObserver(observer, {
|
|
168
|
-
provider: action.provider,
|
|
169
|
-
referenceId: action.referenceId,
|
|
170
|
-
subscriptionId: action.subscriptionId,
|
|
171
|
-
type: "subscription.past_due"
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
export { applyWebhookAction as default };
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { LunoraPaymentError } from './LunoraPaymentError-B3hEzXSs.mjs';
|
|
2
|
-
|
|
3
|
-
const encoder = new TextEncoder();
|
|
4
|
-
const toHex = (buffer) => [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5
|
-
const SYMMETRIC_PREFIX = "whsec_";
|
|
6
|
-
const base64ToBytes = (value) => new Uint8Array(Array.from(atob(value), (character) => character.codePointAt(0) ?? 0));
|
|
7
|
-
const bytesToBase64 = (buffer) => btoa(String.fromCodePoint(...new Uint8Array(buffer)));
|
|
8
|
-
const hmacSha256Base64 = async (keyBytes, payload) => {
|
|
9
|
-
const key = await crypto.subtle.importKey("raw", keyBytes, { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
|
|
10
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
11
|
-
return bytesToBase64(signature);
|
|
12
|
-
};
|
|
13
|
-
const constantTimeEqual = (a, b) => {
|
|
14
|
-
if (a.length !== b.length) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
let mismatch = 0;
|
|
18
|
-
for (let index = 0; index < a.length; index += 1) {
|
|
19
|
-
mismatch |= (a.codePointAt(index) ?? 0) ^ (b.codePointAt(index) ?? 0);
|
|
20
|
-
}
|
|
21
|
-
return mismatch === 0;
|
|
22
|
-
};
|
|
23
|
-
const hmacSha256Hex = async (secret, payload) => {
|
|
24
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
|
|
25
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
26
|
-
return toHex(signature);
|
|
27
|
-
};
|
|
28
|
-
const parseStripeSignatureHeader = (header) => {
|
|
29
|
-
let timestamp = Number.NaN;
|
|
30
|
-
const signatures = [];
|
|
31
|
-
for (const part of header.split(",")) {
|
|
32
|
-
const index = part.indexOf("=");
|
|
33
|
-
if (index === -1) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const scheme = part.slice(0, index).trim();
|
|
37
|
-
const value = part.slice(index + 1).trim();
|
|
38
|
-
if (scheme === "t") {
|
|
39
|
-
timestamp = Number(value);
|
|
40
|
-
} else if (scheme === "v1") {
|
|
41
|
-
signatures.push(value);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return { signatures, timestamp };
|
|
45
|
-
};
|
|
46
|
-
const verifyStripeSignature = async (input) => {
|
|
47
|
-
if (!input.secret) {
|
|
48
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "webhook secret not configured");
|
|
49
|
-
}
|
|
50
|
-
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
51
|
-
const nowMs = input.now ?? Date.now();
|
|
52
|
-
const { signatures, timestamp } = parseStripeSignatureHeader(input.signatureHeader);
|
|
53
|
-
if (!Number.isFinite(timestamp) || signatures.length === 0) {
|
|
54
|
-
throw new LunoraPaymentError("WEBHOOK_SIGNATURE_INVALID", "malformed signature header");
|
|
55
|
-
}
|
|
56
|
-
if (Math.abs(Math.floor(nowMs / 1e3) - timestamp) > toleranceSeconds) {
|
|
57
|
-
throw new LunoraPaymentError("WEBHOOK_TIMESTAMP_INVALID", "signature timestamp outside tolerance");
|
|
58
|
-
}
|
|
59
|
-
const expected = await hmacSha256Hex(input.secret, `${String(timestamp)}.${input.payload}`);
|
|
60
|
-
if (!signatures.some((candidate) => constantTimeEqual(candidate, expected))) {
|
|
61
|
-
throw new LunoraPaymentError("WEBHOOK_SIGNATURE_INVALID", "no matching signature");
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const verifyStandardWebhook = async (input) => {
|
|
65
|
-
if (!input.secret) {
|
|
66
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "webhook secret not configured");
|
|
67
|
-
}
|
|
68
|
-
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
69
|
-
const nowMs = input.now ?? Date.now();
|
|
70
|
-
const timestamp = Number(input.webhookTimestamp);
|
|
71
|
-
if (!input.webhookId || !input.webhookSignature || !Number.isFinite(timestamp)) {
|
|
72
|
-
throw new LunoraPaymentError("WEBHOOK_SIGNATURE_INVALID", "missing standard-webhooks headers");
|
|
73
|
-
}
|
|
74
|
-
if (Math.abs(Math.floor(nowMs / 1e3) - timestamp) > toleranceSeconds) {
|
|
75
|
-
throw new LunoraPaymentError("WEBHOOK_TIMESTAMP_INVALID", "signature timestamp outside tolerance");
|
|
76
|
-
}
|
|
77
|
-
const rawSecret = input.secret.startsWith(SYMMETRIC_PREFIX) ? input.secret.slice(SYMMETRIC_PREFIX.length) : input.secret;
|
|
78
|
-
if (!rawSecret) {
|
|
79
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "webhook secret not configured");
|
|
80
|
-
}
|
|
81
|
-
const keyBytes = base64ToBytes(rawSecret);
|
|
82
|
-
if (keyBytes.length === 0) {
|
|
83
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "webhook secret not configured");
|
|
84
|
-
}
|
|
85
|
-
const expected = await hmacSha256Base64(keyBytes, `${input.webhookId}.${input.webhookTimestamp}.${input.payload}`);
|
|
86
|
-
const provided = input.webhookSignature.split(" ").map((entry) => {
|
|
87
|
-
const comma = entry.indexOf(",");
|
|
88
|
-
return comma === -1 ? "" : entry.slice(comma + 1);
|
|
89
|
-
}).filter(Boolean);
|
|
90
|
-
if (!provided.some((candidate) => constantTimeEqual(candidate, expected))) {
|
|
91
|
-
throw new LunoraPaymentError("WEBHOOK_SIGNATURE_INVALID", "no matching signature");
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
export { constantTimeEqual, hmacSha256Hex, parseStripeSignatureHeader, verifyStandardWebhook, verifyStripeSignature };
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { LunoraPaymentError } from './LunoraPaymentError-B3hEzXSs.mjs';
|
|
2
|
-
|
|
3
|
-
const createAdapterRegistry = (adapters) => {
|
|
4
|
-
const byId = /* @__PURE__ */ new Map();
|
|
5
|
-
for (const adapter of adapters) {
|
|
6
|
-
if (byId.has(adapter.identifier)) {
|
|
7
|
-
throw new LunoraPaymentError("CONFIG_INVALID", `duplicate adapter for provider "${adapter.identifier}"`);
|
|
8
|
-
}
|
|
9
|
-
byId.set(adapter.identifier, adapter);
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
all: () => [...byId.values()],
|
|
13
|
-
get: (provider) => {
|
|
14
|
-
const adapter = byId.get(provider);
|
|
15
|
-
if (!adapter) {
|
|
16
|
-
throw new LunoraPaymentError("CONFIG_INVALID", `no adapter registered for provider "${provider}"`);
|
|
17
|
-
}
|
|
18
|
-
return adapter;
|
|
19
|
-
},
|
|
20
|
-
has: (provider) => byId.has(provider)
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export { createAdapterRegistry };
|