@vesant-sdk/transaction 0.1.1-next.f2f4e87 → 0.1.2-dev.8d808aa

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.ts CHANGED
@@ -1,45 +1,47 @@
1
- import { Timestamp, BaseClient, BaseClientConfig, RequestOptions } from '@vesant-sdk/core';
1
+ import { BaseClient, RequestOptions, BaseClientConfig, VesantError, WebhookDedupStore } 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" | "bet" | "payout";
8
- type TransactionMode = "fiat" | "crypto" | "bank_transfer" | "card" | "wallet" | "ach" | "wire_transfer" | "swift";
9
- type TransactionStatus = "pending" | "processing" | "completed" | "failed" | "cancelled" | "reversed" | "suspend" | "hold";
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
- currency: string;
19
- /**
20
- * Required for non-USD currencies. The backend only auto-converts when currency is "USD".
21
- * If omitted for crypto or foreign-currency withdrawals, withheld_usd and released_usd
22
- * will be 0 and the gateway will receive $0.
23
- */
18
+ /** Defaults server-side to `amount` when omitted and `currency` is USD. */
24
19
  amount_usd?: number;
20
+ currency: string;
25
21
  status?: TransactionStatus;
26
22
  source_account?: string;
27
23
  destination_account?: string;
28
24
  country?: string;
29
25
  ip_address?: string;
30
- metadata?: Record<string, any>;
31
- beneficiary_comment?: string;
32
- transaction_date: Timestamp;
26
+ /** Spelling matches the backend's JSON tag — `beneficiary_comment` is ignored by the API. */
27
+ benificiary_comment?: string;
28
+ metadata?: Record<string, unknown>;
29
+ transaction_date: string;
30
+ /** Device fingerprint ID from the Sift beacon. */
31
+ device_id?: string;
32
+ /** Session ID for session-level fraud signals. */
33
+ session_id?: string;
34
+ /** Raw User-Agent string forwarded to Sift for device classification. Falls back to the request's own header. */
35
+ user_agent?: string;
33
36
  }
34
37
  type WithholdingType = "none" | "backup_us" | "backup_non_us" | "treaty";
35
38
  /** Reason backup withholding or treaty rate was applied. */
36
39
  type WithholdingReason = "tin_not_verified" | "customer_tax_form_not_verified" | "b_notice_backup_withholding";
37
40
  /**
38
- * Reason a withdrawal was blocked (status = "suspend" or "hold").
41
+ * Reason a withdrawal was suspended (status = "suspend").
39
42
  * Reflects TIN/W-9 deficiencies that triggered the tenant's configured transaction action.
40
43
  */
41
44
  type HoldReason = "no_tax_profile_found" | "tin_rejected" | "tin_expired" | "tin_not_provided" | "tin_not_verified" | "form_not_certified";
42
- type JSONB = Record<string, any>;
43
45
  type Transaction = {
44
46
  id: string;
45
47
  tx_id: string;
@@ -48,26 +50,21 @@ type Transaction = {
48
50
  customer_id: string;
49
51
  transaction_type: TransactionType;
50
52
  transaction_mode: TransactionMode;
53
+ sub_type?: TransactionSubType;
51
54
  amount: string;
52
55
  withheld_amount: string;
53
56
  released_amount: string;
54
57
  withholding_rate: number;
55
- withholding_type?: WithholdingType;
56
- /** Set when withholding applies; null/absent when withholding_type is "none". */
57
- withholding_reason?: WithholdingReason;
58
- /** Set when status is "suspend" or "hold" due to TIN/W-9 deficiency. */
59
- hold_reason?: HoldReason;
60
- /** Gross transaction amount in USD. */
61
- amount_usd?: number;
62
- /** Withheld amount in USD. */
63
- withheld_usd?: number;
64
- /** Released (net) amount in USD. */
65
- released_usd?: number;
58
+ withholding_type: string;
59
+ withholding_reason?: string;
60
+ hold_reason?: string;
61
+ amount_usd: number;
62
+ withheld_usd: number;
63
+ released_usd: number;
66
64
  /**
67
65
  * True when this is the customer's first withdrawal.
68
- * Correct on the POST /transactions response and on tm.transaction.callback (both read
69
- * from the in-memory struct before any DB re-fetch). Not persisted — getTransaction()
70
- * always returns false regardless of actual history.
66
+ * Correct on the POST /transactions response and on tm.transaction.callback.
67
+ * Not persisted getTransaction() always returns false regardless of actual history.
71
68
  */
72
69
  is_first_withdrawal?: boolean;
73
70
  tax_year: number;
@@ -78,8 +75,8 @@ type Transaction = {
78
75
  country?: string;
79
76
  ip_address?: string;
80
77
  risk_score: number;
81
- metadata?: JSONB;
82
- beneficiary_comment?: string;
78
+ metadata?: Record<string, unknown>;
79
+ benificiary_comment?: string;
83
80
  transaction_date: string;
84
81
  created_at: string;
85
82
  updated_at: string;
@@ -96,45 +93,45 @@ interface TransactionClientConfig {
96
93
  environment?: 'production' | 'sandbox';
97
94
  }
98
95
  /**
99
- * Response from POST /api/v1/tm/transactions.
96
+ * Response from POST /api/v1/transactions.
100
97
  *
101
- * Status → outcome mapping:
102
- * "completed" — Vesant accepted; trigger the payment gateway (clean TIN,
103
- * backup withholding applied, or treaty rate applied)
104
- * "hold" — blocked, hold_reason set, no funds moved
105
- * "suspend" — blocked, hold_reason set, no funds moved
98
+ * Status → outcome mapping (withdrawal transactions only):
99
+ * "pending" — Vesant accepted; trigger the payment gateway (full release,
100
+ * backup withholding applied, or treaty rate applied)
101
+ * "suspend" — blocked, hold_reason set, no funds moved
106
102
  */
107
103
  type TransactionCreateResponse = {
108
104
  transaction: Transaction;
109
105
  message: string;
110
106
  };
111
107
  /**
112
- * Fired after every transaction is created.
113
- * Delivers the final transaction state including withholding outcome so clients
114
- * can mirror it without a separate polling call.
108
+ * Fired after every transaction is created and after a suspended transaction is released.
109
+ * Delivers the final compliance-evaluated state including withholding outcome.
115
110
  */
116
111
  interface TransactionCallbackEvent {
117
112
  event_type: "tm.transaction.callback";
113
+ id: string;
114
+ tenant_id: string;
115
+ entity_id: string;
116
+ user_id: string;
118
117
  tx_id: string;
119
118
  reference: string;
120
119
  customer_id: string;
121
- transaction_type: TransactionType;
122
- transaction_mode: TransactionMode;
123
- status: TransactionStatus;
120
+ transaction_type: string;
121
+ transaction_mode: string;
122
+ status: string;
124
123
  amount: string;
125
124
  currency: string;
126
125
  withheld_amount: string;
127
126
  released_amount: string;
128
127
  withholding_rate: number;
129
- withholding_type: WithholdingType;
130
- /** Null when withholding_type is "none". */
131
- withholding_reason: WithholdingReason | null;
128
+ withholding_type: string;
129
+ withholding_reason?: string | null;
130
+ hold_reason?: string | null;
132
131
  amount_usd: number;
133
132
  withheld_usd: number;
134
133
  released_usd: number;
135
134
  is_first_withdrawal: boolean;
136
- /** Set when status is "suspend" or "hold"; null otherwise. */
137
- hold_reason: HoldReason | null;
138
135
  transaction_date: string;
139
136
  }
140
137
  /**
@@ -154,34 +151,173 @@ interface TransactionHeldEvent {
154
151
  }
155
152
  /** Discriminated union of all TM service webhook event payloads. */
156
153
  type TransactionWebhookEvent = TransactionCallbackEvent | TransactionHeldEvent;
154
+ interface TmTenantSettings {
155
+ tenant_id: string;
156
+ transaction_monitoring_enabled: boolean;
157
+ tax_monitoring_enabled: boolean;
158
+ }
159
+ /**
160
+ * Semantic handler interface for the three withholding outcomes of a tm.transaction.callback.
161
+ * Register with TransactionWebhookHandler.registerCallbackHandler().
162
+ */
163
+ interface TransactionCallbackHandler {
164
+ onWithheld?(event: TransactionCallbackEvent): void | Promise<void>;
165
+ onHeld?(event: TransactionCallbackEvent): void | Promise<void>;
166
+ onReleased?(event: TransactionCallbackEvent): void | Promise<void>;
167
+ }
168
+
169
+ type WithdrawalState = 'vesant_pending' | 'withheld' | 'suspend' | 'released' | 'failed';
170
+ declare class PendingWithdrawal {
171
+ private _state;
172
+ private _event;
173
+ private _error;
174
+ private _waitPromise;
175
+ private _resolveWait?;
176
+ private _rejectWait?;
177
+ readonly tx_id: string;
178
+ readonly initialTransaction: Transaction;
179
+ constructor(tx_id: string, transaction: Transaction);
180
+ status(): WithdrawalState;
181
+ callbackEvent(): TransactionCallbackEvent | null;
182
+ wait(signal?: AbortSignal): Promise<TransactionCallbackEvent>;
183
+ /** Called by TransactionWebhookHandler when the tm.transaction.callback arrives for this tx_id. */
184
+ _settle(event: TransactionCallbackEvent): void;
185
+ /** Called to fail the pending withdrawal with a generic error (e.g. abort, network loss). */
186
+ _fail(err: unknown): void;
187
+ }
188
+
189
+ type CustomerType = 'US_PERSON' | 'NON_US_PERSON' | 'NON_US_ENTITY';
190
+ type CTPFormStatus = 'requested' | 'signed' | 'manual' | 'bounced' | 'uploaded' | 'expired' | 're_requested';
191
+ type CTPTINStatus = 'not_required' | 'pending' | 'submitted' | 'verified' | 'rejected' | 'error' | 'expired';
192
+ interface CustomerTaxProfileRecord {
193
+ id: string;
194
+ tenant_id: string;
195
+ customer_id: string;
196
+ customer_type: CustomerType;
197
+ form_type: string;
198
+ form_status: CTPFormStatus;
199
+ tin_status: CTPTINStatus;
200
+ tin_value?: string;
201
+ recipient_name?: string;
202
+ business_name?: string;
203
+ account_number?: string;
204
+ address?: string;
205
+ address_city?: string;
206
+ address_state?: string;
207
+ address_zip?: string;
208
+ address_country?: string;
209
+ customer_name?: string;
210
+ avalara_form_id?: string;
211
+ avalara_company_id?: string;
212
+ signed_at?: string;
213
+ verified_at?: string;
214
+ expiry_date?: string;
215
+ backup_withholding: boolean;
216
+ b_notice_received: boolean;
217
+ e_delivery_consent: boolean;
218
+ withholding_enabled_by?: string;
219
+ withholding_enabled_at?: string;
220
+ tin_due_date?: string;
221
+ last_tin_check?: string;
222
+ request_count: number;
223
+ reminders_sent: number;
224
+ last_reminder_at?: string;
225
+ created_at: string;
226
+ updated_at: string;
227
+ }
228
+ type TaxFormTrigger = 'trigger_account_creation' | 'trigger_first_withdrawal' | 'trigger_threshold' | 'trigger_manual' | 'trigger_tin_invalid' | 'trigger_w8ben_expiry' | 'trigger_tin_expired';
229
+ interface CustomerTaxProfileDocuments {
230
+ w_form: string;
231
+ '1099_form': string;
232
+ }
233
+ interface RequestTaxFormWithProfileInput {
234
+ customer_id: string;
235
+ email: string;
236
+ trigger: TaxFormTrigger;
237
+ first_name?: string;
238
+ last_name?: string;
239
+ date_of_birth?: string;
240
+ phone_number?: string;
241
+ country?: string;
242
+ state?: string;
243
+ city?: string;
244
+ zip_code?: string;
245
+ address?: string;
246
+ ip_address?: string;
247
+ form_type?: string;
248
+ }
249
+ interface RequestTaxFormWithProfileOutput {
250
+ profile?: CustomerTaxProfileRecord;
251
+ request_id?: string;
252
+ reference_id?: string;
253
+ form_id?: string;
254
+ public_url?: string;
255
+ }
256
+
257
+ declare class TaxTransactionClient extends BaseClient {
258
+ getCustomerDocuments(customerID: string): Promise<CustomerTaxProfileDocuments>;
259
+ requestTaxFormWithProfile(input: RequestTaxFormWithProfileInput, requestOptions?: RequestOptions): Promise<RequestTaxFormWithProfileOutput>;
260
+ }
157
261
 
158
262
  /**
159
263
  * TransactionClient — SDK for the Transaction Monitoring service
160
264
  */
161
265
 
162
266
  declare class TransactionClient extends BaseClient {
267
+ readonly tax: TaxTransactionClient;
163
268
  constructor(config: BaseClientConfig);
269
+ /**
270
+ * Submit a transaction via the unified ingestion endpoint.
271
+ *
272
+ * POST /api/v1/transactions is gated on ANY of transaction monitoring, tax,
273
+ * or fraud being enabled for the tenant, so fraud-only and tax-only tenants
274
+ * can ingest even with TM switched off.
275
+ */
164
276
  createTransaction(request: TransactionCreateDTO, requestOptions?: RequestOptions): Promise<TransactionCreateResponse>;
277
+ submitWithdrawal(request: TransactionCreateDTO, requestOptions?: RequestOptions): Promise<PendingWithdrawal>;
165
278
  getTransaction(transactionId: string, requestOptions?: RequestOptions): Promise<Transaction>;
279
+ getSettings(): Promise<TmTenantSettings>;
280
+ }
281
+
282
+ declare class DuplicateTransactionError extends VesantError {
283
+ readonly tx_id: string;
284
+ constructor(tx_id: string, message?: string);
285
+ }
286
+ declare class TaxHoldError extends Error {
287
+ readonly tx_id: string;
288
+ readonly hold_reason: HoldReason;
289
+ readonly event: TransactionCallbackEvent;
290
+ constructor(event: TransactionCallbackEvent);
166
291
  }
167
292
 
168
293
  type EventHandler<T> = (event: T) => void | Promise<void>;
294
+ declare const TRANSACTION_SIGNATURE_HEADER = "x-webhook-signature";
169
295
  interface TransactionWebhookHandlerConfig {
170
296
  secret: string;
171
297
  /** Reject duplicate event_type+tx_id pairs within replayWindow. Default: true. */
172
298
  replayProtection?: boolean;
173
299
  /** How long (ms) to remember seen events for replay detection. Default: 300_000 (5 min). */
174
300
  replayWindow?: number;
301
+ /** Pluggable dedup store for replay protection (default: in-memory, process-local). */
302
+ dedupStore?: WebhookDedupStore;
175
303
  }
176
304
  declare class TransactionWebhookHandler {
177
305
  private readonly handlers;
178
306
  private readonly secret;
179
307
  private readonly replayProtection;
180
308
  private readonly replayWindow;
181
- private seenEvents;
309
+ private readonly dedupStore;
310
+ private readonly pendingWithdrawals;
311
+ private callbackHandler;
182
312
  constructor(config: TransactionWebhookHandlerConfig);
183
313
  on(eventType: 'tm.transaction.callback', handler: EventHandler<TransactionCallbackEvent>): this;
184
314
  on(eventType: 'tm.transaction.held', handler: EventHandler<TransactionHeldEvent>): this;
315
+ /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */
316
+ registerCallbackHandler(handler: TransactionCallbackHandler): this;
317
+ /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */
318
+ link(pending: PendingWithdrawal): this;
319
+ /** Stop tracking a PendingWithdrawal by tx_id. */
320
+ unlink(tx_id: string): this;
185
321
  /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */
186
322
  verify(rawBody: string, signature: string): Promise<boolean>;
187
323
  /** Parse and validate required fields. Does not verify signature — use verifyAndParse in production. */
@@ -192,4 +328,4 @@ declare class TransactionWebhookHandler {
192
328
  handle(body: string, signature: string): Promise<void>;
193
329
  }
194
330
 
195
- export { type HoldReason, type JSONB, type Transaction, type TransactionCallbackEvent, TransactionClient, type TransactionClientConfig, type TransactionCreateDTO, type TransactionCreateResponse, type TransactionHeldEvent, type TransactionMode, type TransactionStatus, type TransactionType, type TransactionWebhookEvent, TransactionWebhookHandler, type TransactionWebhookHandlerConfig, type WithholdingReason, type WithholdingType };
331
+ export { type CTPFormStatus, type CTPTINStatus, type CustomerTaxProfileDocuments, type CustomerTaxProfileRecord, type CustomerType, DuplicateTransactionError, type HoldReason, PendingWithdrawal, type RequestTaxFormWithProfileInput, type RequestTaxFormWithProfileOutput, TRANSACTION_SIGNATURE_HEADER, type TaxFormTrigger, TaxHoldError, TaxTransactionClient, 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
@@ -3,39 +3,199 @@
3
3
  var core = require('@vesant-sdk/core');
4
4
 
5
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
+ var TaxTransactionClient = class extends core.BaseClient {
93
+ async getCustomerDocuments(customerID) {
94
+ return this.request(
95
+ `/api/v1/tm/customer-tax-profiles/${customerID}/documents`
96
+ );
97
+ }
98
+ async requestTaxFormWithProfile(input, requestOptions) {
99
+ return this.requestWithRetry(
100
+ "/api/v1/tax/customer-tax-profiles/request-form-with-profile",
101
+ {
102
+ method: "POST",
103
+ body: JSON.stringify(input)
104
+ },
105
+ void 0,
106
+ void 0,
107
+ requestOptions
108
+ );
109
+ }
110
+ };
111
+
112
+ // src/client.ts
113
+ function assertWellFormedCurrency(currency) {
114
+ if (!/^[A-Za-z]{3}$/.test(currency ?? "")) {
115
+ throw new core.ValidationError(
116
+ `Invalid currency code: ${JSON.stringify(currency)}. Expected a 3-letter ISO 4217 code.`,
117
+ ["currency"]
118
+ );
119
+ }
120
+ }
6
121
  var TransactionClient = class extends core.BaseClient {
7
122
  constructor(config) {
8
123
  super(config);
124
+ this.tax = new TaxTransactionClient(config);
9
125
  }
126
+ /**
127
+ * Submit a transaction via the unified ingestion endpoint.
128
+ *
129
+ * POST /api/v1/transactions is gated on ANY of transaction monitoring, tax,
130
+ * or fraud being enabled for the tenant, so fraud-only and tax-only tenants
131
+ * can ingest even with TM switched off.
132
+ */
10
133
  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;
134
+ assertWellFormedCurrency(request.currency);
135
+ try {
136
+ return await this.requestWithRetry("/api/v1/transactions", {
137
+ method: "POST",
138
+ body: JSON.stringify(request)
139
+ }, void 0, void 0, requestOptions);
140
+ } catch (error) {
141
+ if (error instanceof core.VesantError && error.statusCode === 409) {
142
+ const transaction = await this.getTransaction(request.tx_id, requestOptions);
143
+ return { transaction, message: "Transaction already exists" };
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+ async submitWithdrawal(request, requestOptions) {
149
+ const response = await this.createTransaction(
150
+ { ...request, transaction_type: "withdrawal" },
151
+ requestOptions
152
+ );
153
+ return new PendingWithdrawal(response.transaction.tx_id, response.transaction);
16
154
  }
17
155
  async getTransaction(transactionId, requestOptions) {
18
- const data = await this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
156
+ return this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
19
157
  method: "GET"
20
158
  }, void 0, void 0, requestOptions);
21
- return data;
159
+ }
160
+ async getSettings() {
161
+ return this.requestWithRetry("/api/v1/tm/settings", {
162
+ method: "GET"
163
+ });
22
164
  }
23
165
  };
166
+ var TRANSACTION_SIGNATURE_HEADER = "x-webhook-signature";
24
167
  var TransactionWebhookHandler = class {
25
168
  constructor(config) {
26
169
  this.handlers = {
27
170
  "tm.transaction.callback": [],
28
171
  "tm.transaction.held": []
29
172
  };
30
- this.seenEvents = /* @__PURE__ */ new Map();
173
+ this.pendingWithdrawals = /* @__PURE__ */ new Map();
174
+ this.callbackHandler = null;
31
175
  this.secret = config.secret;
32
176
  this.replayProtection = config.replayProtection ?? true;
33
177
  this.replayWindow = config.replayWindow ?? 3e5;
178
+ this.dedupStore = config.dedupStore ?? new core.InMemoryDedupStore();
34
179
  }
35
180
  on(eventType, handler) {
36
181
  this.handlers[eventType].push(handler);
37
182
  return this;
38
183
  }
184
+ /** Register a semantic callback handler for the three withholding outcomes. Replaces any previous handler. */
185
+ registerCallbackHandler(handler) {
186
+ this.callbackHandler = handler;
187
+ return this;
188
+ }
189
+ /** Track a PendingWithdrawal so it is auto-settled when its tm.transaction.callback arrives. */
190
+ link(pending) {
191
+ this.pendingWithdrawals.set(pending.tx_id, pending);
192
+ return this;
193
+ }
194
+ /** Stop tracking a PendingWithdrawal by tx_id. */
195
+ unlink(tx_id) {
196
+ this.pendingWithdrawals.delete(tx_id);
197
+ return this;
198
+ }
39
199
  /** Verify HMAC-SHA256 signature only. Returns false instead of throwing. */
40
200
  async verify(rawBody, signature) {
41
201
  return core.verifyWebhookSignature(rawBody, signature, this.secret);
@@ -60,27 +220,35 @@ var TransactionWebhookHandler = class {
60
220
  const event = this.parse(body);
61
221
  if (this.replayProtection) {
62
222
  const key = `${event.event_type}:${event.tx_id}`;
63
- if (this.seenEvents.has(key)) {
223
+ if (await this.dedupStore.seen(key)) {
64
224
  throw new core.ValidationError(
65
225
  `Duplicate webhook event: ${event.event_type} for ${event.tx_id} has already been processed`,
66
226
  ["tx_id"]
67
227
  );
68
228
  }
69
- const now = Date.now();
70
- this.seenEvents.set(key, now);
71
- if (this.seenEvents.size > 1e3) {
72
- for (const [k, seenAt] of this.seenEvents) {
73
- if (now - seenAt > this.replayWindow) {
74
- this.seenEvents.delete(k);
75
- }
76
- }
77
- }
229
+ await this.dedupStore.mark(key, this.replayWindow);
78
230
  }
79
231
  return event;
80
232
  }
81
233
  /** Verify signature, parse, and dispatch to registered handlers. */
82
234
  async handle(body, signature) {
83
235
  const event = await this.verifyAndParse(body, signature);
236
+ if (event.event_type === "tm.transaction.callback") {
237
+ const pending = this.pendingWithdrawals.get(event.tx_id);
238
+ if (pending) {
239
+ pending._settle(event);
240
+ this.pendingWithdrawals.delete(event.tx_id);
241
+ }
242
+ if (this.callbackHandler) {
243
+ if (event.status === "hold" || event.status === "suspend") {
244
+ await this.callbackHandler.onHeld?.(event);
245
+ } else if (event.withholding_type === "none") {
246
+ await this.callbackHandler.onReleased?.(event);
247
+ } else {
248
+ await this.callbackHandler.onWithheld?.(event);
249
+ }
250
+ }
251
+ }
84
252
  const handlers = this.handlers[event.event_type];
85
253
  for (const handler of handlers) {
86
254
  await handler(event);
@@ -88,6 +256,11 @@ var TransactionWebhookHandler = class {
88
256
  }
89
257
  };
90
258
 
259
+ exports.DuplicateTransactionError = DuplicateTransactionError;
260
+ exports.PendingWithdrawal = PendingWithdrawal;
261
+ exports.TRANSACTION_SIGNATURE_HEADER = TRANSACTION_SIGNATURE_HEADER;
262
+ exports.TaxHoldError = TaxHoldError;
263
+ exports.TaxTransactionClient = TaxTransactionClient;
91
264
  exports.TransactionClient = TransactionClient;
92
265
  exports.TransactionWebhookHandler = TransactionWebhookHandler;
93
266
  //# sourceMappingURL=index.js.map