@nehorai/payments 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/config/index.cjs +116 -0
- package/dist/config/index.cjs.map +1 -0
- package/dist/config/index.d.cts +125 -0
- package/dist/config/index.d.ts +125 -0
- package/dist/config/index.js +83 -0
- package/dist/config/index.js.map +1 -0
- package/dist/factory.cjs +807 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +96 -0
- package/dist/factory.d.ts +96 -0
- package/dist/factory.js +777 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.cjs +1341 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1260 -0
- package/dist/index.js.map +1 -0
- package/dist/payment-orchestrator-CPaLmDM5.d.ts +404 -0
- package/dist/payment-orchestrator-Co_X6T_V.d.cts +404 -0
- package/dist/payment-types-68W-PlGg.d.cts +211 -0
- package/dist/payment-types-68W-PlGg.d.ts +211 -0
- package/dist/providers/interfaces/index.cjs +19 -0
- package/dist/providers/interfaces/index.cjs.map +1 -0
- package/dist/providers/interfaces/index.d.cts +80 -0
- package/dist/providers/interfaces/index.d.ts +80 -0
- package/dist/providers/interfaces/index.js +1 -0
- package/dist/providers/interfaces/index.js.map +1 -0
- package/dist/repository/interfaces/index.cjs +19 -0
- package/dist/repository/interfaces/index.cjs.map +1 -0
- package/dist/repository/interfaces/index.d.cts +556 -0
- package/dist/repository/interfaces/index.d.ts +556 -0
- package/dist/repository/interfaces/index.js +1 -0
- package/dist/repository/interfaces/index.js.map +1 -0
- package/dist/routing-engine.interface-DJzGXor9.d.cts +194 -0
- package/dist/routing-engine.interface-h9_GmQ4b.d.ts +194 -0
- package/dist/services/index.cjs +806 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.cts +75 -0
- package/dist/services/index.d.ts +75 -0
- package/dist/services/index.js +763 -0
- package/dist/services/index.js.map +1 -0
- package/dist/state-machine-Cu6_qKnv.d.cts +109 -0
- package/dist/state-machine-Cu6_qKnv.d.ts +109 -0
- package/dist/types/index.cjs +173 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +127 -0
- package/dist/types/index.d.ts +127 -0
- package/dist/types/index.js +130 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.cjs +167 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +102 -0
- package/dist/utils/index.d.ts +102 -0
- package/dist/utils/index.js +127 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { k as PaymentProvider } from '../payment-types-68W-PlGg.cjs';
|
|
2
|
+
export { A as AuthorizationResult, a as AuthorizePaymentParams, C as CapturePaymentParams, b as CaptureResult, c as CardBrand, d as CreatePaymentIntentParams, e as CurrencyConversion, P as PaymentAmount, f as PaymentError, g as PaymentErrorCode, h as PaymentIntentResult, i as PaymentMetadata, j as PaymentMethodType, l as ProviderHealthStatus, m as ProviderMetadata, R as RefundParams, n as RefundResult, T as TaxInvoiceStatus, o as TransactionType, V as VoidPaymentParams, p as VoidResult } from '../payment-types-68W-PlGg.cjs';
|
|
3
|
+
import { c as TransactionStatus } from '../state-machine-Cu6_qKnv.cjs';
|
|
4
|
+
export { D as DEFAULT_AUTH_HOLD_DAYS, H as HOLD_STATES, S as SUCCESS_STATES, a as StateTransitionResult, T as TERMINAL_STATES, b as TransactionEvent, V as VALID_TRANSITIONS, d as attemptTransition, e as calculateCaptureDeadline, f as canCapture, g as canRefund, h as canTransition, i as canVoid, j as getNextStatus, k as isAuthorizationExpired, l as isHoldState, m as isSuccessState, n as isTerminalState } from '../state-machine-Cu6_qKnv.cjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @nehorai/payments - Webhook Types
|
|
8
|
+
*
|
|
9
|
+
* Types for processing incoming webhooks from payment providers.
|
|
10
|
+
* Includes signature verification and idempotent event handling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Status of webhook processing
|
|
15
|
+
*/
|
|
16
|
+
type WebhookStatus = 'pending' | 'processing' | 'processed' | 'failed' | 'ignored';
|
|
17
|
+
/**
|
|
18
|
+
* Incoming webhook event from any provider
|
|
19
|
+
*/
|
|
20
|
+
interface WebhookEvent {
|
|
21
|
+
/** Source payment provider */
|
|
22
|
+
provider: PaymentProvider;
|
|
23
|
+
/** Provider's unique event ID (for idempotency) */
|
|
24
|
+
eventId: string;
|
|
25
|
+
/** Type of event (e.g., 'payment_intent.succeeded') */
|
|
26
|
+
eventType: string;
|
|
27
|
+
/** When the event occurred */
|
|
28
|
+
timestamp: Date;
|
|
29
|
+
/** Raw event payload */
|
|
30
|
+
payload: Record<string, unknown>;
|
|
31
|
+
/** Signature from provider for verification */
|
|
32
|
+
signature: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Result of processing a webhook event
|
|
36
|
+
*/
|
|
37
|
+
interface WebhookProcessingResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
/** Associated transaction ID if applicable */
|
|
40
|
+
transactionId?: string;
|
|
41
|
+
/** What action was taken */
|
|
42
|
+
action?: WebhookAction;
|
|
43
|
+
/** Error message if failed */
|
|
44
|
+
error?: string;
|
|
45
|
+
/** Whether this was a duplicate event */
|
|
46
|
+
wasDuplicate?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Actions taken when processing webhooks
|
|
50
|
+
*/
|
|
51
|
+
type WebhookAction = 'transaction_created' | 'status_updated' | 'refund_processed' | 'dispute_created' | 'skipped_duplicate' | 'ignored_event_type' | 'no_action_needed';
|
|
52
|
+
/**
|
|
53
|
+
* Stripe webhook event types we handle
|
|
54
|
+
*/
|
|
55
|
+
type StripeEventType = 'payment_intent.created' | 'payment_intent.processing' | 'payment_intent.succeeded' | 'payment_intent.payment_failed' | 'payment_intent.canceled' | 'payment_intent.amount_capturable_updated' | 'charge.succeeded' | 'charge.failed' | 'charge.refunded' | 'charge.dispute.created' | 'charge.dispute.closed' | 'customer.subscription.created' | 'customer.subscription.updated' | 'customer.subscription.deleted' | 'invoice.paid' | 'invoice.payment_failed';
|
|
56
|
+
/**
|
|
57
|
+
* Map of Stripe events to transaction status updates
|
|
58
|
+
*/
|
|
59
|
+
declare const STRIPE_EVENT_TO_STATUS: Partial<Record<StripeEventType, TransactionStatus>>;
|
|
60
|
+
/**
|
|
61
|
+
* Parameters for verifying webhook signature
|
|
62
|
+
*/
|
|
63
|
+
interface WebhookVerificationParams {
|
|
64
|
+
payload: string;
|
|
65
|
+
signature: string;
|
|
66
|
+
secret: string;
|
|
67
|
+
/** Tolerance in seconds for timestamp validation */
|
|
68
|
+
tolerance?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Result of webhook signature verification
|
|
72
|
+
*/
|
|
73
|
+
interface WebhookVerificationResult {
|
|
74
|
+
valid: boolean;
|
|
75
|
+
error?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Message format for webhook queue
|
|
79
|
+
*/
|
|
80
|
+
interface WebhookQueueMessage {
|
|
81
|
+
/** Message ID for deduplication */
|
|
82
|
+
messageId: string;
|
|
83
|
+
/** Provider that sent the webhook */
|
|
84
|
+
provider: PaymentProvider;
|
|
85
|
+
/** Provider's event ID */
|
|
86
|
+
eventId: string;
|
|
87
|
+
/** Event type */
|
|
88
|
+
eventType: string;
|
|
89
|
+
/** Raw payload (JSON string) */
|
|
90
|
+
payload: string;
|
|
91
|
+
/** Signature for verification */
|
|
92
|
+
signature: string;
|
|
93
|
+
/** When received by our API */
|
|
94
|
+
receivedAt: string;
|
|
95
|
+
/** Number of processing attempts */
|
|
96
|
+
attemptCount: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Result of queue message processing
|
|
100
|
+
*/
|
|
101
|
+
interface QueueProcessingResult {
|
|
102
|
+
success: boolean;
|
|
103
|
+
messageId: string;
|
|
104
|
+
action?: WebhookAction;
|
|
105
|
+
shouldDelete: boolean;
|
|
106
|
+
shouldRetry: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Result of reconciling payment state
|
|
111
|
+
* Handles race condition between redirect and webhook
|
|
112
|
+
*/
|
|
113
|
+
interface ReconciliationResult {
|
|
114
|
+
reconciled: boolean;
|
|
115
|
+
/** Final determined status */
|
|
116
|
+
finalStatus: TransactionStatus;
|
|
117
|
+
/** Source of truth (redirect callback vs webhook) */
|
|
118
|
+
source: 'redirect' | 'webhook' | 'provider_query';
|
|
119
|
+
/** Whether status was updated */
|
|
120
|
+
statusChanged: boolean;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reconciliation strategy when redirect and webhook conflict
|
|
124
|
+
*/
|
|
125
|
+
type ReconciliationStrategy = 'prefer_webhook' | 'prefer_redirect' | 'query_provider';
|
|
126
|
+
|
|
127
|
+
export { PaymentProvider, type QueueProcessingResult, type ReconciliationResult, type ReconciliationStrategy, STRIPE_EVENT_TO_STATUS, type StripeEventType, TransactionStatus, type WebhookAction, type WebhookEvent, type WebhookProcessingResult, type WebhookQueueMessage, type WebhookStatus, type WebhookVerificationParams, type WebhookVerificationResult };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { k as PaymentProvider } from '../payment-types-68W-PlGg.js';
|
|
2
|
+
export { A as AuthorizationResult, a as AuthorizePaymentParams, C as CapturePaymentParams, b as CaptureResult, c as CardBrand, d as CreatePaymentIntentParams, e as CurrencyConversion, P as PaymentAmount, f as PaymentError, g as PaymentErrorCode, h as PaymentIntentResult, i as PaymentMetadata, j as PaymentMethodType, l as ProviderHealthStatus, m as ProviderMetadata, R as RefundParams, n as RefundResult, T as TaxInvoiceStatus, o as TransactionType, V as VoidPaymentParams, p as VoidResult } from '../payment-types-68W-PlGg.js';
|
|
3
|
+
import { c as TransactionStatus } from '../state-machine-Cu6_qKnv.js';
|
|
4
|
+
export { D as DEFAULT_AUTH_HOLD_DAYS, H as HOLD_STATES, S as SUCCESS_STATES, a as StateTransitionResult, T as TERMINAL_STATES, b as TransactionEvent, V as VALID_TRANSITIONS, d as attemptTransition, e as calculateCaptureDeadline, f as canCapture, g as canRefund, h as canTransition, i as canVoid, j as getNextStatus, k as isAuthorizationExpired, l as isHoldState, m as isSuccessState, n as isTerminalState } from '../state-machine-Cu6_qKnv.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @nehorai/payments - Webhook Types
|
|
8
|
+
*
|
|
9
|
+
* Types for processing incoming webhooks from payment providers.
|
|
10
|
+
* Includes signature verification and idempotent event handling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Status of webhook processing
|
|
15
|
+
*/
|
|
16
|
+
type WebhookStatus = 'pending' | 'processing' | 'processed' | 'failed' | 'ignored';
|
|
17
|
+
/**
|
|
18
|
+
* Incoming webhook event from any provider
|
|
19
|
+
*/
|
|
20
|
+
interface WebhookEvent {
|
|
21
|
+
/** Source payment provider */
|
|
22
|
+
provider: PaymentProvider;
|
|
23
|
+
/** Provider's unique event ID (for idempotency) */
|
|
24
|
+
eventId: string;
|
|
25
|
+
/** Type of event (e.g., 'payment_intent.succeeded') */
|
|
26
|
+
eventType: string;
|
|
27
|
+
/** When the event occurred */
|
|
28
|
+
timestamp: Date;
|
|
29
|
+
/** Raw event payload */
|
|
30
|
+
payload: Record<string, unknown>;
|
|
31
|
+
/** Signature from provider for verification */
|
|
32
|
+
signature: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Result of processing a webhook event
|
|
36
|
+
*/
|
|
37
|
+
interface WebhookProcessingResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
/** Associated transaction ID if applicable */
|
|
40
|
+
transactionId?: string;
|
|
41
|
+
/** What action was taken */
|
|
42
|
+
action?: WebhookAction;
|
|
43
|
+
/** Error message if failed */
|
|
44
|
+
error?: string;
|
|
45
|
+
/** Whether this was a duplicate event */
|
|
46
|
+
wasDuplicate?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Actions taken when processing webhooks
|
|
50
|
+
*/
|
|
51
|
+
type WebhookAction = 'transaction_created' | 'status_updated' | 'refund_processed' | 'dispute_created' | 'skipped_duplicate' | 'ignored_event_type' | 'no_action_needed';
|
|
52
|
+
/**
|
|
53
|
+
* Stripe webhook event types we handle
|
|
54
|
+
*/
|
|
55
|
+
type StripeEventType = 'payment_intent.created' | 'payment_intent.processing' | 'payment_intent.succeeded' | 'payment_intent.payment_failed' | 'payment_intent.canceled' | 'payment_intent.amount_capturable_updated' | 'charge.succeeded' | 'charge.failed' | 'charge.refunded' | 'charge.dispute.created' | 'charge.dispute.closed' | 'customer.subscription.created' | 'customer.subscription.updated' | 'customer.subscription.deleted' | 'invoice.paid' | 'invoice.payment_failed';
|
|
56
|
+
/**
|
|
57
|
+
* Map of Stripe events to transaction status updates
|
|
58
|
+
*/
|
|
59
|
+
declare const STRIPE_EVENT_TO_STATUS: Partial<Record<StripeEventType, TransactionStatus>>;
|
|
60
|
+
/**
|
|
61
|
+
* Parameters for verifying webhook signature
|
|
62
|
+
*/
|
|
63
|
+
interface WebhookVerificationParams {
|
|
64
|
+
payload: string;
|
|
65
|
+
signature: string;
|
|
66
|
+
secret: string;
|
|
67
|
+
/** Tolerance in seconds for timestamp validation */
|
|
68
|
+
tolerance?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Result of webhook signature verification
|
|
72
|
+
*/
|
|
73
|
+
interface WebhookVerificationResult {
|
|
74
|
+
valid: boolean;
|
|
75
|
+
error?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Message format for webhook queue
|
|
79
|
+
*/
|
|
80
|
+
interface WebhookQueueMessage {
|
|
81
|
+
/** Message ID for deduplication */
|
|
82
|
+
messageId: string;
|
|
83
|
+
/** Provider that sent the webhook */
|
|
84
|
+
provider: PaymentProvider;
|
|
85
|
+
/** Provider's event ID */
|
|
86
|
+
eventId: string;
|
|
87
|
+
/** Event type */
|
|
88
|
+
eventType: string;
|
|
89
|
+
/** Raw payload (JSON string) */
|
|
90
|
+
payload: string;
|
|
91
|
+
/** Signature for verification */
|
|
92
|
+
signature: string;
|
|
93
|
+
/** When received by our API */
|
|
94
|
+
receivedAt: string;
|
|
95
|
+
/** Number of processing attempts */
|
|
96
|
+
attemptCount: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Result of queue message processing
|
|
100
|
+
*/
|
|
101
|
+
interface QueueProcessingResult {
|
|
102
|
+
success: boolean;
|
|
103
|
+
messageId: string;
|
|
104
|
+
action?: WebhookAction;
|
|
105
|
+
shouldDelete: boolean;
|
|
106
|
+
shouldRetry: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Result of reconciling payment state
|
|
111
|
+
* Handles race condition between redirect and webhook
|
|
112
|
+
*/
|
|
113
|
+
interface ReconciliationResult {
|
|
114
|
+
reconciled: boolean;
|
|
115
|
+
/** Final determined status */
|
|
116
|
+
finalStatus: TransactionStatus;
|
|
117
|
+
/** Source of truth (redirect callback vs webhook) */
|
|
118
|
+
source: 'redirect' | 'webhook' | 'provider_query';
|
|
119
|
+
/** Whether status was updated */
|
|
120
|
+
statusChanged: boolean;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reconciliation strategy when redirect and webhook conflict
|
|
124
|
+
*/
|
|
125
|
+
type ReconciliationStrategy = 'prefer_webhook' | 'prefer_redirect' | 'query_provider';
|
|
126
|
+
|
|
127
|
+
export { PaymentProvider, type QueueProcessingResult, type ReconciliationResult, type ReconciliationStrategy, STRIPE_EVENT_TO_STATUS, type StripeEventType, TransactionStatus, type WebhookAction, type WebhookEvent, type WebhookProcessingResult, type WebhookQueueMessage, type WebhookStatus, type WebhookVerificationParams, type WebhookVerificationResult };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/types/state-machine.ts
|
|
2
|
+
var TERMINAL_STATES = [
|
|
3
|
+
"voided",
|
|
4
|
+
"failed",
|
|
5
|
+
"expired",
|
|
6
|
+
"fully_refunded"
|
|
7
|
+
];
|
|
8
|
+
var SUCCESS_STATES = [
|
|
9
|
+
"captured",
|
|
10
|
+
"partially_refunded",
|
|
11
|
+
"fully_refunded"
|
|
12
|
+
];
|
|
13
|
+
var HOLD_STATES = [
|
|
14
|
+
"authorized"
|
|
15
|
+
];
|
|
16
|
+
var VALID_TRANSITIONS = {
|
|
17
|
+
created: ["INITIATE", "AUTHORIZE_PENDING", "AUTHORIZE_FAILED"],
|
|
18
|
+
pending_authorization: ["AUTHORIZE_SUCCESS", "AUTHORIZE_FAILED", "EXPIRED"],
|
|
19
|
+
authorized: ["CAPTURE_STARTED", "VOID_SUCCESS", "VOID_FAILED", "EXPIRED"],
|
|
20
|
+
capturing: ["CAPTURE_SUCCESS", "CAPTURE_FAILED"],
|
|
21
|
+
captured: ["PARTIAL_REFUND", "FULL_REFUND"],
|
|
22
|
+
voided: [],
|
|
23
|
+
// Terminal state
|
|
24
|
+
failed: [],
|
|
25
|
+
// Terminal state
|
|
26
|
+
expired: [],
|
|
27
|
+
// Terminal state
|
|
28
|
+
partially_refunded: ["PARTIAL_REFUND", "FULL_REFUND"],
|
|
29
|
+
fully_refunded: []
|
|
30
|
+
// Terminal state
|
|
31
|
+
};
|
|
32
|
+
var EVENT_TO_STATE = {
|
|
33
|
+
INITIATE: "pending_authorization",
|
|
34
|
+
AUTHORIZE_PENDING: "pending_authorization",
|
|
35
|
+
AUTHORIZE_SUCCESS: "authorized",
|
|
36
|
+
AUTHORIZE_FAILED: "failed",
|
|
37
|
+
CAPTURE_STARTED: "capturing",
|
|
38
|
+
CAPTURE_SUCCESS: "captured",
|
|
39
|
+
CAPTURE_FAILED: "failed",
|
|
40
|
+
VOID_SUCCESS: "voided",
|
|
41
|
+
VOID_FAILED: "authorized",
|
|
42
|
+
// Remain authorized if void fails
|
|
43
|
+
EXPIRED: "expired",
|
|
44
|
+
PARTIAL_REFUND: "partially_refunded",
|
|
45
|
+
FULL_REFUND: "fully_refunded"
|
|
46
|
+
};
|
|
47
|
+
function canTransition(currentStatus, event) {
|
|
48
|
+
return VALID_TRANSITIONS[currentStatus].includes(event);
|
|
49
|
+
}
|
|
50
|
+
function getNextStatus(currentStatus, event) {
|
|
51
|
+
if (!canTransition(currentStatus, event)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return EVENT_TO_STATE[event];
|
|
55
|
+
}
|
|
56
|
+
function isTerminalState(status) {
|
|
57
|
+
return TERMINAL_STATES.includes(status);
|
|
58
|
+
}
|
|
59
|
+
function isSuccessState(status) {
|
|
60
|
+
return SUCCESS_STATES.includes(status);
|
|
61
|
+
}
|
|
62
|
+
function isHoldState(status) {
|
|
63
|
+
return HOLD_STATES.includes(status);
|
|
64
|
+
}
|
|
65
|
+
function canRefund(status) {
|
|
66
|
+
return status === "captured" || status === "partially_refunded";
|
|
67
|
+
}
|
|
68
|
+
function canCapture(status) {
|
|
69
|
+
return status === "authorized";
|
|
70
|
+
}
|
|
71
|
+
function canVoid(status) {
|
|
72
|
+
return status === "authorized";
|
|
73
|
+
}
|
|
74
|
+
function attemptTransition(currentStatus, event) {
|
|
75
|
+
const nextStatus = getNextStatus(currentStatus, event);
|
|
76
|
+
if (nextStatus === null) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
previousStatus: currentStatus,
|
|
80
|
+
newStatus: currentStatus,
|
|
81
|
+
event,
|
|
82
|
+
error: `Invalid transition: ${currentStatus} -> ${event}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
previousStatus: currentStatus,
|
|
88
|
+
newStatus: nextStatus,
|
|
89
|
+
event
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var DEFAULT_AUTH_HOLD_DAYS = 7;
|
|
93
|
+
function calculateCaptureDeadline(authorizedAt, holdDays = DEFAULT_AUTH_HOLD_DAYS) {
|
|
94
|
+
const deadline = new Date(authorizedAt);
|
|
95
|
+
deadline.setDate(deadline.getDate() + holdDays);
|
|
96
|
+
return deadline;
|
|
97
|
+
}
|
|
98
|
+
function isAuthorizationExpired(captureDeadline) {
|
|
99
|
+
return /* @__PURE__ */ new Date() > captureDeadline;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/types/webhook-types.ts
|
|
103
|
+
var STRIPE_EVENT_TO_STATUS = {
|
|
104
|
+
"payment_intent.succeeded": "captured",
|
|
105
|
+
"payment_intent.payment_failed": "failed",
|
|
106
|
+
"payment_intent.canceled": "voided",
|
|
107
|
+
"payment_intent.amount_capturable_updated": "authorized",
|
|
108
|
+
"charge.refunded": "partially_refunded"
|
|
109
|
+
// or fully_refunded based on amount
|
|
110
|
+
};
|
|
111
|
+
export {
|
|
112
|
+
DEFAULT_AUTH_HOLD_DAYS,
|
|
113
|
+
HOLD_STATES,
|
|
114
|
+
STRIPE_EVENT_TO_STATUS,
|
|
115
|
+
SUCCESS_STATES,
|
|
116
|
+
TERMINAL_STATES,
|
|
117
|
+
VALID_TRANSITIONS,
|
|
118
|
+
attemptTransition,
|
|
119
|
+
calculateCaptureDeadline,
|
|
120
|
+
canCapture,
|
|
121
|
+
canRefund,
|
|
122
|
+
canTransition,
|
|
123
|
+
canVoid,
|
|
124
|
+
getNextStatus,
|
|
125
|
+
isAuthorizationExpired,
|
|
126
|
+
isHoldState,
|
|
127
|
+
isSuccessState,
|
|
128
|
+
isTerminalState
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/state-machine.ts","../../src/types/webhook-types.ts"],"sourcesContent":["/**\r\n * @nehorai/payments - Transaction State Machine\r\n *\r\n * Defines the strict state transitions for payment transactions.\r\n * Implements the J5 (Two-Phase Commit) pattern for authorize/capture flows.\r\n *\r\n * State Flow:\r\n * +-------------------------------------------------------------+\r\n * | CREATED --------------------------------------------------+ |\r\n * | | | |\r\n * | v v |\r\n * | PENDING_AUTHORIZATION -----------------------------> FAILED |\r\n * | | ^ |\r\n * | v | |\r\n * | AUTHORIZED ---------> VOIDED | |\r\n * | | | |\r\n * | +----------------> EXPIRED ------------------------+ |\r\n * | | | |\r\n * | v | |\r\n * | CAPTURING --------------------------------------------+ |\r\n * | | |\r\n * | v |\r\n * | CAPTURED ------> PARTIALLY_REFUNDED ------> FULLY_REFUNDED |\r\n * +-------------------------------------------------------------+\r\n */\r\n\r\n// ============================================================================\r\n// Transaction States\r\n// ============================================================================\r\n\r\n/**\r\n * All possible transaction states\r\n */\r\nexport type TransactionStatus =\r\n | 'created'\r\n | 'pending_authorization'\r\n | 'authorized'\r\n | 'capturing'\r\n | 'captured'\r\n | 'voided'\r\n | 'failed'\r\n | 'expired'\r\n | 'partially_refunded'\r\n | 'fully_refunded';\r\n\r\n/**\r\n * Terminal states - no further transitions possible\r\n */\r\nexport const TERMINAL_STATES: readonly TransactionStatus[] = [\r\n 'voided',\r\n 'failed',\r\n 'expired',\r\n 'fully_refunded',\r\n] as const;\r\n\r\n/**\r\n * States that indicate successful completion\r\n */\r\nexport const SUCCESS_STATES: readonly TransactionStatus[] = [\r\n 'captured',\r\n 'partially_refunded',\r\n 'fully_refunded',\r\n] as const;\r\n\r\n/**\r\n * States where funds are held but not captured\r\n */\r\nexport const HOLD_STATES: readonly TransactionStatus[] = [\r\n 'authorized',\r\n] as const;\r\n\r\n// ============================================================================\r\n// Transaction Events\r\n// ============================================================================\r\n\r\n/**\r\n * Events that trigger state transitions\r\n */\r\nexport type TransactionEvent =\r\n | 'INITIATE'\r\n | 'AUTHORIZE_PENDING'\r\n | 'AUTHORIZE_SUCCESS'\r\n | 'AUTHORIZE_FAILED'\r\n | 'CAPTURE_STARTED'\r\n | 'CAPTURE_SUCCESS'\r\n | 'CAPTURE_FAILED'\r\n | 'VOID_SUCCESS'\r\n | 'VOID_FAILED'\r\n | 'EXPIRED'\r\n | 'PARTIAL_REFUND'\r\n | 'FULL_REFUND';\r\n\r\n// ============================================================================\r\n// State Transition Map\r\n// ============================================================================\r\n\r\n/**\r\n * Valid transitions from each state\r\n */\r\nexport const VALID_TRANSITIONS: Record<TransactionStatus, TransactionEvent[]> = {\r\n created: ['INITIATE', 'AUTHORIZE_PENDING', 'AUTHORIZE_FAILED'],\r\n pending_authorization: ['AUTHORIZE_SUCCESS', 'AUTHORIZE_FAILED', 'EXPIRED'],\r\n authorized: ['CAPTURE_STARTED', 'VOID_SUCCESS', 'VOID_FAILED', 'EXPIRED'],\r\n capturing: ['CAPTURE_SUCCESS', 'CAPTURE_FAILED'],\r\n captured: ['PARTIAL_REFUND', 'FULL_REFUND'],\r\n voided: [], // Terminal state\r\n failed: [], // Terminal state\r\n expired: [], // Terminal state\r\n partially_refunded: ['PARTIAL_REFUND', 'FULL_REFUND'],\r\n fully_refunded: [], // Terminal state\r\n};\r\n\r\n/**\r\n * Event to next state mapping\r\n */\r\nconst EVENT_TO_STATE: Record<TransactionEvent, TransactionStatus> = {\r\n INITIATE: 'pending_authorization',\r\n AUTHORIZE_PENDING: 'pending_authorization',\r\n AUTHORIZE_SUCCESS: 'authorized',\r\n AUTHORIZE_FAILED: 'failed',\r\n CAPTURE_STARTED: 'capturing',\r\n CAPTURE_SUCCESS: 'captured',\r\n CAPTURE_FAILED: 'failed',\r\n VOID_SUCCESS: 'voided',\r\n VOID_FAILED: 'authorized', // Remain authorized if void fails\r\n EXPIRED: 'expired',\r\n PARTIAL_REFUND: 'partially_refunded',\r\n FULL_REFUND: 'fully_refunded',\r\n};\r\n\r\n// ============================================================================\r\n// State Machine Functions\r\n// ============================================================================\r\n\r\n/**\r\n * Check if a transition is valid\r\n */\r\nexport function canTransition(\r\n currentStatus: TransactionStatus,\r\n event: TransactionEvent\r\n): boolean {\r\n return VALID_TRANSITIONS[currentStatus].includes(event);\r\n}\r\n\r\n/**\r\n * Get the next state after an event, or null if transition is invalid\r\n */\r\nexport function getNextStatus(\r\n currentStatus: TransactionStatus,\r\n event: TransactionEvent\r\n): TransactionStatus | null {\r\n if (!canTransition(currentStatus, event)) {\r\n return null;\r\n }\r\n return EVENT_TO_STATE[event];\r\n}\r\n\r\n/**\r\n * Check if a state is terminal (no further transitions)\r\n */\r\nexport function isTerminalState(status: TransactionStatus): boolean {\r\n return TERMINAL_STATES.includes(status);\r\n}\r\n\r\n/**\r\n * Check if a state represents a successful payment\r\n */\r\nexport function isSuccessState(status: TransactionStatus): boolean {\r\n return SUCCESS_STATES.includes(status);\r\n}\r\n\r\n/**\r\n * Check if funds are currently held (authorized but not captured)\r\n */\r\nexport function isHoldState(status: TransactionStatus): boolean {\r\n return HOLD_STATES.includes(status);\r\n}\r\n\r\n/**\r\n * Check if a refund is possible from current state\r\n */\r\nexport function canRefund(status: TransactionStatus): boolean {\r\n return status === 'captured' || status === 'partially_refunded';\r\n}\r\n\r\n/**\r\n * Check if capture is possible from current state\r\n */\r\nexport function canCapture(status: TransactionStatus): boolean {\r\n return status === 'authorized';\r\n}\r\n\r\n/**\r\n * Check if void is possible from current state\r\n */\r\nexport function canVoid(status: TransactionStatus): boolean {\r\n return status === 'authorized';\r\n}\r\n\r\n// ============================================================================\r\n// State Transition Result Types\r\n// ============================================================================\r\n\r\n/**\r\n * Result of a state transition attempt\r\n */\r\nexport interface StateTransitionResult {\r\n success: boolean;\r\n previousStatus: TransactionStatus;\r\n newStatus: TransactionStatus;\r\n event: TransactionEvent;\r\n error?: string;\r\n}\r\n\r\n/**\r\n * Attempt a state transition with validation\r\n */\r\nexport function attemptTransition(\r\n currentStatus: TransactionStatus,\r\n event: TransactionEvent\r\n): StateTransitionResult {\r\n const nextStatus = getNextStatus(currentStatus, event);\r\n\r\n if (nextStatus === null) {\r\n return {\r\n success: false,\r\n previousStatus: currentStatus,\r\n newStatus: currentStatus,\r\n event,\r\n error: `Invalid transition: ${currentStatus} -> ${event}`,\r\n };\r\n }\r\n\r\n return {\r\n success: true,\r\n previousStatus: currentStatus,\r\n newStatus: nextStatus,\r\n event,\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Authorization Expiry\r\n// ============================================================================\r\n\r\n/**\r\n * Default authorization hold period (7 days for most providers)\r\n */\r\nexport const DEFAULT_AUTH_HOLD_DAYS = 7;\r\n\r\n/**\r\n * Calculate capture deadline from authorization time\r\n */\r\nexport function calculateCaptureDeadline(\r\n authorizedAt: Date,\r\n holdDays: number = DEFAULT_AUTH_HOLD_DAYS\r\n): Date {\r\n const deadline = new Date(authorizedAt);\r\n deadline.setDate(deadline.getDate() + holdDays);\r\n return deadline;\r\n}\r\n\r\n/**\r\n * Check if authorization has expired\r\n */\r\nexport function isAuthorizationExpired(captureDeadline: Date): boolean {\r\n return new Date() > captureDeadline;\r\n}\r\n","/**\r\n * @nehorai/payments - Webhook Types\r\n *\r\n * Types for processing incoming webhooks from payment providers.\r\n * Includes signature verification and idempotent event handling.\r\n */\r\n\r\nimport type { PaymentProvider } from './payment-types.js';\r\nimport type { TransactionStatus } from './state-machine.js';\r\n\r\n// ============================================================================\r\n// Webhook Event Types\r\n// ============================================================================\r\n\r\n/**\r\n * Status of webhook processing\r\n */\r\nexport type WebhookStatus =\r\n | 'pending'\r\n | 'processing'\r\n | 'processed'\r\n | 'failed'\r\n | 'ignored';\r\n\r\n/**\r\n * Incoming webhook event from any provider\r\n */\r\nexport interface WebhookEvent {\r\n /** Source payment provider */\r\n provider: PaymentProvider;\r\n /** Provider's unique event ID (for idempotency) */\r\n eventId: string;\r\n /** Type of event (e.g., 'payment_intent.succeeded') */\r\n eventType: string;\r\n /** When the event occurred */\r\n timestamp: Date;\r\n /** Raw event payload */\r\n payload: Record<string, unknown>;\r\n /** Signature from provider for verification */\r\n signature: string;\r\n}\r\n\r\n/**\r\n * Result of processing a webhook event\r\n */\r\nexport interface WebhookProcessingResult {\r\n success: boolean;\r\n /** Associated transaction ID if applicable */\r\n transactionId?: string;\r\n /** What action was taken */\r\n action?: WebhookAction;\r\n /** Error message if failed */\r\n error?: string;\r\n /** Whether this was a duplicate event */\r\n wasDuplicate?: boolean;\r\n}\r\n\r\n/**\r\n * Actions taken when processing webhooks\r\n */\r\nexport type WebhookAction =\r\n | 'transaction_created'\r\n | 'status_updated'\r\n | 'refund_processed'\r\n | 'dispute_created'\r\n | 'skipped_duplicate'\r\n | 'ignored_event_type'\r\n | 'no_action_needed';\r\n\r\n// ============================================================================\r\n// Provider-Specific Event Types\r\n// ============================================================================\r\n\r\n/**\r\n * Stripe webhook event types we handle\r\n */\r\nexport type StripeEventType =\r\n | 'payment_intent.created'\r\n | 'payment_intent.processing'\r\n | 'payment_intent.succeeded'\r\n | 'payment_intent.payment_failed'\r\n | 'payment_intent.canceled'\r\n | 'payment_intent.amount_capturable_updated'\r\n | 'charge.succeeded'\r\n | 'charge.failed'\r\n | 'charge.refunded'\r\n | 'charge.dispute.created'\r\n | 'charge.dispute.closed'\r\n | 'customer.subscription.created'\r\n | 'customer.subscription.updated'\r\n | 'customer.subscription.deleted'\r\n | 'invoice.paid'\r\n | 'invoice.payment_failed';\r\n\r\n/**\r\n * Map of Stripe events to transaction status updates\r\n */\r\nexport const STRIPE_EVENT_TO_STATUS: Partial<Record<StripeEventType, TransactionStatus>> = {\r\n 'payment_intent.succeeded': 'captured',\r\n 'payment_intent.payment_failed': 'failed',\r\n 'payment_intent.canceled': 'voided',\r\n 'payment_intent.amount_capturable_updated': 'authorized',\r\n 'charge.refunded': 'partially_refunded', // or fully_refunded based on amount\r\n};\r\n\r\n// ============================================================================\r\n// Webhook Signature Verification\r\n// ============================================================================\r\n\r\n/**\r\n * Parameters for verifying webhook signature\r\n */\r\nexport interface WebhookVerificationParams {\r\n payload: string;\r\n signature: string;\r\n secret: string;\r\n /** Tolerance in seconds for timestamp validation */\r\n tolerance?: number;\r\n}\r\n\r\n/**\r\n * Result of webhook signature verification\r\n */\r\nexport interface WebhookVerificationResult {\r\n valid: boolean;\r\n error?: string;\r\n}\r\n\r\n// ============================================================================\r\n// Webhook Queue Types (for SQS processing)\r\n// ============================================================================\r\n\r\n/**\r\n * Message format for webhook queue\r\n */\r\nexport interface WebhookQueueMessage {\r\n /** Message ID for deduplication */\r\n messageId: string;\r\n /** Provider that sent the webhook */\r\n provider: PaymentProvider;\r\n /** Provider's event ID */\r\n eventId: string;\r\n /** Event type */\r\n eventType: string;\r\n /** Raw payload (JSON string) */\r\n payload: string;\r\n /** Signature for verification */\r\n signature: string;\r\n /** When received by our API */\r\n receivedAt: string;\r\n /** Number of processing attempts */\r\n attemptCount: number;\r\n}\r\n\r\n/**\r\n * Result of queue message processing\r\n */\r\nexport interface QueueProcessingResult {\r\n success: boolean;\r\n messageId: string;\r\n action?: WebhookAction;\r\n shouldDelete: boolean;\r\n shouldRetry: boolean;\r\n error?: string;\r\n}\r\n\r\n// ============================================================================\r\n// Reconciliation Types\r\n// ============================================================================\r\n\r\n/**\r\n * Result of reconciling payment state\r\n * Handles race condition between redirect and webhook\r\n */\r\nexport interface ReconciliationResult {\r\n reconciled: boolean;\r\n /** Final determined status */\r\n finalStatus: TransactionStatus;\r\n /** Source of truth (redirect callback vs webhook) */\r\n source: 'redirect' | 'webhook' | 'provider_query';\r\n /** Whether status was updated */\r\n statusChanged: boolean;\r\n}\r\n\r\n/**\r\n * Reconciliation strategy when redirect and webhook conflict\r\n */\r\nexport type ReconciliationStrategy =\r\n | 'prefer_webhook' // Wait for webhook (more reliable)\r\n | 'prefer_redirect' // Use redirect result immediately\r\n | 'query_provider'; // Query provider API directly\r\n"],"mappings":";AAgDO,IAAM,kBAAgD;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,IAAM,iBAA+C;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AACF;AAKO,IAAM,cAA4C;AAAA,EACvD;AACF;AA8BO,IAAM,oBAAmE;AAAA,EAC9E,SAAS,CAAC,YAAY,qBAAqB,kBAAkB;AAAA,EAC7D,uBAAuB,CAAC,qBAAqB,oBAAoB,SAAS;AAAA,EAC1E,YAAY,CAAC,mBAAmB,gBAAgB,eAAe,SAAS;AAAA,EACxE,WAAW,CAAC,mBAAmB,gBAAgB;AAAA,EAC/C,UAAU,CAAC,kBAAkB,aAAa;AAAA,EAC1C,QAAQ,CAAC;AAAA;AAAA,EACT,QAAQ,CAAC;AAAA;AAAA,EACT,SAAS,CAAC;AAAA;AAAA,EACV,oBAAoB,CAAC,kBAAkB,aAAa;AAAA,EACpD,gBAAgB,CAAC;AAAA;AACnB;AAKA,IAAM,iBAA8D;AAAA,EAClE,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,aAAa;AAAA;AAAA,EACb,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,aAAa;AACf;AASO,SAAS,cACd,eACA,OACS;AACT,SAAO,kBAAkB,aAAa,EAAE,SAAS,KAAK;AACxD;AAKO,SAAS,cACd,eACA,OAC0B;AAC1B,MAAI,CAAC,cAAc,eAAe,KAAK,GAAG;AACxC,WAAO;AAAA,EACT;AACA,SAAO,eAAe,KAAK;AAC7B;AAKO,SAAS,gBAAgB,QAAoC;AAClE,SAAO,gBAAgB,SAAS,MAAM;AACxC;AAKO,SAAS,eAAe,QAAoC;AACjE,SAAO,eAAe,SAAS,MAAM;AACvC;AAKO,SAAS,YAAY,QAAoC;AAC9D,SAAO,YAAY,SAAS,MAAM;AACpC;AAKO,SAAS,UAAU,QAAoC;AAC5D,SAAO,WAAW,cAAc,WAAW;AAC7C;AAKO,SAAS,WAAW,QAAoC;AAC7D,SAAO,WAAW;AACpB;AAKO,SAAS,QAAQ,QAAoC;AAC1D,SAAO,WAAW;AACpB;AAoBO,SAAS,kBACd,eACA,OACuB;AACvB,QAAM,aAAa,cAAc,eAAe,KAAK;AAErD,MAAI,eAAe,MAAM;AACvB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,gBAAgB;AAAA,MAChB,WAAW;AAAA,MACX;AAAA,MACA,OAAO,uBAAuB,aAAa,OAAO,KAAK;AAAA,IACzD;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,WAAW;AAAA,IACX;AAAA,EACF;AACF;AASO,IAAM,yBAAyB;AAK/B,SAAS,yBACd,cACA,WAAmB,wBACb;AACN,QAAM,WAAW,IAAI,KAAK,YAAY;AACtC,WAAS,QAAQ,SAAS,QAAQ,IAAI,QAAQ;AAC9C,SAAO;AACT;AAKO,SAAS,uBAAuB,iBAAgC;AACrE,SAAO,oBAAI,KAAK,IAAI;AACtB;;;AC1KO,IAAM,yBAA8E;AAAA,EACzF,4BAA4B;AAAA,EAC5B,iCAAiC;AAAA,EACjC,2BAA2B;AAAA,EAC3B,4CAA4C;AAAA,EAC5C,mBAAmB;AAAA;AACrB;","names":[]}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/utils/index.ts
|
|
21
|
+
var utils_exports = {};
|
|
22
|
+
__export(utils_exports, {
|
|
23
|
+
extractUuid: () => extractUuid,
|
|
24
|
+
generateDeterministicKey: () => generateDeterministicKey,
|
|
25
|
+
generateIdempotencyKey: () => generateIdempotencyKey,
|
|
26
|
+
generateInternalPaymentId: () => generateInternalPaymentId,
|
|
27
|
+
generateOperationKey: () => generateOperationKey,
|
|
28
|
+
getSignatureHeaderName: () => getSignatureHeaderName,
|
|
29
|
+
getSignatureVerifier: () => getSignatureVerifier,
|
|
30
|
+
isValidIdempotencyKey: () => isValidIdempotencyKey,
|
|
31
|
+
isValidInternalPaymentId: () => isValidInternalPaymentId,
|
|
32
|
+
registerSignatureVerifier: () => registerSignatureVerifier,
|
|
33
|
+
verifyHmacSha256Signature: () => verifyHmacSha256Signature,
|
|
34
|
+
verifySortedFieldsHmacSignature: () => verifySortedFieldsHmacSignature,
|
|
35
|
+
verifyStripeStyleSignature: () => verifyStripeStyleSignature,
|
|
36
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(utils_exports);
|
|
39
|
+
|
|
40
|
+
// src/utils/idempotency.ts
|
|
41
|
+
var import_crypto = require("crypto");
|
|
42
|
+
function generateInternalPaymentId() {
|
|
43
|
+
return `pay_${(0, import_crypto.randomUUID)()}`;
|
|
44
|
+
}
|
|
45
|
+
function generateIdempotencyKey() {
|
|
46
|
+
return `idem_${(0, import_crypto.randomUUID)()}`;
|
|
47
|
+
}
|
|
48
|
+
function generateDeterministicKey(...components) {
|
|
49
|
+
const data = components.join(":");
|
|
50
|
+
const hash = (0, import_crypto.createHash)("sha256").update(data).digest("hex");
|
|
51
|
+
return `idem_${hash.substring(0, 32)}`;
|
|
52
|
+
}
|
|
53
|
+
function generateOperationKey(operation, transactionId) {
|
|
54
|
+
return `${operation}_${transactionId}`;
|
|
55
|
+
}
|
|
56
|
+
function isValidIdempotencyKey(key) {
|
|
57
|
+
return /^idem_[a-f0-9-]{32,36}$/.test(key);
|
|
58
|
+
}
|
|
59
|
+
function isValidInternalPaymentId(id) {
|
|
60
|
+
return /^pay_[a-f0-9-]{36}$/.test(id);
|
|
61
|
+
}
|
|
62
|
+
function extractUuid(key) {
|
|
63
|
+
const match = key.match(/^(?:pay|idem)_([a-f0-9-]+)$/);
|
|
64
|
+
return match ? match[1] : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/utils/signature-verification.ts
|
|
68
|
+
var import_crypto2 = require("crypto");
|
|
69
|
+
function verifyStripeStyleSignature(payload, signature, secret, tolerance) {
|
|
70
|
+
try {
|
|
71
|
+
const elements = signature.split(",");
|
|
72
|
+
const timestamp = elements.find((e) => e.startsWith("t="))?.slice(2);
|
|
73
|
+
const sig = elements.find((e) => e.startsWith("v1="))?.slice(3);
|
|
74
|
+
if (!timestamp || !sig) {
|
|
75
|
+
return { valid: false, error: "Invalid signature format" };
|
|
76
|
+
}
|
|
77
|
+
const timestampNum = parseInt(timestamp, 10);
|
|
78
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
79
|
+
if (Math.abs(now - timestampNum) > tolerance) {
|
|
80
|
+
return { valid: false, error: "Timestamp outside tolerance" };
|
|
81
|
+
}
|
|
82
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
83
|
+
const expectedSig = (0, import_crypto2.createHmac)("sha256", secret).update(signedPayload).digest("hex");
|
|
84
|
+
const valid = (0, import_crypto2.timingSafeEqual)(
|
|
85
|
+
Buffer.from(sig),
|
|
86
|
+
Buffer.from(expectedSig)
|
|
87
|
+
);
|
|
88
|
+
return { valid };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function verifySortedFieldsHmacSignature(payload, signature, secret, _tolerance) {
|
|
97
|
+
try {
|
|
98
|
+
const data = JSON.parse(payload);
|
|
99
|
+
const sortedKeys = Object.keys(data).sort();
|
|
100
|
+
const sortedPayload = sortedKeys.map((k) => `${k}=${data[k]}`).join("&");
|
|
101
|
+
const expectedSig = (0, import_crypto2.createHmac)("sha256", secret).update(sortedPayload).digest("hex");
|
|
102
|
+
const valid = (0, import_crypto2.timingSafeEqual)(
|
|
103
|
+
Buffer.from(signature.toLowerCase()),
|
|
104
|
+
Buffer.from(expectedSig)
|
|
105
|
+
);
|
|
106
|
+
return { valid };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
valid: false,
|
|
110
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function verifyHmacSha256Signature(payload, signature, secret, _tolerance) {
|
|
115
|
+
try {
|
|
116
|
+
const expectedSig = (0, import_crypto2.createHmac)("sha256", secret).update(payload).digest("hex");
|
|
117
|
+
const valid = (0, import_crypto2.timingSafeEqual)(
|
|
118
|
+
Buffer.from(signature.toLowerCase()),
|
|
119
|
+
Buffer.from(expectedSig.toLowerCase())
|
|
120
|
+
);
|
|
121
|
+
return { valid };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
valid: false,
|
|
125
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
var verifierRegistry = /* @__PURE__ */ new Map();
|
|
130
|
+
function registerSignatureVerifier(provider, verifier) {
|
|
131
|
+
verifierRegistry.set(provider, verifier);
|
|
132
|
+
}
|
|
133
|
+
function getSignatureVerifier(provider) {
|
|
134
|
+
return verifierRegistry.get(provider);
|
|
135
|
+
}
|
|
136
|
+
function verifyWebhookSignature(params) {
|
|
137
|
+
const { provider, payload, signature, secret, tolerance = 300 } = params;
|
|
138
|
+
if (!signature || !secret) {
|
|
139
|
+
return { valid: false, error: "Missing signature or secret" };
|
|
140
|
+
}
|
|
141
|
+
const verifier = verifierRegistry.get(provider);
|
|
142
|
+
if (verifier) {
|
|
143
|
+
return verifier(payload, signature, secret, tolerance);
|
|
144
|
+
}
|
|
145
|
+
return verifyHmacSha256Signature(payload, signature, secret, tolerance);
|
|
146
|
+
}
|
|
147
|
+
function getSignatureHeaderName(provider) {
|
|
148
|
+
return `x-${provider}-signature`;
|
|
149
|
+
}
|
|
150
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
151
|
+
0 && (module.exports = {
|
|
152
|
+
extractUuid,
|
|
153
|
+
generateDeterministicKey,
|
|
154
|
+
generateIdempotencyKey,
|
|
155
|
+
generateInternalPaymentId,
|
|
156
|
+
generateOperationKey,
|
|
157
|
+
getSignatureHeaderName,
|
|
158
|
+
getSignatureVerifier,
|
|
159
|
+
isValidIdempotencyKey,
|
|
160
|
+
isValidInternalPaymentId,
|
|
161
|
+
registerSignatureVerifier,
|
|
162
|
+
verifyHmacSha256Signature,
|
|
163
|
+
verifySortedFieldsHmacSignature,
|
|
164
|
+
verifyStripeStyleSignature,
|
|
165
|
+
verifyWebhookSignature
|
|
166
|
+
});
|
|
167
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/index.ts","../../src/utils/idempotency.ts","../../src/utils/signature-verification.ts"],"sourcesContent":["/**\r\n * @nehorai/payments - Utilities Exports\r\n */\r\n\r\n// Idempotency\r\nexport {\r\n generateInternalPaymentId,\r\n generateIdempotencyKey,\r\n generateDeterministicKey,\r\n generateOperationKey,\r\n isValidIdempotencyKey,\r\n isValidInternalPaymentId,\r\n extractUuid,\r\n} from './idempotency.js'\r\n\r\n// Signature Verification\r\nexport {\r\n verifyWebhookSignature,\r\n verifyStripeStyleSignature,\r\n verifySortedFieldsHmacSignature,\r\n verifyHmacSha256Signature,\r\n registerSignatureVerifier,\r\n getSignatureVerifier,\r\n getSignatureHeaderName,\r\n type SignatureVerificationParams,\r\n type SignatureVerificationResult,\r\n type SignatureVerifier,\r\n} from './signature-verification.js'\r\n","/**\r\n * @nehorai/payments - Idempotency Utilities\r\n *\r\n * Generates and validates idempotency keys to prevent duplicate charges.\r\n * Framework-agnostic utility that can be used anywhere.\r\n */\r\n\r\nimport { randomUUID, createHash } from 'crypto'\r\n\r\n/**\r\n * Generate a unique internal payment ID\r\n * Format: pay_{uuid}\r\n */\r\nexport function generateInternalPaymentId(): string {\r\n return `pay_${randomUUID()}`\r\n}\r\n\r\n/**\r\n * Generate an idempotency key for API calls\r\n * Format: idem_{uuid}\r\n */\r\nexport function generateIdempotencyKey(): string {\r\n return `idem_${randomUUID()}`\r\n}\r\n\r\n/**\r\n * Generate a deterministic idempotency key based on inputs\r\n * Useful when you need the same key for retries\r\n *\r\n * @param components - Array of values to hash together\r\n */\r\nexport function generateDeterministicKey(\r\n ...components: (string | number)[]\r\n): string {\r\n const data = components.join(':')\r\n const hash = createHash('sha256').update(data).digest('hex')\r\n return `idem_${hash.substring(0, 32)}`\r\n}\r\n\r\n/**\r\n * Generate idempotency key for a specific operation\r\n *\r\n * @param operation - Type of operation (e.g., 'capture', 'refund')\r\n * @param transactionId - Associated transaction ID\r\n */\r\nexport function generateOperationKey(\r\n operation: string,\r\n transactionId: string\r\n): string {\r\n return `${operation}_${transactionId}`\r\n}\r\n\r\n/**\r\n * Validate idempotency key format\r\n */\r\nexport function isValidIdempotencyKey(key: string): boolean {\r\n return /^idem_[a-f0-9-]{32,36}$/.test(key)\r\n}\r\n\r\n/**\r\n * Validate internal payment ID format\r\n */\r\nexport function isValidInternalPaymentId(id: string): boolean {\r\n return /^pay_[a-f0-9-]{36}$/.test(id)\r\n}\r\n\r\n/**\r\n * Extract UUID from payment ID or idempotency key\r\n */\r\nexport function extractUuid(key: string): string | null {\r\n const match = key.match(/^(?:pay|idem)_([a-f0-9-]+)$/)\r\n return match ? match[1] : null\r\n}\r\n","/**\r\n * @nehorai/payments - Webhook Signature Verification\r\n *\r\n * Verifies HMAC signatures from payment providers to prevent spoofing.\r\n * Provides generic verification functions plus a registry pattern\r\n * for provider-specific verification strategies.\r\n */\r\n\r\nimport { createHmac, createHash, timingSafeEqual } from 'crypto'\r\nimport type { PaymentProvider } from '../types/index.js'\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport interface SignatureVerificationParams {\r\n provider: PaymentProvider\r\n payload: string\r\n signature: string\r\n secret: string\r\n /** Tolerance in seconds for timestamp validation (default: 300) */\r\n tolerance?: number\r\n}\r\n\r\nexport interface SignatureVerificationResult {\r\n valid: boolean\r\n error?: string\r\n}\r\n\r\n/**\r\n * A function that verifies a webhook signature for a specific provider\r\n */\r\nexport type SignatureVerifier = (\r\n payload: string,\r\n signature: string,\r\n secret: string,\r\n tolerance: number\r\n) => SignatureVerificationResult\r\n\r\n// ============================================================================\r\n// Built-in Verification Strategies\r\n// ============================================================================\r\n\r\n/**\r\n * Verify a Stripe-style webhook signature\r\n * Stripe uses: t={timestamp},v1={signature}\r\n */\r\nexport function verifyStripeStyleSignature(\r\n payload: string,\r\n signature: string,\r\n secret: string,\r\n tolerance: number\r\n): SignatureVerificationResult {\r\n try {\r\n const elements = signature.split(',')\r\n const timestamp = elements.find((e) => e.startsWith('t='))?.slice(2)\r\n const sig = elements.find((e) => e.startsWith('v1='))?.slice(3)\r\n\r\n if (!timestamp || !sig) {\r\n return { valid: false, error: 'Invalid signature format' }\r\n }\r\n\r\n const timestampNum = parseInt(timestamp, 10)\r\n const now = Math.floor(Date.now() / 1000)\r\n if (Math.abs(now - timestampNum) > tolerance) {\r\n return { valid: false, error: 'Timestamp outside tolerance' }\r\n }\r\n\r\n const signedPayload = `${timestamp}.${payload}`\r\n const expectedSig = createHmac('sha256', secret)\r\n .update(signedPayload)\r\n .digest('hex')\r\n\r\n const valid = timingSafeEqual(\r\n Buffer.from(sig),\r\n Buffer.from(expectedSig)\r\n )\r\n\r\n return { valid }\r\n } catch (error) {\r\n return {\r\n valid: false,\r\n error: error instanceof Error ? error.message : 'Verification failed',\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Verify an HMAC-SHA256 signature of sorted payload fields\r\n */\r\nexport function verifySortedFieldsHmacSignature(\r\n payload: string,\r\n signature: string,\r\n secret: string,\r\n _tolerance: number\r\n): SignatureVerificationResult {\r\n try {\r\n const data = JSON.parse(payload)\r\n const sortedKeys = Object.keys(data).sort()\r\n const sortedPayload = sortedKeys.map((k) => `${k}=${data[k]}`).join('&')\r\n\r\n const expectedSig = createHmac('sha256', secret)\r\n .update(sortedPayload)\r\n .digest('hex')\r\n\r\n const valid = timingSafeEqual(\r\n Buffer.from(signature.toLowerCase()),\r\n Buffer.from(expectedSig)\r\n )\r\n\r\n return { valid }\r\n } catch (error) {\r\n return {\r\n valid: false,\r\n error: error instanceof Error ? error.message : 'Verification failed',\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Verify a simple HMAC-SHA256 signature of the raw payload\r\n */\r\nexport function verifyHmacSha256Signature(\r\n payload: string,\r\n signature: string,\r\n secret: string,\r\n _tolerance: number\r\n): SignatureVerificationResult {\r\n try {\r\n const expectedSig = createHmac('sha256', secret)\r\n .update(payload)\r\n .digest('hex')\r\n\r\n const valid = timingSafeEqual(\r\n Buffer.from(signature.toLowerCase()),\r\n Buffer.from(expectedSig.toLowerCase())\r\n )\r\n\r\n return { valid }\r\n } catch (error) {\r\n return {\r\n valid: false,\r\n error: error instanceof Error ? error.message : 'Verification failed',\r\n }\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Verifier Registry\r\n// ============================================================================\r\n\r\nconst verifierRegistry = new Map<string, SignatureVerifier>()\r\n\r\n/**\r\n * Register a custom signature verifier for a provider\r\n */\r\nexport function registerSignatureVerifier(\r\n provider: PaymentProvider,\r\n verifier: SignatureVerifier\r\n): void {\r\n verifierRegistry.set(provider, verifier)\r\n}\r\n\r\n/**\r\n * Get the registered verifier for a provider\r\n */\r\nexport function getSignatureVerifier(provider: PaymentProvider): SignatureVerifier | undefined {\r\n return verifierRegistry.get(provider)\r\n}\r\n\r\n// ============================================================================\r\n// Main Verification Function\r\n// ============================================================================\r\n\r\n/**\r\n * Verify webhook signature based on provider.\r\n * Uses registered verifiers. Falls back to HMAC-SHA256 if no verifier is registered.\r\n */\r\nexport function verifyWebhookSignature(\r\n params: SignatureVerificationParams\r\n): SignatureVerificationResult {\r\n const { provider, payload, signature, secret, tolerance = 300 } = params\r\n\r\n if (!signature || !secret) {\r\n return { valid: false, error: 'Missing signature or secret' }\r\n }\r\n\r\n const verifier = verifierRegistry.get(provider)\r\n if (verifier) {\r\n return verifier(payload, signature, secret, tolerance)\r\n }\r\n\r\n // Default: simple HMAC-SHA256\r\n return verifyHmacSha256Signature(payload, signature, secret, tolerance)\r\n}\r\n\r\n/**\r\n * Get signature header name for a provider.\r\n * Returns a generic default; override per provider if needed.\r\n */\r\nexport function getSignatureHeaderName(provider: PaymentProvider): string {\r\n return `x-${provider}-signature`\r\n}\r\n"],"mappings":"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","names":["import_crypto"]}
|