@vesant-sdk/transaction 0.1.0 → 0.1.1-dev.059da4f

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.d.mts CHANGED
@@ -1,32 +1,38 @@
1
- import { Timestamp, BaseClient, BaseClientConfig, RequestOptions } from '@vesant-sdk/core';
1
+ import { BaseClient, BaseClientConfig, RequestOptions, VesantError } from '@vesant-sdk/core';
2
2
 
3
3
  /**
4
4
  * TypeScript types for the Transaction Monitoring (TM) Service
5
5
  */
6
-
7
- type TransactionType = "deposit" | "withdrawal" | "transfer" | "payment" | "refund" | "fee" | "adjustment";
8
- type TransactionMode = "fiat" | "crypto" | "bank_transfer" | "card" | "wallet";
9
- type TransactionStatus = "pending" | "processing" | "completed" | "failed" | "cancelled" | "reversed";
6
+ type TransactionType = "withdrawal" | "deposit" | "transfer" | "bet" | "payment" | "adjustment" | "payout";
7
+ type TransactionMode = "wire_transfer" | "crypto" | "swift" | "ach" | "bank_transfer" | "card";
8
+ type TransactionSubType = "sweepstakes" | "cash";
9
+ type TransactionStatus = "pending" | "processing" | "completed" | "failed" | "cancelled" | "reversed" | "blocked" | "flagged" | "hold" | "suspend";
10
10
  interface TransactionCreateDTO {
11
11
  tx_id: string;
12
12
  reference: string;
13
- tenant_id: string;
14
13
  customer_id: string;
15
14
  transaction_type: TransactionType;
16
15
  transaction_mode: TransactionMode;
16
+ sub_type?: TransactionSubType;
17
17
  amount: string;
18
+ amount_usd: number;
18
19
  currency: string;
19
- status: TransactionStatus;
20
- source_account: string;
21
- destination_account: string;
22
- country: string;
23
- ip_address: string;
24
- metadata: Record<string, any>;
25
- benificiary_comment: string;
26
- transaction_date: Timestamp;
20
+ source_account?: string;
21
+ destination_account?: string;
22
+ country?: string;
23
+ ip_address?: string;
24
+ benificiary_comment?: string;
25
+ metadata?: Record<string, unknown>;
26
+ transaction_date: string;
27
27
  }
28
28
  type WithholdingType = "none" | "backup_us" | "backup_non_us" | "treaty";
29
- type JSONB = Record<string, any>;
29
+ /** Reason backup withholding or treaty rate was applied. */
30
+ type WithholdingReason = "tin_not_verified" | "customer_tax_form_not_verified" | "b_notice_backup_withholding";
31
+ /**
32
+ * Reason a withdrawal was suspended (status = "suspend").
33
+ * Reflects TIN/W-9 deficiencies that triggered the tenant's configured transaction action.
34
+ */
35
+ type HoldReason = "no_tax_profile_found" | "tin_rejected" | "tin_expired" | "tin_not_provided" | "tin_not_verified" | "form_not_certified";
30
36
  type Transaction = {
31
37
  id: string;
32
38
  tx_id: string;
@@ -35,12 +41,23 @@ type Transaction = {
35
41
  customer_id: string;
36
42
  transaction_type: TransactionType;
37
43
  transaction_mode: TransactionMode;
44
+ sub_type?: TransactionSubType;
38
45
  amount: string;
39
46
  withheld_amount: string;
40
47
  released_amount: string;
41
48
  withholding_rate: number;
49
+ withholding_type: string;
42
50
  withholding_reason?: string;
43
- withholding_type?: WithholdingType;
51
+ hold_reason?: string;
52
+ amount_usd: number;
53
+ withheld_usd: number;
54
+ released_usd: number;
55
+ /**
56
+ * True when this is the customer's first withdrawal.
57
+ * Correct on the POST /transactions response and on tm.transaction.callback.
58
+ * Not persisted — getTransaction() always returns false regardless of actual history.
59
+ */
60
+ is_first_withdrawal?: boolean;
44
61
  tax_year: number;
45
62
  currency: string;
46
63
  status: TransactionStatus;
@@ -49,7 +66,7 @@ type Transaction = {
49
66
  country?: string;
50
67
  ip_address?: string;
51
68
  risk_score: number;
52
- metadata?: JSONB;
69
+ metadata?: Record<string, unknown>;
53
70
  benificiary_comment?: string;
54
71
  transaction_date: string;
55
72
  created_at: string;
@@ -66,10 +83,99 @@ interface TransactionClientConfig {
66
83
  debug?: boolean;
67
84
  environment?: 'production' | 'sandbox';
68
85
  }
86
+ /**
87
+ * Response from POST /api/v1/tm/transactions.
88
+ *
89
+ * Status → outcome mapping (withdrawal transactions only):
90
+ * "pending" — Vesant accepted; trigger the payment gateway (full release,
91
+ * backup withholding applied, or treaty rate applied)
92
+ * "suspend" — blocked, hold_reason set, no funds moved
93
+ */
69
94
  type TransactionCreateResponse = {
70
95
  transaction: Transaction;
71
96
  message: string;
72
97
  };
98
+ /**
99
+ * Fired after every transaction is created and after a suspended transaction is released.
100
+ * Delivers the final compliance-evaluated state including withholding outcome.
101
+ */
102
+ interface TransactionCallbackEvent {
103
+ event_type: "tm.transaction.callback";
104
+ id: string;
105
+ tenant_id: string;
106
+ entity_id: string;
107
+ user_id: string;
108
+ tx_id: string;
109
+ reference: string;
110
+ customer_id: string;
111
+ transaction_type: string;
112
+ transaction_mode: string;
113
+ status: string;
114
+ amount: string;
115
+ currency: string;
116
+ withheld_amount: string;
117
+ released_amount: string;
118
+ withholding_rate: number;
119
+ withholding_type: string;
120
+ withholding_reason?: string | null;
121
+ hold_reason?: string | null;
122
+ amount_usd: number;
123
+ withheld_usd: number;
124
+ released_usd: number;
125
+ is_first_withdrawal: boolean;
126
+ transaction_date: string;
127
+ }
128
+ /**
129
+ * Fired when a withdrawal is blocked due to missing or unverified W-9/TIN
130
+ * (status = "suspend" or "hold").
131
+ * Use this to notify the customer that TIN verification or W-9 submission is required.
132
+ */
133
+ interface TransactionHeldEvent {
134
+ event_type: "tm.transaction.held";
135
+ tx_id: string;
136
+ customer_id: string;
137
+ customer_email: string;
138
+ customer_name: string;
139
+ /** Reflects the tenant's configured transaction action that was applied. */
140
+ action: "suspend" | "hold";
141
+ hold_reason: HoldReason;
142
+ }
143
+ /** Discriminated union of all TM service webhook event payloads. */
144
+ type TransactionWebhookEvent = TransactionCallbackEvent | TransactionHeldEvent;
145
+ interface TmTenantSettings {
146
+ tenant_id: string;
147
+ transaction_monitoring_enabled: boolean;
148
+ tax_monitoring_enabled: boolean;
149
+ }
150
+ /**
151
+ * Semantic handler interface for the three withholding outcomes of a tm.transaction.callback.
152
+ * Register with TransactionWebhookHandler.registerCallbackHandler().
153
+ */
154
+ interface TransactionCallbackHandler {
155
+ onWithheld?(event: TransactionCallbackEvent): void | Promise<void>;
156
+ onHeld?(event: TransactionCallbackEvent): void | Promise<void>;
157
+ onReleased?(event: TransactionCallbackEvent): void | Promise<void>;
158
+ }
159
+
160
+ type WithdrawalState = 'vesant_pending' | 'withheld' | 'suspend' | 'released' | 'failed';
161
+ declare class PendingWithdrawal {
162
+ private _state;
163
+ private _event;
164
+ private _error;
165
+ private _waitPromise;
166
+ private _resolveWait?;
167
+ private _rejectWait?;
168
+ readonly tx_id: string;
169
+ readonly initialTransaction: Transaction;
170
+ constructor(tx_id: string, transaction: Transaction);
171
+ status(): WithdrawalState;
172
+ callbackEvent(): TransactionCallbackEvent | null;
173
+ wait(signal?: AbortSignal): Promise<TransactionCallbackEvent>;
174
+ /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */
175
+ _settle(event: TransactionCallbackEvent): void;
176
+ /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */
177
+ _fail(err: unknown): void;
178
+ }
73
179
 
74
180
  /**
75
181
  * TransactionClient — SDK for the Transaction Monitoring service
@@ -78,7 +184,56 @@ type TransactionCreateResponse = {
78
184
  declare class TransactionClient extends BaseClient {
79
185
  constructor(config: BaseClientConfig);
80
186
  createTransaction(request: TransactionCreateDTO, requestOptions?: RequestOptions): Promise<TransactionCreateResponse>;
81
- getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<void>;
187
+ submitWithdrawal(request: TransactionCreateDTO, requestOptions?: RequestOptions): Promise<PendingWithdrawal>;
188
+ getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<Transaction>;
189
+ getSettings(): Promise<TmTenantSettings>;
190
+ }
191
+
192
+ declare class DuplicateTransactionError extends VesantError {
193
+ readonly tx_id: string;
194
+ constructor(tx_id: string, message?: string);
195
+ }
196
+ declare class TaxHoldError extends Error {
197
+ readonly tx_id: string;
198
+ readonly hold_reason: HoldReason;
199
+ readonly event: TransactionCallbackEvent;
200
+ constructor(event: TransactionCallbackEvent);
201
+ }
202
+
203
+ type EventHandler<T> = (event: T) => void | Promise<void>;
204
+ declare const TRANSACTION_SIGNATURE_HEADER = "x-webhook-signature";
205
+ interface TransactionWebhookHandlerConfig {
206
+ secret: string;
207
+ /** Reject duplicate event_type+tx_id pairs within replayWindow. Default: true. */
208
+ replayProtection?: boolean;
209
+ /** How long (ms) to remember seen events for replay detection. Default: 300_000 (5 min). */
210
+ replayWindow?: number;
211
+ }
212
+ declare class TransactionWebhookHandler {
213
+ private readonly handlers;
214
+ private readonly secret;
215
+ private readonly replayProtection;
216
+ private readonly replayWindow;
217
+ private readonly seenEvents;
218
+ private readonly pendingWithdrawals;
219
+ private callbackHandler;
220
+ constructor(config: TransactionWebhookHandlerConfig);
221
+ on(eventType: 'tm.transaction.callback', handler: EventHandler<TransactionCallbackEvent>): this;
222
+ on(eventType: 'tm.transaction.held', handler: EventHandler<TransactionHeldEvent>): this;
223
+ /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */
224
+ registerCallbackHandler(handler: TransactionCallbackHandler): this;
225
+ /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */
226
+ link(pending: PendingWithdrawal): this;
227
+ /** Stop tracking a PendingWithdrawal by tx_id. */
228
+ unlink(tx_id: string): this;
229
+ /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */
230
+ verify(rawBody: string, signature: string): Promise<boolean>;
231
+ /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */
232
+ parse(body: string): TransactionWebhookEvent;
233
+ /** Verify signature, parse, and check for replays. Throws ValidationError on any failure. */
234
+ verifyAndParse(body: string, signature: string): Promise<TransactionWebhookEvent>;
235
+ /** Verify signature, parse, and dispatch to registered handlers. */
236
+ handle(body: string, signature: string): Promise<void>;
82
237
  }
83
238
 
84
- export { type JSONB, type Transaction, TransactionClient, type TransactionClientConfig, type TransactionCreateDTO, type TransactionCreateResponse, type TransactionMode, type TransactionStatus, type TransactionType, type WithholdingType };
239
+ export { DuplicateTransactionError, type HoldReason, PendingWithdrawal, TRANSACTION_SIGNATURE_HEADER, TaxHoldError, type TmTenantSettings, type Transaction, type TransactionCallbackEvent, type TransactionCallbackHandler, TransactionClient, type TransactionClientConfig, type TransactionCreateDTO, type TransactionCreateResponse, type TransactionHeldEvent, type TransactionMode, type TransactionStatus, type TransactionSubType, type TransactionType, type TransactionWebhookEvent, TransactionWebhookHandler, type TransactionWebhookHandlerConfig, type WithdrawalState, type WithholdingReason, type WithholdingType };
package/dist/index.d.ts CHANGED
@@ -1,32 +1,38 @@
1
- import { Timestamp, BaseClient, BaseClientConfig, RequestOptions } from '@vesant-sdk/core';
1
+ import { BaseClient, BaseClientConfig, RequestOptions, VesantError } from '@vesant-sdk/core';
2
2
 
3
3
  /**
4
4
  * TypeScript types for the Transaction Monitoring (TM) Service
5
5
  */
6
-
7
- type TransactionType = "deposit" | "withdrawal" | "transfer" | "payment" | "refund" | "fee" | "adjustment";
8
- type TransactionMode = "fiat" | "crypto" | "bank_transfer" | "card" | "wallet";
9
- type TransactionStatus = "pending" | "processing" | "completed" | "failed" | "cancelled" | "reversed";
6
+ type TransactionType = "withdrawal" | "deposit" | "transfer" | "bet" | "payment" | "adjustment" | "payout";
7
+ type TransactionMode = "wire_transfer" | "crypto" | "swift" | "ach" | "bank_transfer" | "card";
8
+ type TransactionSubType = "sweepstakes" | "cash";
9
+ type TransactionStatus = "pending" | "processing" | "completed" | "failed" | "cancelled" | "reversed" | "blocked" | "flagged" | "hold" | "suspend";
10
10
  interface TransactionCreateDTO {
11
11
  tx_id: string;
12
12
  reference: string;
13
- tenant_id: string;
14
13
  customer_id: string;
15
14
  transaction_type: TransactionType;
16
15
  transaction_mode: TransactionMode;
16
+ sub_type?: TransactionSubType;
17
17
  amount: string;
18
+ amount_usd: number;
18
19
  currency: string;
19
- status: TransactionStatus;
20
- source_account: string;
21
- destination_account: string;
22
- country: string;
23
- ip_address: string;
24
- metadata: Record<string, any>;
25
- benificiary_comment: string;
26
- transaction_date: Timestamp;
20
+ source_account?: string;
21
+ destination_account?: string;
22
+ country?: string;
23
+ ip_address?: string;
24
+ benificiary_comment?: string;
25
+ metadata?: Record<string, unknown>;
26
+ transaction_date: string;
27
27
  }
28
28
  type WithholdingType = "none" | "backup_us" | "backup_non_us" | "treaty";
29
- type JSONB = Record<string, any>;
29
+ /** Reason backup withholding or treaty rate was applied. */
30
+ type WithholdingReason = "tin_not_verified" | "customer_tax_form_not_verified" | "b_notice_backup_withholding";
31
+ /**
32
+ * Reason a withdrawal was suspended (status = "suspend").
33
+ * Reflects TIN/W-9 deficiencies that triggered the tenant's configured transaction action.
34
+ */
35
+ type HoldReason = "no_tax_profile_found" | "tin_rejected" | "tin_expired" | "tin_not_provided" | "tin_not_verified" | "form_not_certified";
30
36
  type Transaction = {
31
37
  id: string;
32
38
  tx_id: string;
@@ -35,12 +41,23 @@ type Transaction = {
35
41
  customer_id: string;
36
42
  transaction_type: TransactionType;
37
43
  transaction_mode: TransactionMode;
44
+ sub_type?: TransactionSubType;
38
45
  amount: string;
39
46
  withheld_amount: string;
40
47
  released_amount: string;
41
48
  withholding_rate: number;
49
+ withholding_type: string;
42
50
  withholding_reason?: string;
43
- withholding_type?: WithholdingType;
51
+ hold_reason?: string;
52
+ amount_usd: number;
53
+ withheld_usd: number;
54
+ released_usd: number;
55
+ /**
56
+ * True when this is the customer's first withdrawal.
57
+ * Correct on the POST /transactions response and on tm.transaction.callback.
58
+ * Not persisted — getTransaction() always returns false regardless of actual history.
59
+ */
60
+ is_first_withdrawal?: boolean;
44
61
  tax_year: number;
45
62
  currency: string;
46
63
  status: TransactionStatus;
@@ -49,7 +66,7 @@ type Transaction = {
49
66
  country?: string;
50
67
  ip_address?: string;
51
68
  risk_score: number;
52
- metadata?: JSONB;
69
+ metadata?: Record<string, unknown>;
53
70
  benificiary_comment?: string;
54
71
  transaction_date: string;
55
72
  created_at: string;
@@ -66,10 +83,99 @@ interface TransactionClientConfig {
66
83
  debug?: boolean;
67
84
  environment?: 'production' | 'sandbox';
68
85
  }
86
+ /**
87
+ * Response from POST /api/v1/tm/transactions.
88
+ *
89
+ * Status → outcome mapping (withdrawal transactions only):
90
+ * "pending" — Vesant accepted; trigger the payment gateway (full release,
91
+ * backup withholding applied, or treaty rate applied)
92
+ * "suspend" — blocked, hold_reason set, no funds moved
93
+ */
69
94
  type TransactionCreateResponse = {
70
95
  transaction: Transaction;
71
96
  message: string;
72
97
  };
98
+ /**
99
+ * Fired after every transaction is created and after a suspended transaction is released.
100
+ * Delivers the final compliance-evaluated state including withholding outcome.
101
+ */
102
+ interface TransactionCallbackEvent {
103
+ event_type: "tm.transaction.callback";
104
+ id: string;
105
+ tenant_id: string;
106
+ entity_id: string;
107
+ user_id: string;
108
+ tx_id: string;
109
+ reference: string;
110
+ customer_id: string;
111
+ transaction_type: string;
112
+ transaction_mode: string;
113
+ status: string;
114
+ amount: string;
115
+ currency: string;
116
+ withheld_amount: string;
117
+ released_amount: string;
118
+ withholding_rate: number;
119
+ withholding_type: string;
120
+ withholding_reason?: string | null;
121
+ hold_reason?: string | null;
122
+ amount_usd: number;
123
+ withheld_usd: number;
124
+ released_usd: number;
125
+ is_first_withdrawal: boolean;
126
+ transaction_date: string;
127
+ }
128
+ /**
129
+ * Fired when a withdrawal is blocked due to missing or unverified W-9/TIN
130
+ * (status = "suspend" or "hold").
131
+ * Use this to notify the customer that TIN verification or W-9 submission is required.
132
+ */
133
+ interface TransactionHeldEvent {
134
+ event_type: "tm.transaction.held";
135
+ tx_id: string;
136
+ customer_id: string;
137
+ customer_email: string;
138
+ customer_name: string;
139
+ /** Reflects the tenant's configured transaction action that was applied. */
140
+ action: "suspend" | "hold";
141
+ hold_reason: HoldReason;
142
+ }
143
+ /** Discriminated union of all TM service webhook event payloads. */
144
+ type TransactionWebhookEvent = TransactionCallbackEvent | TransactionHeldEvent;
145
+ interface TmTenantSettings {
146
+ tenant_id: string;
147
+ transaction_monitoring_enabled: boolean;
148
+ tax_monitoring_enabled: boolean;
149
+ }
150
+ /**
151
+ * Semantic handler interface for the three withholding outcomes of a tm.transaction.callback.
152
+ * Register with TransactionWebhookHandler.registerCallbackHandler().
153
+ */
154
+ interface TransactionCallbackHandler {
155
+ onWithheld?(event: TransactionCallbackEvent): void | Promise<void>;
156
+ onHeld?(event: TransactionCallbackEvent): void | Promise<void>;
157
+ onReleased?(event: TransactionCallbackEvent): void | Promise<void>;
158
+ }
159
+
160
+ type WithdrawalState = 'vesant_pending' | 'withheld' | 'suspend' | 'released' | 'failed';
161
+ declare class PendingWithdrawal {
162
+ private _state;
163
+ private _event;
164
+ private _error;
165
+ private _waitPromise;
166
+ private _resolveWait?;
167
+ private _rejectWait?;
168
+ readonly tx_id: string;
169
+ readonly initialTransaction: Transaction;
170
+ constructor(tx_id: string, transaction: Transaction);
171
+ status(): WithdrawalState;
172
+ callbackEvent(): TransactionCallbackEvent | null;
173
+ wait(signal?: AbortSignal): Promise<TransactionCallbackEvent>;
174
+ /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */
175
+ _settle(event: TransactionCallbackEvent): void;
176
+ /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */
177
+ _fail(err: unknown): void;
178
+ }
73
179
 
74
180
  /**
75
181
  * TransactionClient — SDK for the Transaction Monitoring service
@@ -78,7 +184,56 @@ type TransactionCreateResponse = {
78
184
  declare class TransactionClient extends BaseClient {
79
185
  constructor(config: BaseClientConfig);
80
186
  createTransaction(request: TransactionCreateDTO, requestOptions?: RequestOptions): Promise<TransactionCreateResponse>;
81
- getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<void>;
187
+ submitWithdrawal(request: TransactionCreateDTO, requestOptions?: RequestOptions): Promise<PendingWithdrawal>;
188
+ getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<Transaction>;
189
+ getSettings(): Promise<TmTenantSettings>;
190
+ }
191
+
192
+ declare class DuplicateTransactionError extends VesantError {
193
+ readonly tx_id: string;
194
+ constructor(tx_id: string, message?: string);
195
+ }
196
+ declare class TaxHoldError extends Error {
197
+ readonly tx_id: string;
198
+ readonly hold_reason: HoldReason;
199
+ readonly event: TransactionCallbackEvent;
200
+ constructor(event: TransactionCallbackEvent);
201
+ }
202
+
203
+ type EventHandler<T> = (event: T) => void | Promise<void>;
204
+ declare const TRANSACTION_SIGNATURE_HEADER = "x-webhook-signature";
205
+ interface TransactionWebhookHandlerConfig {
206
+ secret: string;
207
+ /** Reject duplicate event_type+tx_id pairs within replayWindow. Default: true. */
208
+ replayProtection?: boolean;
209
+ /** How long (ms) to remember seen events for replay detection. Default: 300_000 (5 min). */
210
+ replayWindow?: number;
211
+ }
212
+ declare class TransactionWebhookHandler {
213
+ private readonly handlers;
214
+ private readonly secret;
215
+ private readonly replayProtection;
216
+ private readonly replayWindow;
217
+ private readonly seenEvents;
218
+ private readonly pendingWithdrawals;
219
+ private callbackHandler;
220
+ constructor(config: TransactionWebhookHandlerConfig);
221
+ on(eventType: 'tm.transaction.callback', handler: EventHandler<TransactionCallbackEvent>): this;
222
+ on(eventType: 'tm.transaction.held', handler: EventHandler<TransactionHeldEvent>): this;
223
+ /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */
224
+ registerCallbackHandler(handler: TransactionCallbackHandler): this;
225
+ /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */
226
+ link(pending: PendingWithdrawal): this;
227
+ /** Stop tracking a PendingWithdrawal by tx_id. */
228
+ unlink(tx_id: string): this;
229
+ /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */
230
+ verify(rawBody: string, signature: string): Promise<boolean>;
231
+ /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */
232
+ parse(body: string): TransactionWebhookEvent;
233
+ /** Verify signature, parse, and check for replays. Throws ValidationError on any failure. */
234
+ verifyAndParse(body: string, signature: string): Promise<TransactionWebhookEvent>;
235
+ /** Verify signature, parse, and dispatch to registered handlers. */
236
+ handle(body: string, signature: string): Promise<void>;
82
237
  }
83
238
 
84
- export { type JSONB, type Transaction, TransactionClient, type TransactionClientConfig, type TransactionCreateDTO, type TransactionCreateResponse, type TransactionMode, type TransactionStatus, type TransactionType, type WithholdingType };
239
+ export { DuplicateTransactionError, type HoldReason, PendingWithdrawal, TRANSACTION_SIGNATURE_HEADER, TaxHoldError, type TmTenantSettings, type Transaction, type TransactionCallbackEvent, type TransactionCallbackHandler, TransactionClient, type TransactionClientConfig, type TransactionCreateDTO, type TransactionCreateResponse, type TransactionHeldEvent, type TransactionMode, type TransactionStatus, type TransactionSubType, type TransactionType, type TransactionWebhookEvent, TransactionWebhookHandler, type TransactionWebhookHandlerConfig, type WithdrawalState, type WithholdingReason, type WithholdingType };
package/dist/index.js CHANGED
@@ -2,25 +2,237 @@
2
2
 
3
3
  var core = require('@vesant-sdk/core');
4
4
 
5
+ // src/client.ts
6
+ var DuplicateTransactionError = class _DuplicateTransactionError extends core.VesantError {
7
+ constructor(tx_id, message = `Duplicate transaction: ${tx_id} already exists`) {
8
+ super(message, "DUPLICATE_TRANSACTION", 409);
9
+ this.name = "DuplicateTransactionError";
10
+ this.tx_id = tx_id;
11
+ Object.setPrototypeOf(this, _DuplicateTransactionError.prototype);
12
+ }
13
+ };
14
+ var TaxHoldError = class _TaxHoldError extends Error {
15
+ constructor(event) {
16
+ super(`Transaction ${event.tx_id} held: ${event.hold_reason}`);
17
+ this.name = "TaxHoldError";
18
+ this.tx_id = event.tx_id;
19
+ this.hold_reason = event.hold_reason;
20
+ this.event = event;
21
+ Object.setPrototypeOf(this, _TaxHoldError.prototype);
22
+ }
23
+ };
24
+
25
+ // src/pending-withdrawal.ts
26
+ var PendingWithdrawal = class {
27
+ constructor(tx_id, transaction) {
28
+ this._state = "vesant_pending";
29
+ this._event = null;
30
+ this._error = void 0;
31
+ // Deferred promise — created lazily only when wait() is called.
32
+ this._waitPromise = null;
33
+ this.tx_id = tx_id;
34
+ this.initialTransaction = transaction;
35
+ }
36
+ status() {
37
+ return this._state;
38
+ }
39
+ callbackEvent() {
40
+ return this._event;
41
+ }
42
+ wait(signal) {
43
+ if (this._state !== "vesant_pending") {
44
+ if (this._state === "suspend" || this._state === "failed") {
45
+ return Promise.reject(this._error);
46
+ }
47
+ if (this._event !== null) {
48
+ return Promise.resolve(this._event);
49
+ }
50
+ }
51
+ if (!this._waitPromise) {
52
+ this._waitPromise = new Promise((resolve, reject) => {
53
+ this._resolveWait = resolve;
54
+ this._rejectWait = reject;
55
+ });
56
+ }
57
+ if (signal) {
58
+ if (signal.aborted) {
59
+ this._fail(new core.VesantError("Request aborted", "REQUEST_ABORTED"));
60
+ } else {
61
+ signal.addEventListener("abort", () => {
62
+ this._fail(new core.VesantError("Request aborted", "REQUEST_ABORTED"));
63
+ }, { once: true });
64
+ }
65
+ }
66
+ return this._waitPromise;
67
+ }
68
+ /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */
69
+ _settle(event) {
70
+ if (this._state !== "vesant_pending") return;
71
+ this._event = event;
72
+ if (event.status === "hold" || event.status === "suspend") {
73
+ this._state = "suspend";
74
+ this._error = new TaxHoldError(event);
75
+ this._rejectWait?.(this._error);
76
+ } else if (event.withholding_type === "none") {
77
+ this._state = "released";
78
+ this._resolveWait?.(event);
79
+ } else {
80
+ this._state = "withheld";
81
+ this._resolveWait?.(event);
82
+ }
83
+ }
84
+ /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */
85
+ _fail(err) {
86
+ if (this._state !== "vesant_pending") return;
87
+ this._state = "failed";
88
+ this._error = err;
89
+ this._rejectWait?.(err);
90
+ }
91
+ };
92
+
5
93
  // src/client.ts
6
94
  var TransactionClient = class extends core.BaseClient {
7
95
  constructor(config) {
8
96
  super(config);
9
97
  }
10
98
  async createTransaction(request, requestOptions) {
11
- const data = await this.requestWithRetry("/api/v1/tm/transactions", {
12
- method: "POST",
13
- body: JSON.stringify(request)
14
- }, void 0, void 0, requestOptions);
15
- return data;
99
+ try {
100
+ return await this.requestWithRetry("/api/v1/tm/transactions", {
101
+ method: "POST",
102
+ body: JSON.stringify(request)
103
+ }, void 0, void 0, requestOptions);
104
+ } catch (error) {
105
+ if (error instanceof core.VesantError && error.statusCode === 409) {
106
+ const transaction = await this.getTransaction(request.tx_id, requestOptions);
107
+ return { transaction, message: "Transaction already exists" };
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ async submitWithdrawal(request, requestOptions) {
113
+ const response = await this.createTransaction(
114
+ { ...request, transaction_type: "withdrawal" },
115
+ requestOptions
116
+ );
117
+ return new PendingWithdrawal(response.transaction.tx_id, response.transaction);
16
118
  }
17
119
  async getTransaction(transactionId, requestOptions) {
18
- await this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
120
+ return this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
19
121
  method: "GET"
20
122
  }, void 0, void 0, requestOptions);
21
123
  }
124
+ async getSettings() {
125
+ return this.requestWithRetry("/api/v1/tm/settings", {
126
+ method: "GET"
127
+ });
128
+ }
129
+ };
130
+ var TRANSACTION_SIGNATURE_HEADER = "x-webhook-signature";
131
+ var TransactionWebhookHandler = class {
132
+ constructor(config) {
133
+ this.handlers = {
134
+ "tm.transaction.callback": [],
135
+ "tm.transaction.held": []
136
+ };
137
+ this.seenEvents = /* @__PURE__ */ new Map();
138
+ this.pendingWithdrawals = /* @__PURE__ */ new Map();
139
+ this.callbackHandler = null;
140
+ this.secret = config.secret;
141
+ this.replayProtection = config.replayProtection ?? true;
142
+ this.replayWindow = config.replayWindow ?? 3e5;
143
+ }
144
+ on(eventType, handler) {
145
+ this.handlers[eventType].push(handler);
146
+ return this;
147
+ }
148
+ /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */
149
+ registerCallbackHandler(handler) {
150
+ this.callbackHandler = handler;
151
+ return this;
152
+ }
153
+ /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */
154
+ link(pending) {
155
+ this.pendingWithdrawals.set(pending.tx_id, pending);
156
+ return this;
157
+ }
158
+ /** Stop tracking a PendingWithdrawal by tx_id. */
159
+ unlink(tx_id) {
160
+ this.pendingWithdrawals.delete(tx_id);
161
+ return this;
162
+ }
163
+ /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */
164
+ async verify(rawBody, signature) {
165
+ return core.verifyWebhookSignature(rawBody, signature, this.secret);
166
+ }
167
+ /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */
168
+ parse(body) {
169
+ const event = JSON.parse(body);
170
+ if (!event.event_type || !event.tx_id) {
171
+ throw new core.ValidationError(
172
+ "Invalid TM webhook event: missing required fields (event_type, tx_id)",
173
+ ["event_type", "tx_id"]
174
+ );
175
+ }
176
+ return event;
177
+ }
178
+ /** Verify signature, parse, and check for replays. Throws ValidationError on any failure. */
179
+ async verifyAndParse(body, signature) {
180
+ const isValid = await core.verifyWebhookSignature(body, signature, this.secret);
181
+ if (!isValid) {
182
+ throw new core.ValidationError("Invalid webhook signature", ["signature"]);
183
+ }
184
+ const event = this.parse(body);
185
+ if (this.replayProtection) {
186
+ const key = `${event.event_type}:${event.tx_id}`;
187
+ if (this.seenEvents.has(key)) {
188
+ throw new core.ValidationError(
189
+ `Duplicate webhook event: ${event.event_type} for ${event.tx_id} has already been processed`,
190
+ ["tx_id"]
191
+ );
192
+ }
193
+ const now = Date.now();
194
+ this.seenEvents.set(key, now);
195
+ if (this.seenEvents.size > 1e3) {
196
+ for (const [k, seenAt] of this.seenEvents) {
197
+ if (now - seenAt > this.replayWindow) {
198
+ this.seenEvents.delete(k);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ return event;
204
+ }
205
+ /** Verify signature, parse, and dispatch to registered handlers. */
206
+ async handle(body, signature) {
207
+ const event = await this.verifyAndParse(body, signature);
208
+ if (event.event_type === "tm.transaction.callback") {
209
+ const pending = this.pendingWithdrawals.get(event.tx_id);
210
+ if (pending) {
211
+ pending._settle(event);
212
+ this.pendingWithdrawals.delete(event.tx_id);
213
+ }
214
+ if (this.callbackHandler) {
215
+ if (event.status === "hold" || event.status === "suspend") {
216
+ await this.callbackHandler.onHeld?.(event);
217
+ } else if (event.withholding_type === "none") {
218
+ await this.callbackHandler.onReleased?.(event);
219
+ } else {
220
+ await this.callbackHandler.onWithheld?.(event);
221
+ }
222
+ }
223
+ }
224
+ const handlers = this.handlers[event.event_type];
225
+ for (const handler of handlers) {
226
+ await handler(event);
227
+ }
228
+ }
22
229
  };
23
230
 
231
+ exports.DuplicateTransactionError = DuplicateTransactionError;
232
+ exports.PendingWithdrawal = PendingWithdrawal;
233
+ exports.TRANSACTION_SIGNATURE_HEADER = TRANSACTION_SIGNATURE_HEADER;
234
+ exports.TaxHoldError = TaxHoldError;
24
235
  exports.TransactionClient = TransactionClient;
236
+ exports.TransactionWebhookHandler = TransactionWebhookHandler;
25
237
  //# sourceMappingURL=index.js.map
26
238
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"names":["BaseClient"],"mappings":";;;;;AAQO,IAAM,iBAAA,GAAN,cAAgCA,eAAA,CAAW;AAAA,EAChD,YAAY,MAAA,EAA0B;AACpC,IAAA,KAAA,CAAM,MAAM,CAAA;AAAA,EACd;AAAA,EAEA,MAAM,iBAAA,CACJ,OAAA,EACA,cAAA,EACoC;AACpC,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,gBAAA,CAA4C,yBAAA,EAA2B;AAAA,MAC7F,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAO;AAAA,KAC9B,EAAG,MAAA,EAAW,MAAA,EAAW,cAAc,CAAA;AACvC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,cAAA,CAAe,aAAA,EAAuB,cAAA,EAAgD;AAC1F,IAAA,MAAM,IAAA,CAAK,gBAAA,CAAuB,CAAA,+BAAA,EAAkC,aAAa,CAAA,CAAA,EAAI;AAAA,MACnF,MAAA,EAAQ;AAAA,KACV,EAAG,MAAA,EAAW,MAAA,EAAW,cAAc,CAAA;AAAA,EACzC;AACF","file":"index.js","sourcesContent":["/**\n * TransactionClient — SDK for the Transaction Monitoring service\n */\n\nimport { BaseClient } from '@vesant-sdk/core';\nimport type { BaseClientConfig, RequestOptions } from '@vesant-sdk/core';\nimport type { TransactionCreateDTO, TransactionCreateResponse } from './types';\n\nexport class TransactionClient extends BaseClient {\n constructor(config: BaseClientConfig) {\n super(config);\n }\n\n async createTransaction(\n request: TransactionCreateDTO,\n requestOptions?: RequestOptions\n ): Promise<TransactionCreateResponse> {\n const data = await this.requestWithRetry<TransactionCreateResponse>('/api/v1/tm/transactions', {\n method: 'POST',\n body: JSON.stringify(request),\n }, undefined, undefined, requestOptions);\n return data;\n }\n\n async getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<void> {\n await this.requestWithRetry<void>(`/api/v1/tm/transactions/status/${transactionId}`, {\n method: 'GET',\n }, undefined, undefined, requestOptions);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/pending-withdrawal.ts","../src/client.ts","../src/webhook-handler.ts"],"names":["VesantError","BaseClient","verifyWebhookSignature","ValidationError"],"mappings":"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","file":"index.js","sourcesContent":["import { VesantError } from '@vesant-sdk/core';\nimport type { TransactionCallbackEvent, HoldReason } from './types';\n\nexport class DuplicateTransactionError extends VesantError {\n readonly tx_id: string;\n\n constructor(tx_id: string, message = `Duplicate transaction: ${tx_id} already exists`) {\n super(message, 'DUPLICATE_TRANSACTION', 409);\n this.name = 'DuplicateTransactionError';\n this.tx_id = tx_id;\n Object.setPrototypeOf(this, DuplicateTransactionError.prototype);\n }\n}\n\nexport class TaxHoldError extends Error {\n readonly tx_id: string;\n readonly hold_reason: HoldReason;\n readonly event: TransactionCallbackEvent;\n\n constructor(event: TransactionCallbackEvent) {\n super(`Transaction ${event.tx_id} held: ${event.hold_reason}`);\n this.name = 'TaxHoldError';\n this.tx_id = event.tx_id;\n this.hold_reason = event.hold_reason as HoldReason;\n this.event = event;\n Object.setPrototypeOf(this, TaxHoldError.prototype);\n }\n}\n","import { VesantError } from '@vesant-sdk/core';\nimport { TaxHoldError } from './errors';\nimport type { Transaction, TransactionCallbackEvent } from './types';\n\nexport type WithdrawalState = 'vesant_pending' | 'withheld' | 'suspend' | 'released' | 'failed';\n\nexport class PendingWithdrawal {\n private _state: WithdrawalState = 'vesant_pending';\n private _event: TransactionCallbackEvent | null = null;\n private _error: unknown = undefined;\n\n // Deferred promise — created lazily only when wait() is called.\n private _waitPromise: Promise<TransactionCallbackEvent> | null = null;\n private _resolveWait?: (event: TransactionCallbackEvent) => void;\n private _rejectWait?: (err: unknown) => void;\n\n readonly tx_id: string;\n readonly initialTransaction: Transaction;\n\n constructor(tx_id: string, transaction: Transaction) {\n this.tx_id = tx_id;\n this.initialTransaction = transaction;\n }\n\n status(): WithdrawalState {\n return this._state;\n }\n\n callbackEvent(): TransactionCallbackEvent | null {\n return this._event;\n }\n\n wait(signal?: AbortSignal): Promise<TransactionCallbackEvent> {\n if (this._state !== 'vesant_pending') {\n if (this._state === 'suspend' || this._state === 'failed') {\n return Promise.reject(this._error);\n }\n if (this._event !== null) {\n return Promise.resolve(this._event);\n }\n }\n\n if (!this._waitPromise) {\n this._waitPromise = new Promise<TransactionCallbackEvent>((resolve, reject) => {\n this._resolveWait = resolve;\n this._rejectWait = reject;\n });\n }\n\n if (signal) {\n if (signal.aborted) {\n this._fail(new VesantError('Request aborted', 'REQUEST_ABORTED'));\n } else {\n signal.addEventListener('abort', () => {\n this._fail(new VesantError('Request aborted', 'REQUEST_ABORTED'));\n }, { once: true });\n }\n }\n\n return this._waitPromise;\n }\n\n /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */\n _settle(event: TransactionCallbackEvent): void {\n if (this._state !== 'vesant_pending') return;\n this._event = event;\n if (event.status === 'hold' || event.status === 'suspend') {\n this._state = 'suspend';\n this._error = new TaxHoldError(event);\n this._rejectWait?.(this._error);\n } else if (event.withholding_type === 'none') {\n this._state = 'released';\n this._resolveWait?.(event);\n } else {\n this._state = 'withheld';\n this._resolveWait?.(event);\n }\n }\n\n /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */\n _fail(err: unknown): void {\n if (this._state !== 'vesant_pending') return;\n this._state = 'failed';\n this._error = err;\n this._rejectWait?.(err);\n }\n}\n","/**\n * TransactionClient — SDK for the Transaction Monitoring service\n */\nimport { BaseClient, VesantError } from '@vesant-sdk/core';\nimport type { BaseClientConfig, RequestOptions } from '@vesant-sdk/core';\nimport type { Transaction, TransactionCreateDTO, TransactionCreateResponse, TmTenantSettings } from './types';\nimport { PendingWithdrawal } from './pending-withdrawal';\n\nexport class TransactionClient extends BaseClient {\n constructor(config: BaseClientConfig) {\n super(config);\n }\n\n async createTransaction(\n request: TransactionCreateDTO,\n requestOptions?: RequestOptions\n ): Promise<TransactionCreateResponse> {\n try {\n return await this.requestWithRetry<TransactionCreateResponse>('/api/v1/tm/transactions', {\n method: 'POST',\n body: JSON.stringify(request),\n }, undefined, undefined, requestOptions);\n } catch (error) {\n if (error instanceof VesantError && error.statusCode === 409) {\n const transaction = await this.getTransaction(request.tx_id, requestOptions);\n return { transaction, message: 'Transaction already exists' };\n }\n throw error;\n }\n }\n\n async submitWithdrawal(\n request: TransactionCreateDTO,\n requestOptions?: RequestOptions\n ): Promise<PendingWithdrawal> {\n const response = await this.createTransaction(\n { ...request, transaction_type: 'withdrawal' },\n requestOptions\n );\n return new PendingWithdrawal(response.transaction.tx_id, response.transaction);\n }\n\n async getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<Transaction> {\n return this.requestWithRetry<Transaction>(`/api/v1/tm/transactions/status/${transactionId}`, {\n method: 'GET',\n }, undefined, undefined, requestOptions);\n }\n\n async getSettings(): Promise<TmTenantSettings> {\n return this.requestWithRetry<TmTenantSettings>('/api/v1/tm/settings', {\n method: 'GET',\n });\n }\n}\n","import { verifyWebhookSignature, ValidationError } from '@vesant-sdk/core';\nimport { PendingWithdrawal } from './pending-withdrawal';\nimport type {\n TransactionCallbackEvent,\n TransactionCallbackHandler,\n TransactionHeldEvent,\n TransactionWebhookEvent,\n} from './types';\n\ntype EventHandler<T> = (event: T) => void | Promise<void>;\n\ntype HandlerMap = {\n 'tm.transaction.callback': Array<EventHandler<TransactionCallbackEvent>>;\n 'tm.transaction.held': Array<EventHandler<TransactionHeldEvent>>;\n};\n\nexport const TRANSACTION_SIGNATURE_HEADER = 'x-webhook-signature';\n\nexport interface TransactionWebhookHandlerConfig {\n secret: string;\n /** Reject duplicate event_type+tx_id pairs within replayWindow. Default: true. */\n replayProtection?: boolean;\n /** How long (ms) to remember seen events for replay detection. Default: 300_000 (5 min). */\n replayWindow?: number;\n}\n\nexport class TransactionWebhookHandler {\n private readonly handlers: HandlerMap = {\n 'tm.transaction.callback': [],\n 'tm.transaction.held': [],\n };\n private readonly secret: string;\n private readonly replayProtection: boolean;\n private readonly replayWindow: number;\n private readonly seenEvents = new Map<string, number>();\n private readonly pendingWithdrawals = new Map<string, PendingWithdrawal>();\n private callbackHandler: TransactionCallbackHandler | null = null;\n\n constructor(config: TransactionWebhookHandlerConfig) {\n this.secret = config.secret;\n this.replayProtection = config.replayProtection ?? true;\n this.replayWindow = config.replayWindow ?? 300_000;\n }\n\n on(eventType: 'tm.transaction.callback', handler: EventHandler<TransactionCallbackEvent>): this;\n on(eventType: 'tm.transaction.held', handler: EventHandler<TransactionHeldEvent>): this;\n on(eventType: keyof HandlerMap, handler: EventHandler<TransactionCallbackEvent> | EventHandler<TransactionHeldEvent>): this {\n (this.handlers[eventType] as Array<typeof handler>).push(handler);\n return this;\n }\n\n /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */\n registerCallbackHandler(handler: TransactionCallbackHandler): this {\n this.callbackHandler = handler;\n return this;\n }\n\n /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */\n link(pending: PendingWithdrawal): this {\n this.pendingWithdrawals.set(pending.tx_id, pending);\n return this;\n }\n\n /** Stop tracking a PendingWithdrawal by tx_id. */\n unlink(tx_id: string): this {\n this.pendingWithdrawals.delete(tx_id);\n return this;\n }\n\n /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */\n async verify(rawBody: string, signature: string): Promise<boolean> {\n return verifyWebhookSignature(rawBody, signature, this.secret);\n }\n\n /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */\n parse(body: string): TransactionWebhookEvent {\n const event = JSON.parse(body) as TransactionWebhookEvent;\n if (!event.event_type || !event.tx_id) {\n throw new ValidationError(\n 'Invalid TM webhook event: missing required fields (event_type, tx_id)',\n ['event_type', 'tx_id']\n );\n }\n return event;\n }\n\n /** Verify signature, parse, and check for replays. Throws ValidationError on any failure. */\n async verifyAndParse(body: string, signature: string): Promise<TransactionWebhookEvent> {\n const isValid = await verifyWebhookSignature(body, signature, this.secret);\n if (!isValid) {\n throw new ValidationError('Invalid webhook signature', ['signature']);\n }\n\n const event = this.parse(body);\n\n if (this.replayProtection) {\n const key = `${event.event_type}:${event.tx_id}`;\n if (this.seenEvents.has(key)) {\n throw new ValidationError(\n `Duplicate webhook event: ${event.event_type} for ${event.tx_id} has already been processed`,\n ['tx_id']\n );\n }\n const now = Date.now();\n this.seenEvents.set(key, now);\n if (this.seenEvents.size > 1000) {\n for (const [k, seenAt] of this.seenEvents) {\n if (now - seenAt > this.replayWindow) {\n this.seenEvents.delete(k);\n }\n }\n }\n }\n\n return event;\n }\n\n /** Verify signature, parse, and dispatch to registered handlers. */\n async handle(body: string, signature: string): Promise<void> {\n const event = await this.verifyAndParse(body, signature);\n\n if (event.event_type === 'tm.transaction.callback') {\n const pending = this.pendingWithdrawals.get(event.tx_id);\n if (pending) {\n pending._settle(event);\n this.pendingWithdrawals.delete(event.tx_id);\n }\n\n if (this.callbackHandler) {\n if (event.status === 'hold' || event.status === 'suspend') {\n await this.callbackHandler.onHeld?.(event);\n } else if (event.withholding_type === 'none') {\n await this.callbackHandler.onReleased?.(event);\n } else {\n await this.callbackHandler.onWithheld?.(event);\n }\n }\n }\n\n const handlers = this.handlers[event.event_type] as Array<EventHandler<typeof event>>;\n for (const handler of handlers) {\n await handler(event);\n }\n }\n}\n"]}
package/dist/index.mjs CHANGED
@@ -1,4 +1,92 @@
1
- import { BaseClient } from '@vesant-sdk/core';
1
+ import { VesantError, BaseClient, verifyWebhookSignature, ValidationError } from '@vesant-sdk/core';
2
+
3
+ // src/client.ts
4
+ var DuplicateTransactionError = class _DuplicateTransactionError extends VesantError {
5
+ constructor(tx_id, message = `Duplicate transaction: ${tx_id} already exists`) {
6
+ super(message, "DUPLICATE_TRANSACTION", 409);
7
+ this.name = "DuplicateTransactionError";
8
+ this.tx_id = tx_id;
9
+ Object.setPrototypeOf(this, _DuplicateTransactionError.prototype);
10
+ }
11
+ };
12
+ var TaxHoldError = class _TaxHoldError extends Error {
13
+ constructor(event) {
14
+ super(`Transaction ${event.tx_id} held: ${event.hold_reason}`);
15
+ this.name = "TaxHoldError";
16
+ this.tx_id = event.tx_id;
17
+ this.hold_reason = event.hold_reason;
18
+ this.event = event;
19
+ Object.setPrototypeOf(this, _TaxHoldError.prototype);
20
+ }
21
+ };
22
+
23
+ // src/pending-withdrawal.ts
24
+ var PendingWithdrawal = class {
25
+ constructor(tx_id, transaction) {
26
+ this._state = "vesant_pending";
27
+ this._event = null;
28
+ this._error = void 0;
29
+ // Deferred promise — created lazily only when wait() is called.
30
+ this._waitPromise = null;
31
+ this.tx_id = tx_id;
32
+ this.initialTransaction = transaction;
33
+ }
34
+ status() {
35
+ return this._state;
36
+ }
37
+ callbackEvent() {
38
+ return this._event;
39
+ }
40
+ wait(signal) {
41
+ if (this._state !== "vesant_pending") {
42
+ if (this._state === "suspend" || this._state === "failed") {
43
+ return Promise.reject(this._error);
44
+ }
45
+ if (this._event !== null) {
46
+ return Promise.resolve(this._event);
47
+ }
48
+ }
49
+ if (!this._waitPromise) {
50
+ this._waitPromise = new Promise((resolve, reject) => {
51
+ this._resolveWait = resolve;
52
+ this._rejectWait = reject;
53
+ });
54
+ }
55
+ if (signal) {
56
+ if (signal.aborted) {
57
+ this._fail(new VesantError("Request aborted", "REQUEST_ABORTED"));
58
+ } else {
59
+ signal.addEventListener("abort", () => {
60
+ this._fail(new VesantError("Request aborted", "REQUEST_ABORTED"));
61
+ }, { once: true });
62
+ }
63
+ }
64
+ return this._waitPromise;
65
+ }
66
+ /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */
67
+ _settle(event) {
68
+ if (this._state !== "vesant_pending") return;
69
+ this._event = event;
70
+ if (event.status === "hold" || event.status === "suspend") {
71
+ this._state = "suspend";
72
+ this._error = new TaxHoldError(event);
73
+ this._rejectWait?.(this._error);
74
+ } else if (event.withholding_type === "none") {
75
+ this._state = "released";
76
+ this._resolveWait?.(event);
77
+ } else {
78
+ this._state = "withheld";
79
+ this._resolveWait?.(event);
80
+ }
81
+ }
82
+ /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */
83
+ _fail(err) {
84
+ if (this._state !== "vesant_pending") return;
85
+ this._state = "failed";
86
+ this._error = err;
87
+ this._rejectWait?.(err);
88
+ }
89
+ };
2
90
 
3
91
  // src/client.ts
4
92
  var TransactionClient = class extends BaseClient {
@@ -6,19 +94,138 @@ var TransactionClient = class extends BaseClient {
6
94
  super(config);
7
95
  }
8
96
  async createTransaction(request, requestOptions) {
9
- const data = await this.requestWithRetry("/api/v1/tm/transactions", {
10
- method: "POST",
11
- body: JSON.stringify(request)
12
- }, void 0, void 0, requestOptions);
13
- return data;
97
+ try {
98
+ return await this.requestWithRetry("/api/v1/tm/transactions", {
99
+ method: "POST",
100
+ body: JSON.stringify(request)
101
+ }, void 0, void 0, requestOptions);
102
+ } catch (error) {
103
+ if (error instanceof VesantError && error.statusCode === 409) {
104
+ const transaction = await this.getTransaction(request.tx_id, requestOptions);
105
+ return { transaction, message: "Transaction already exists" };
106
+ }
107
+ throw error;
108
+ }
109
+ }
110
+ async submitWithdrawal(request, requestOptions) {
111
+ const response = await this.createTransaction(
112
+ { ...request, transaction_type: "withdrawal" },
113
+ requestOptions
114
+ );
115
+ return new PendingWithdrawal(response.transaction.tx_id, response.transaction);
14
116
  }
15
117
  async getTransaction(transactionId, requestOptions) {
16
- await this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
118
+ return this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
17
119
  method: "GET"
18
120
  }, void 0, void 0, requestOptions);
19
121
  }
122
+ async getSettings() {
123
+ return this.requestWithRetry("/api/v1/tm/settings", {
124
+ method: "GET"
125
+ });
126
+ }
127
+ };
128
+ var TRANSACTION_SIGNATURE_HEADER = "x-webhook-signature";
129
+ var TransactionWebhookHandler = class {
130
+ constructor(config) {
131
+ this.handlers = {
132
+ "tm.transaction.callback": [],
133
+ "tm.transaction.held": []
134
+ };
135
+ this.seenEvents = /* @__PURE__ */ new Map();
136
+ this.pendingWithdrawals = /* @__PURE__ */ new Map();
137
+ this.callbackHandler = null;
138
+ this.secret = config.secret;
139
+ this.replayProtection = config.replayProtection ?? true;
140
+ this.replayWindow = config.replayWindow ?? 3e5;
141
+ }
142
+ on(eventType, handler) {
143
+ this.handlers[eventType].push(handler);
144
+ return this;
145
+ }
146
+ /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */
147
+ registerCallbackHandler(handler) {
148
+ this.callbackHandler = handler;
149
+ return this;
150
+ }
151
+ /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */
152
+ link(pending) {
153
+ this.pendingWithdrawals.set(pending.tx_id, pending);
154
+ return this;
155
+ }
156
+ /** Stop tracking a PendingWithdrawal by tx_id. */
157
+ unlink(tx_id) {
158
+ this.pendingWithdrawals.delete(tx_id);
159
+ return this;
160
+ }
161
+ /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */
162
+ async verify(rawBody, signature) {
163
+ return verifyWebhookSignature(rawBody, signature, this.secret);
164
+ }
165
+ /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */
166
+ parse(body) {
167
+ const event = JSON.parse(body);
168
+ if (!event.event_type || !event.tx_id) {
169
+ throw new ValidationError(
170
+ "Invalid TM webhook event: missing required fields (event_type, tx_id)",
171
+ ["event_type", "tx_id"]
172
+ );
173
+ }
174
+ return event;
175
+ }
176
+ /** Verify signature, parse, and check for replays. Throws ValidationError on any failure. */
177
+ async verifyAndParse(body, signature) {
178
+ const isValid = await verifyWebhookSignature(body, signature, this.secret);
179
+ if (!isValid) {
180
+ throw new ValidationError("Invalid webhook signature", ["signature"]);
181
+ }
182
+ const event = this.parse(body);
183
+ if (this.replayProtection) {
184
+ const key = `${event.event_type}:${event.tx_id}`;
185
+ if (this.seenEvents.has(key)) {
186
+ throw new ValidationError(
187
+ `Duplicate webhook event: ${event.event_type} for ${event.tx_id} has already been processed`,
188
+ ["tx_id"]
189
+ );
190
+ }
191
+ const now = Date.now();
192
+ this.seenEvents.set(key, now);
193
+ if (this.seenEvents.size > 1e3) {
194
+ for (const [k, seenAt] of this.seenEvents) {
195
+ if (now - seenAt > this.replayWindow) {
196
+ this.seenEvents.delete(k);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return event;
202
+ }
203
+ /** Verify signature, parse, and dispatch to registered handlers. */
204
+ async handle(body, signature) {
205
+ const event = await this.verifyAndParse(body, signature);
206
+ if (event.event_type === "tm.transaction.callback") {
207
+ const pending = this.pendingWithdrawals.get(event.tx_id);
208
+ if (pending) {
209
+ pending._settle(event);
210
+ this.pendingWithdrawals.delete(event.tx_id);
211
+ }
212
+ if (this.callbackHandler) {
213
+ if (event.status === "hold" || event.status === "suspend") {
214
+ await this.callbackHandler.onHeld?.(event);
215
+ } else if (event.withholding_type === "none") {
216
+ await this.callbackHandler.onReleased?.(event);
217
+ } else {
218
+ await this.callbackHandler.onWithheld?.(event);
219
+ }
220
+ }
221
+ }
222
+ const handlers = this.handlers[event.event_type];
223
+ for (const handler of handlers) {
224
+ await handler(event);
225
+ }
226
+ }
20
227
  };
21
228
 
22
- export { TransactionClient };
229
+ export { DuplicateTransactionError, PendingWithdrawal, TRANSACTION_SIGNATURE_HEADER, TaxHoldError, TransactionClient, TransactionWebhookHandler };
23
230
  //# sourceMappingURL=index.mjs.map
24
231
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"names":[],"mappings":";;;AAQO,IAAM,iBAAA,GAAN,cAAgC,UAAA,CAAW;AAAA,EAChD,YAAY,MAAA,EAA0B;AACpC,IAAA,KAAA,CAAM,MAAM,CAAA;AAAA,EACd;AAAA,EAEA,MAAM,iBAAA,CACJ,OAAA,EACA,cAAA,EACoC;AACpC,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,gBAAA,CAA4C,yBAAA,EAA2B;AAAA,MAC7F,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAO;AAAA,KAC9B,EAAG,MAAA,EAAW,MAAA,EAAW,cAAc,CAAA;AACvC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,cAAA,CAAe,aAAA,EAAuB,cAAA,EAAgD;AAC1F,IAAA,MAAM,IAAA,CAAK,gBAAA,CAAuB,CAAA,+BAAA,EAAkC,aAAa,CAAA,CAAA,EAAI;AAAA,MACnF,MAAA,EAAQ;AAAA,KACV,EAAG,MAAA,EAAW,MAAA,EAAW,cAAc,CAAA;AAAA,EACzC;AACF","file":"index.mjs","sourcesContent":["/**\n * TransactionClient — SDK for the Transaction Monitoring service\n */\n\nimport { BaseClient } from '@vesant-sdk/core';\nimport type { BaseClientConfig, RequestOptions } from '@vesant-sdk/core';\nimport type { TransactionCreateDTO, TransactionCreateResponse } from './types';\n\nexport class TransactionClient extends BaseClient {\n constructor(config: BaseClientConfig) {\n super(config);\n }\n\n async createTransaction(\n request: TransactionCreateDTO,\n requestOptions?: RequestOptions\n ): Promise<TransactionCreateResponse> {\n const data = await this.requestWithRetry<TransactionCreateResponse>('/api/v1/tm/transactions', {\n method: 'POST',\n body: JSON.stringify(request),\n }, undefined, undefined, requestOptions);\n return data;\n }\n\n async getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<void> {\n await this.requestWithRetry<void>(`/api/v1/tm/transactions/status/${transactionId}`, {\n method: 'GET',\n }, undefined, undefined, requestOptions);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/pending-withdrawal.ts","../src/client.ts","../src/webhook-handler.ts"],"names":["VesantError"],"mappings":"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","file":"index.mjs","sourcesContent":["import { VesantError } from '@vesant-sdk/core';\nimport type { TransactionCallbackEvent, HoldReason } from './types';\n\nexport class DuplicateTransactionError extends VesantError {\n readonly tx_id: string;\n\n constructor(tx_id: string, message = `Duplicate transaction: ${tx_id} already exists`) {\n super(message, 'DUPLICATE_TRANSACTION', 409);\n this.name = 'DuplicateTransactionError';\n this.tx_id = tx_id;\n Object.setPrototypeOf(this, DuplicateTransactionError.prototype);\n }\n}\n\nexport class TaxHoldError extends Error {\n readonly tx_id: string;\n readonly hold_reason: HoldReason;\n readonly event: TransactionCallbackEvent;\n\n constructor(event: TransactionCallbackEvent) {\n super(`Transaction ${event.tx_id} held: ${event.hold_reason}`);\n this.name = 'TaxHoldError';\n this.tx_id = event.tx_id;\n this.hold_reason = event.hold_reason as HoldReason;\n this.event = event;\n Object.setPrototypeOf(this, TaxHoldError.prototype);\n }\n}\n","import { VesantError } from '@vesant-sdk/core';\nimport { TaxHoldError } from './errors';\nimport type { Transaction, TransactionCallbackEvent } from './types';\n\nexport type WithdrawalState = 'vesant_pending' | 'withheld' | 'suspend' | 'released' | 'failed';\n\nexport class PendingWithdrawal {\n private _state: WithdrawalState = 'vesant_pending';\n private _event: TransactionCallbackEvent | null = null;\n private _error: unknown = undefined;\n\n // Deferred promise — created lazily only when wait() is called.\n private _waitPromise: Promise<TransactionCallbackEvent> | null = null;\n private _resolveWait?: (event: TransactionCallbackEvent) => void;\n private _rejectWait?: (err: unknown) => void;\n\n readonly tx_id: string;\n readonly initialTransaction: Transaction;\n\n constructor(tx_id: string, transaction: Transaction) {\n this.tx_id = tx_id;\n this.initialTransaction = transaction;\n }\n\n status(): WithdrawalState {\n return this._state;\n }\n\n callbackEvent(): TransactionCallbackEvent | null {\n return this._event;\n }\n\n wait(signal?: AbortSignal): Promise<TransactionCallbackEvent> {\n if (this._state !== 'vesant_pending') {\n if (this._state === 'suspend' || this._state === 'failed') {\n return Promise.reject(this._error);\n }\n if (this._event !== null) {\n return Promise.resolve(this._event);\n }\n }\n\n if (!this._waitPromise) {\n this._waitPromise = new Promise<TransactionCallbackEvent>((resolve, reject) => {\n this._resolveWait = resolve;\n this._rejectWait = reject;\n });\n }\n\n if (signal) {\n if (signal.aborted) {\n this._fail(new VesantError('Request aborted', 'REQUEST_ABORTED'));\n } else {\n signal.addEventListener('abort', () => {\n this._fail(new VesantError('Request aborted', 'REQUEST_ABORTED'));\n }, { once: true });\n }\n }\n\n return this._waitPromise;\n }\n\n /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */\n _settle(event: TransactionCallbackEvent): void {\n if (this._state !== 'vesant_pending') return;\n this._event = event;\n if (event.status === 'hold' || event.status === 'suspend') {\n this._state = 'suspend';\n this._error = new TaxHoldError(event);\n this._rejectWait?.(this._error);\n } else if (event.withholding_type === 'none') {\n this._state = 'released';\n this._resolveWait?.(event);\n } else {\n this._state = 'withheld';\n this._resolveWait?.(event);\n }\n }\n\n /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */\n _fail(err: unknown): void {\n if (this._state !== 'vesant_pending') return;\n this._state = 'failed';\n this._error = err;\n this._rejectWait?.(err);\n }\n}\n","/**\n * TransactionClient — SDK for the Transaction Monitoring service\n */\nimport { BaseClient, VesantError } from '@vesant-sdk/core';\nimport type { BaseClientConfig, RequestOptions } from '@vesant-sdk/core';\nimport type { Transaction, TransactionCreateDTO, TransactionCreateResponse, TmTenantSettings } from './types';\nimport { PendingWithdrawal } from './pending-withdrawal';\n\nexport class TransactionClient extends BaseClient {\n constructor(config: BaseClientConfig) {\n super(config);\n }\n\n async createTransaction(\n request: TransactionCreateDTO,\n requestOptions?: RequestOptions\n ): Promise<TransactionCreateResponse> {\n try {\n return await this.requestWithRetry<TransactionCreateResponse>('/api/v1/tm/transactions', {\n method: 'POST',\n body: JSON.stringify(request),\n }, undefined, undefined, requestOptions);\n } catch (error) {\n if (error instanceof VesantError && error.statusCode === 409) {\n const transaction = await this.getTransaction(request.tx_id, requestOptions);\n return { transaction, message: 'Transaction already exists' };\n }\n throw error;\n }\n }\n\n async submitWithdrawal(\n request: TransactionCreateDTO,\n requestOptions?: RequestOptions\n ): Promise<PendingWithdrawal> {\n const response = await this.createTransaction(\n { ...request, transaction_type: 'withdrawal' },\n requestOptions\n );\n return new PendingWithdrawal(response.transaction.tx_id, response.transaction);\n }\n\n async getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<Transaction> {\n return this.requestWithRetry<Transaction>(`/api/v1/tm/transactions/status/${transactionId}`, {\n method: 'GET',\n }, undefined, undefined, requestOptions);\n }\n\n async getSettings(): Promise<TmTenantSettings> {\n return this.requestWithRetry<TmTenantSettings>('/api/v1/tm/settings', {\n method: 'GET',\n });\n }\n}\n","import { verifyWebhookSignature, ValidationError } from '@vesant-sdk/core';\nimport { PendingWithdrawal } from './pending-withdrawal';\nimport type {\n TransactionCallbackEvent,\n TransactionCallbackHandler,\n TransactionHeldEvent,\n TransactionWebhookEvent,\n} from './types';\n\ntype EventHandler<T> = (event: T) => void | Promise<void>;\n\ntype HandlerMap = {\n 'tm.transaction.callback': Array<EventHandler<TransactionCallbackEvent>>;\n 'tm.transaction.held': Array<EventHandler<TransactionHeldEvent>>;\n};\n\nexport const TRANSACTION_SIGNATURE_HEADER = 'x-webhook-signature';\n\nexport interface TransactionWebhookHandlerConfig {\n secret: string;\n /** Reject duplicate event_type+tx_id pairs within replayWindow. Default: true. */\n replayProtection?: boolean;\n /** How long (ms) to remember seen events for replay detection. Default: 300_000 (5 min). */\n replayWindow?: number;\n}\n\nexport class TransactionWebhookHandler {\n private readonly handlers: HandlerMap = {\n 'tm.transaction.callback': [],\n 'tm.transaction.held': [],\n };\n private readonly secret: string;\n private readonly replayProtection: boolean;\n private readonly replayWindow: number;\n private readonly seenEvents = new Map<string, number>();\n private readonly pendingWithdrawals = new Map<string, PendingWithdrawal>();\n private callbackHandler: TransactionCallbackHandler | null = null;\n\n constructor(config: TransactionWebhookHandlerConfig) {\n this.secret = config.secret;\n this.replayProtection = config.replayProtection ?? true;\n this.replayWindow = config.replayWindow ?? 300_000;\n }\n\n on(eventType: 'tm.transaction.callback', handler: EventHandler<TransactionCallbackEvent>): this;\n on(eventType: 'tm.transaction.held', handler: EventHandler<TransactionHeldEvent>): this;\n on(eventType: keyof HandlerMap, handler: EventHandler<TransactionCallbackEvent> | EventHandler<TransactionHeldEvent>): this {\n (this.handlers[eventType] as Array<typeof handler>).push(handler);\n return this;\n }\n\n /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */\n registerCallbackHandler(handler: TransactionCallbackHandler): this {\n this.callbackHandler = handler;\n return this;\n }\n\n /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */\n link(pending: PendingWithdrawal): this {\n this.pendingWithdrawals.set(pending.tx_id, pending);\n return this;\n }\n\n /** Stop tracking a PendingWithdrawal by tx_id. */\n unlink(tx_id: string): this {\n this.pendingWithdrawals.delete(tx_id);\n return this;\n }\n\n /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */\n async verify(rawBody: string, signature: string): Promise<boolean> {\n return verifyWebhookSignature(rawBody, signature, this.secret);\n }\n\n /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */\n parse(body: string): TransactionWebhookEvent {\n const event = JSON.parse(body) as TransactionWebhookEvent;\n if (!event.event_type || !event.tx_id) {\n throw new ValidationError(\n 'Invalid TM webhook event: missing required fields (event_type, tx_id)',\n ['event_type', 'tx_id']\n );\n }\n return event;\n }\n\n /** Verify signature, parse, and check for replays. Throws ValidationError on any failure. */\n async verifyAndParse(body: string, signature: string): Promise<TransactionWebhookEvent> {\n const isValid = await verifyWebhookSignature(body, signature, this.secret);\n if (!isValid) {\n throw new ValidationError('Invalid webhook signature', ['signature']);\n }\n\n const event = this.parse(body);\n\n if (this.replayProtection) {\n const key = `${event.event_type}:${event.tx_id}`;\n if (this.seenEvents.has(key)) {\n throw new ValidationError(\n `Duplicate webhook event: ${event.event_type} for ${event.tx_id} has already been processed`,\n ['tx_id']\n );\n }\n const now = Date.now();\n this.seenEvents.set(key, now);\n if (this.seenEvents.size > 1000) {\n for (const [k, seenAt] of this.seenEvents) {\n if (now - seenAt > this.replayWindow) {\n this.seenEvents.delete(k);\n }\n }\n }\n }\n\n return event;\n }\n\n /** Verify signature, parse, and dispatch to registered handlers. */\n async handle(body: string, signature: string): Promise<void> {\n const event = await this.verifyAndParse(body, signature);\n\n if (event.event_type === 'tm.transaction.callback') {\n const pending = this.pendingWithdrawals.get(event.tx_id);\n if (pending) {\n pending._settle(event);\n this.pendingWithdrawals.delete(event.tx_id);\n }\n\n if (this.callbackHandler) {\n if (event.status === 'hold' || event.status === 'suspend') {\n await this.callbackHandler.onHeld?.(event);\n } else if (event.withholding_type === 'none') {\n await this.callbackHandler.onReleased?.(event);\n } else {\n await this.callbackHandler.onWithheld?.(event);\n }\n }\n }\n\n const handlers = this.handlers[event.event_type] as Array<EventHandler<typeof event>>;\n for (const handler of handlers) {\n await handler(event);\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesant-sdk/transaction",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-dev.059da4f",
4
4
  "description": "Transaction monitoring client for the Vesant Compliance Platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -12,7 +12,9 @@
12
12
  "require": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": ["dist"],
15
+ "files": [
16
+ "dist"
17
+ ],
16
18
  "scripts": {
17
19
  "build": "tsup",
18
20
  "dev": "tsup --watch",
@@ -21,7 +23,12 @@
21
23
  "type-check": "tsc --noEmit",
22
24
  "lint": "eslint src --ext .ts"
23
25
  },
24
- "keywords": ["vesant", "compliance", "transaction", "transaction-monitoring"],
26
+ "keywords": [
27
+ "vesant",
28
+ "compliance",
29
+ "transaction",
30
+ "transaction-monitoring"
31
+ ],
25
32
  "author": "Vesant Compliance",
26
33
  "license": "MIT",
27
34
  "dependencies": {