@lunora/payment 0.0.0 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,16 @@
1
+ export { createAdapterRegistry } from './packem_shared/createAdapterRegistry-BuDHFCBc.mjs';
2
+ export { lunoraDatabaseToPaymentDatabase, paymentsFromContext } from './packem_shared/lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs';
3
+ export { createPayment } from './packem_shared/createPayment-BccfPGyw.mjs';
4
+ export { createDatabasePaymentStore } from './packem_shared/createDatabasePaymentStore-bYB_HUE6.mjs';
5
+ export { entitlementsForReference, featureNames, hasActivePrice, resolveEntitlements, usagePeriodStart } from './packem_shared/entitlementsForReference-CzZGXPoZ.mjs';
6
+ export { LunoraPaymentError } from './packem_shared/LunoraPaymentError-B3hEzXSs.mjs';
7
+ export { default as idempotencyKey } from './packem_shared/idempotencyKey-BFzDCA7g.mjs';
8
+ export { addMoney, allocateMoney, compareMoney, formatMoney, fromMoneyJSON, isZeroDecimalCurrency, isZeroMoney, money, subtractMoney, toMoneyJSON, zeroMoney } from './packem_shared/addMoney-bCcs1nyw.mjs';
9
+ export { createPolarAdapter } from './packem_shared/createPolarAdapter-BJtVGSlF.mjs';
10
+ export { createStripeAdapter } from './packem_shared/createStripeAdapter-D40MVBXg.mjs';
11
+ export { reconcile } from './packem_shared/reconcile-CI1ukJF9.mjs';
12
+ export { default as paymentTables } from './packem_shared/paymentTables-DccHwWr_.mjs';
13
+ export { PAYMENT_TERMINAL_STATES, SUBSCRIPTION_TERMINAL_STATES, canTransitionPayment, canTransitionSubscription, nextPaymentState, nextSubscriptionState } from './packem_shared/PAYMENT_TERMINAL_STATES-DrxV0clv.mjs';
14
+ export { MemoryPaymentStore } from './packem_shared/MemoryPaymentStore-DvgdWa3C.mjs';
15
+ export { default as applyWebhookAction } from './packem_shared/applyWebhookAction-DpAqf3Lw.mjs';
16
+ export { constantTimeEqual, hmacSha256Hex, parseStripeSignatureHeader, verifyStandardWebhook, verifyStripeSignature } from './packem_shared/constantTimeEqual-CfY0jYcL.mjs';
@@ -0,0 +1,22 @@
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 };
@@ -0,0 +1,72 @@
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 };
@@ -0,0 +1,26 @@
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 };
@@ -0,0 +1,60 @@
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 };
@@ -0,0 +1,177 @@
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 };
@@ -0,0 +1,95 @@
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 };
@@ -0,0 +1,24 @@
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 };
@@ -0,0 +1,172 @@
1
+ import { money } from './addMoney-bCcs1nyw.mjs';
2
+
3
+ const readString = (row, key) => typeof row[key] === "string" ? row[key] : "";
4
+ const readOptionalString = (row, key) => typeof row[key] === "string" ? row[key] : void 0;
5
+ const readNumber = (row, key) => typeof row[key] === "number" ? row[key] : 0;
6
+ const readOptionalNumber = (row, key) => typeof row[key] === "number" ? row[key] : void 0;
7
+ const readBoolean = (row, key) => row[key] === true;
8
+ const readBigint = (row, key) => {
9
+ const value = row[key];
10
+ if (typeof value === "bigint") {
11
+ return value;
12
+ }
13
+ return typeof value === "number" || typeof value === "string" ? BigInt(value) : 0n;
14
+ };
15
+ const customerToRow = (customer) => {
16
+ return {
17
+ createdAt: customer.createdAt,
18
+ email: customer.email,
19
+ provider: customer.provider,
20
+ providerCustomerId: customer.id,
21
+ referenceId: customer.referenceId
22
+ };
23
+ };
24
+ const rowToCustomer = (row) => {
25
+ return {
26
+ createdAt: readNumber(row, "createdAt"),
27
+ email: readOptionalString(row, "email"),
28
+ id: readString(row, "providerCustomerId"),
29
+ provider: readString(row, "provider"),
30
+ referenceId: readString(row, "referenceId")
31
+ };
32
+ };
33
+ const sessionToRow = (session) => {
34
+ return {
35
+ amountMinor: session.amount.minorUnits,
36
+ capturedMinor: session.capturedAmount.minorUnits,
37
+ createdAt: session.createdAt,
38
+ currency: session.amount.currency,
39
+ provider: session.provider,
40
+ providerSessionId: session.id,
41
+ referenceId: session.referenceId,
42
+ refundedMinor: session.refundedAmount.minorUnits,
43
+ state: session.state,
44
+ updatedAt: session.updatedAt
45
+ };
46
+ };
47
+ const rowToSession = (row) => {
48
+ const currency = readString(row, "currency");
49
+ return {
50
+ amount: money(readBigint(row, "amountMinor"), currency),
51
+ capturedAmount: money(readBigint(row, "capturedMinor"), currency),
52
+ createdAt: readNumber(row, "createdAt"),
53
+ id: readString(row, "providerSessionId"),
54
+ provider: readString(row, "provider"),
55
+ referenceId: readString(row, "referenceId"),
56
+ refundedAmount: money(readBigint(row, "refundedMinor"), currency),
57
+ state: readString(row, "state"),
58
+ updatedAt: readNumber(row, "updatedAt")
59
+ };
60
+ };
61
+ const subscriptionToRow = (subscription) => {
62
+ return {
63
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
64
+ createdAt: subscription.createdAt,
65
+ currentPeriodEnd: subscription.currentPeriodEnd,
66
+ currentPeriodStart: subscription.currentPeriodStart,
67
+ priceId: subscription.priceId,
68
+ provider: subscription.provider,
69
+ providerSubscriptionId: subscription.id,
70
+ quantity: subscription.quantity,
71
+ referenceId: subscription.referenceId,
72
+ state: subscription.state,
73
+ updatedAt: subscription.updatedAt
74
+ };
75
+ };
76
+ const rowToSubscription = (row) => {
77
+ return {
78
+ cancelAtPeriodEnd: readBoolean(row, "cancelAtPeriodEnd"),
79
+ createdAt: readNumber(row, "createdAt"),
80
+ currentPeriodEnd: readOptionalNumber(row, "currentPeriodEnd"),
81
+ currentPeriodStart: readOptionalNumber(row, "currentPeriodStart"),
82
+ id: readString(row, "providerSubscriptionId"),
83
+ priceId: readString(row, "priceId"),
84
+ provider: readString(row, "provider"),
85
+ quantity: readNumber(row, "quantity"),
86
+ referenceId: readString(row, "referenceId"),
87
+ state: readString(row, "state"),
88
+ updatedAt: readNumber(row, "updatedAt")
89
+ };
90
+ };
91
+ const usageEventToRow = (event) => {
92
+ return {
93
+ createdAt: event.createdAt,
94
+ featureId: event.featureId,
95
+ idempotencyKey: event.idempotencyKey,
96
+ provider: event.provider,
97
+ quantity: event.quantity,
98
+ referenceId: event.referenceId,
99
+ reportedToProvider: event.reportedToProvider
100
+ };
101
+ };
102
+ const createDatabasePaymentStore = (database) => {
103
+ const upsert = async (table, where, row) => {
104
+ const existing = await database.findFirst(table, where);
105
+ if (existing) {
106
+ await database.patch(existing._id, row);
107
+ return;
108
+ }
109
+ await database.insert(table, row);
110
+ };
111
+ return {
112
+ getCustomerByReference: async (provider, referenceId) => {
113
+ const row = await database.findFirst("customers", { provider, referenceId });
114
+ return row ? rowToCustomer(row) : void 0;
115
+ },
116
+ getPaymentSession: async (provider, id) => {
117
+ const row = await database.findFirst("paymentSessions", { provider, providerSessionId: id });
118
+ return row ? rowToSession(row) : void 0;
119
+ },
120
+ getSubscription: async (provider, id) => {
121
+ const row = await database.findFirst("subscriptions", { provider, providerSubscriptionId: id });
122
+ return row ? rowToSubscription(row) : void 0;
123
+ },
124
+ listSubscriptionsByReference: async (referenceId) => {
125
+ const rows = await database.findMany("subscriptions", { referenceId });
126
+ return rows.map((row) => rowToSubscription(row));
127
+ },
128
+ markEventProcessed: async (provider, eventId) => {
129
+ const existing = await database.findFirst("events", { provider, providerEventId: eventId });
130
+ if (existing) {
131
+ return false;
132
+ }
133
+ await database.insert("events", { processedAt: Date.now(), provider, providerEventId: eventId, type: "" });
134
+ return true;
135
+ },
136
+ releaseEvent: async (provider, eventId) => {
137
+ const existing = await database.findFirst("events", { provider, providerEventId: eventId });
138
+ if (existing) {
139
+ await database.delete(existing._id);
140
+ }
141
+ },
142
+ markUsageReported: async (provider, idempotencyKey) => {
143
+ const existing = await database.findFirst("usageEvents", { idempotencyKey, provider });
144
+ if (existing) {
145
+ await database.patch(existing._id, { reportedToProvider: true });
146
+ }
147
+ },
148
+ recordUsage: async (event) => {
149
+ const existing = await database.findFirst("usageEvents", { idempotencyKey: event.idempotencyKey, provider: event.provider });
150
+ if (existing) {
151
+ return false;
152
+ }
153
+ await database.insert("usageEvents", usageEventToRow(event));
154
+ return true;
155
+ },
156
+ sumUsage: async (referenceId, featureId, since) => {
157
+ const rows = await database.findMany("usageEvents", { featureId, referenceId });
158
+ let total = 0;
159
+ for (const row of rows) {
160
+ if (readNumber(row, "createdAt") >= since) {
161
+ total += readNumber(row, "quantity");
162
+ }
163
+ }
164
+ return total;
165
+ },
166
+ upsertCustomer: async (customer) => upsert("customers", { provider: customer.provider, providerCustomerId: customer.id }, customerToRow(customer)),
167
+ upsertPaymentSession: async (session) => upsert("paymentSessions", { provider: session.provider, providerSessionId: session.id }, sessionToRow(session)),
168
+ upsertSubscription: async (subscription) => upsert("subscriptions", { provider: subscription.provider, providerSubscriptionId: subscription.id }, subscriptionToRow(subscription))
169
+ };
170
+ };
171
+
172
+ export { createDatabasePaymentStore };