zkp-sdk-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/SECURITY.md +34 -0
  4. package/dist/cjs/client.js +233 -0
  5. package/dist/cjs/config.js +54 -0
  6. package/dist/cjs/errors.js +70 -0
  7. package/dist/cjs/http.js +78 -0
  8. package/dist/cjs/index.js +39 -0
  9. package/dist/cjs/invoices.js +74 -0
  10. package/dist/cjs/models/invoice.js +189 -0
  11. package/dist/cjs/models/webhook.js +97 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/replay.js +109 -0
  14. package/dist/cjs/status.js +46 -0
  15. package/dist/cjs/version.js +5 -0
  16. package/dist/cjs/webhooks/verifier.js +100 -0
  17. package/dist/client.d.ts +57 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +228 -0
  20. package/dist/config.d.ts +29 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +50 -0
  23. package/dist/errors.d.ts +47 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +60 -0
  26. package/dist/http.d.ts +32 -0
  27. package/dist/http.d.ts.map +1 -0
  28. package/dist/http.js +73 -0
  29. package/dist/index.d.ts +16 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +15 -0
  32. package/dist/invoices.d.ts +35 -0
  33. package/dist/invoices.d.ts.map +1 -0
  34. package/dist/invoices.js +71 -0
  35. package/dist/models/invoice.d.ts +82 -0
  36. package/dist/models/invoice.d.ts.map +1 -0
  37. package/dist/models/invoice.js +184 -0
  38. package/dist/models/webhook.d.ts +42 -0
  39. package/dist/models/webhook.d.ts.map +1 -0
  40. package/dist/models/webhook.js +93 -0
  41. package/dist/replay.d.ts +52 -0
  42. package/dist/replay.d.ts.map +1 -0
  43. package/dist/replay.js +103 -0
  44. package/dist/status.d.ts +16 -0
  45. package/dist/status.d.ts.map +1 -0
  46. package/dist/status.js +41 -0
  47. package/dist/version.d.ts +3 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/dist/version.js +2 -0
  50. package/dist/webhooks/verifier.d.ts +38 -0
  51. package/dist/webhooks/verifier.d.ts.map +1 -0
  52. package/dist/webhooks/verifier.js +96 -0
  53. package/package.json +66 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Invoice creation options + payload building with local validation.
3
+ *
4
+ * Amounts are decimal STRINGS end to end (never JS numbers). Validation
5
+ * accepts only plain finite positive decimals - the strict ^[0-9]+(\.[0-9]+)?$
6
+ * grammar (no sign, exponent, NaN/Infinity spellings) with a redundant
7
+ * finiteness/positivity check, so obvious mistakes never leave the process.
8
+ */
9
+ export interface CreateInvoiceOptions {
10
+ /** Decimal string, e.g. "19.90". */
11
+ amount: string;
12
+ /** Base currency: "USD" | "EUR" | "RUB" | asset id. Default "USD". */
13
+ currency?: string;
14
+ paymentCurrency?: string;
15
+ orderId?: string;
16
+ description?: string;
17
+ ttlMinutes?: number;
18
+ webhookUrl?: string;
19
+ successUrl?: string;
20
+ metadata?: Record<string, unknown>;
21
+ idempotencyKey?: string;
22
+ }
23
+ export interface CreateInvoicePayload {
24
+ amount: string;
25
+ base_currency: string;
26
+ payment_currency: string;
27
+ order_id?: string;
28
+ description?: string;
29
+ ttl_minutes?: number;
30
+ webhook_url?: string;
31
+ success_url?: string;
32
+ metadata?: Record<string, unknown>;
33
+ }
34
+ export declare function buildCreatePayload(options: CreateInvoiceOptions): CreateInvoicePayload;
35
+ //# sourceMappingURL=invoices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoices.d.ts","sourceRoot":"","sources":["../src/invoices.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,MAAM,WAAW,oBAAoB;IACnC,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAuDtF"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Invoice creation options + payload building with local validation.
3
+ *
4
+ * Amounts are decimal STRINGS end to end (never JS numbers). Validation
5
+ * accepts only plain finite positive decimals - the strict ^[0-9]+(\.[0-9]+)?$
6
+ * grammar (no sign, exponent, NaN/Infinity spellings) with a redundant
7
+ * finiteness/positivity check, so obvious mistakes never leave the process.
8
+ */
9
+ import { ValidationError } from "./errors.js";
10
+ /** Plain decimal only: no sign, no exponent, no NaN/Infinity spellings. */
11
+ const AMOUNT_RE = /^[0-9]+(?:\.[0-9]+)?$/;
12
+ export function buildCreatePayload(options) {
13
+ const { amount } = options;
14
+ if (typeof amount !== "string" || !AMOUNT_RE.test(amount)) {
15
+ throw new ValidationError(`amount must be a plain positive decimal string, got ${JSON.stringify(amount)}`);
16
+ }
17
+ // belt and suspenders: the regex already rejects NaN/Infinity/0/negatives
18
+ if (!isFinitePositiveDecimal(amount)) {
19
+ throw new ValidationError(`amount must be a positive finite decimal, got ${JSON.stringify(amount)}`);
20
+ }
21
+ const currency = options.currency ?? "USD";
22
+ if (!currency) {
23
+ throw new ValidationError("currency must not be empty");
24
+ }
25
+ if (options.ttlMinutes !== undefined &&
26
+ (!Number.isInteger(options.ttlMinutes) ||
27
+ options.ttlMinutes < 10 || options.ttlMinutes > 4320)) {
28
+ throw new ValidationError("ttlMinutes must be an integer between 10 and 4320");
29
+ }
30
+ if (options.orderId !== undefined && options.orderId.length > 255) {
31
+ throw new ValidationError("orderId must be at most 255 characters");
32
+ }
33
+ if (options.description !== undefined && options.description.length > 500) {
34
+ throw new ValidationError("description must be at most 500 characters");
35
+ }
36
+ if (options.webhookUrl !== undefined && options.webhookUrl.length > 2000) {
37
+ throw new ValidationError("webhookUrl must be at most 2000 characters");
38
+ }
39
+ if (options.successUrl !== undefined && options.successUrl.length > 2000) {
40
+ throw new ValidationError("successUrl must be at most 2000 characters");
41
+ }
42
+ const payload = {
43
+ amount, // stays a string - no float round-trip
44
+ base_currency: currency,
45
+ payment_currency: options.paymentCurrency ?? "any",
46
+ };
47
+ if (options.orderId !== undefined) {
48
+ payload.order_id = options.orderId;
49
+ }
50
+ if (options.description !== undefined) {
51
+ payload.description = options.description;
52
+ }
53
+ if (options.ttlMinutes !== undefined) {
54
+ payload.ttl_minutes = options.ttlMinutes;
55
+ }
56
+ if (options.webhookUrl !== undefined) {
57
+ payload.webhook_url = options.webhookUrl;
58
+ }
59
+ if (options.successUrl !== undefined) {
60
+ payload.success_url = options.successUrl;
61
+ }
62
+ if (options.metadata && Object.keys(options.metadata).length > 0) {
63
+ payload.metadata = options.metadata;
64
+ }
65
+ return payload;
66
+ }
67
+ /** Rejects "0", "0.000", all-zero decimals; grammar already excludes the rest. */
68
+ function isFinitePositiveDecimal(amount) {
69
+ const trimmed = amount.replace(/^0+(?=\d)/, "");
70
+ return !/^0*(\.0+)?$/.test(trimmed);
71
+ }
@@ -0,0 +1,82 @@
1
+ /** Typed invoice models (create + get responses share the shape). */
2
+ import { type InvoiceStatus } from "../status.js";
3
+ export interface PaymentOption {
4
+ asset: string;
5
+ network: string;
6
+ payment_address: string;
7
+ amount_crypto: string;
8
+ rate: string;
9
+ status: string;
10
+ window_low?: string | null;
11
+ window_high?: string | null;
12
+ payment_url_extra?: string | null;
13
+ confirmed_tx_hash?: string | null;
14
+ [key: string]: unknown;
15
+ }
16
+ export interface InvoiceObservation {
17
+ option_id: string;
18
+ tx_hash: string;
19
+ output_index: number;
20
+ amount_paid: string;
21
+ confirmations: number;
22
+ seen_at: string;
23
+ [key: string]: unknown;
24
+ }
25
+ export interface InvoiceData {
26
+ id: string;
27
+ order_id?: string | null;
28
+ description?: string | null;
29
+ amount: string;
30
+ base_currency: string;
31
+ payment_currency: string;
32
+ status: string;
33
+ ttl_minutes: number;
34
+ expires_at: string;
35
+ created_at: string;
36
+ checkout_url: string;
37
+ metadata: Record<string, unknown>;
38
+ options: PaymentOption[];
39
+ observations: InvoiceObservation[];
40
+ paid_amount?: string | null;
41
+ paid_asset?: string | null;
42
+ [key: string]: unknown;
43
+ }
44
+ export declare class Invoice {
45
+ readonly id: string;
46
+ readonly orderId: string | null;
47
+ readonly description: string | null;
48
+ /** Decimal string - never a JS number. */
49
+ readonly amount: string;
50
+ readonly baseCurrency: string;
51
+ readonly paymentCurrency: string;
52
+ readonly rawStatus: string;
53
+ readonly status: InvoiceStatus;
54
+ readonly ttlMinutes: number;
55
+ readonly expiresAt: string;
56
+ readonly createdAt: string;
57
+ readonly checkoutUrl: string;
58
+ readonly metadata: Record<string, unknown>;
59
+ readonly options: PaymentOption[];
60
+ readonly observations: InvoiceObservation[];
61
+ readonly paidAmount: string | null;
62
+ readonly paidAsset: string | null;
63
+ /** Full decoded response (unknown fields preserved). */
64
+ readonly raw: InvoiceData;
65
+ private constructor();
66
+ static fromJson(data: unknown): Invoice;
67
+ get isPaid(): boolean;
68
+ get isTerminal(): boolean;
69
+ /** Payment option by asset id (e.g. "USDT_TRON"), or null. */
70
+ option(asset: string): PaymentOption | null;
71
+ }
72
+ export declare class CreateInvoiceResponse {
73
+ readonly invoice: Invoice;
74
+ /** True when the API returned a previously created invoice for the same
75
+ * Idempotency-Key (safe timeout/retry path). */
76
+ readonly idempotentReplay: boolean;
77
+ constructor(invoice: Invoice,
78
+ /** True when the API returned a previously created invoice for the same
79
+ * Idempotency-Key (safe timeout/retry path). */
80
+ idempotentReplay: boolean);
81
+ }
82
+ //# sourceMappingURL=invoice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoice.d.ts","sourceRoot":"","sources":["../../src/models/invoice.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,184 @@
1
+ /** Typed invoice models (create + get responses share the shape). */
2
+ import { isTerminalStatus, StatusMapper } from "../status.js";
3
+ export class Invoice {
4
+ id;
5
+ orderId;
6
+ description;
7
+ /** Decimal string - never a JS number. */
8
+ amount;
9
+ baseCurrency;
10
+ paymentCurrency;
11
+ rawStatus;
12
+ status;
13
+ ttlMinutes;
14
+ expiresAt;
15
+ createdAt;
16
+ checkoutUrl;
17
+ metadata;
18
+ options;
19
+ observations;
20
+ paidAmount;
21
+ paidAsset;
22
+ /** Full decoded response (unknown fields preserved). */
23
+ raw;
24
+ constructor(data, raw) {
25
+ this.id = data.id;
26
+ this.orderId = data.order_id ?? null;
27
+ this.description = data.description ?? null;
28
+ this.amount = data.amount;
29
+ this.baseCurrency = data.base_currency;
30
+ this.paymentCurrency = data.payment_currency;
31
+ this.rawStatus = data.status;
32
+ this.status = StatusMapper.normalize(data.status) ?? "FAILED";
33
+ this.ttlMinutes = data.ttl_minutes;
34
+ this.expiresAt = data.expires_at;
35
+ this.createdAt = data.created_at;
36
+ this.checkoutUrl = data.checkout_url;
37
+ this.metadata = data.metadata ?? {};
38
+ this.options = data.options ?? [];
39
+ this.observations = data.observations ?? [];
40
+ this.paidAmount = data.paid_amount ?? null;
41
+ this.paidAsset = data.paid_asset ?? null;
42
+ this.raw = raw;
43
+ }
44
+ static fromJson(data) {
45
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
46
+ throw new Error("invoice response is not a JSON object");
47
+ }
48
+ const d = data;
49
+ // Every declared field is validated and built explicitly; unknown extras
50
+ // live in `raw` only and can never overwrite a checked/normalized value.
51
+ return new Invoice({
52
+ id: requiredString(d, "id"),
53
+ order_id: nullableString(d.order_id, "order_id"),
54
+ description: nullableString(d.description, "description"),
55
+ amount: requiredDecimalString(d.amount, "amount"),
56
+ base_currency: requiredString(d, "base_currency"),
57
+ payment_currency: requiredString(d, "payment_currency"),
58
+ status: requiredString(d, "status"),
59
+ ttl_minutes: requiredInteger(d.ttl_minutes, "ttl_minutes"),
60
+ expires_at: requiredString(d, "expires_at"),
61
+ created_at: requiredString(d, "created_at"),
62
+ checkout_url: requiredString(d, "checkout_url"),
63
+ metadata: requiredObject(d.metadata, "metadata"),
64
+ options: requiredOptions(d.options),
65
+ observations: requiredObservations(d.observations),
66
+ paid_amount: nullableDecimalString(d.paid_amount, "paid_amount"),
67
+ paid_asset: nullableString(d.paid_asset, "paid_asset"),
68
+ }, d);
69
+ }
70
+ get isPaid() {
71
+ return this.status === "PAID";
72
+ }
73
+ get isTerminal() {
74
+ return isTerminalStatus(this.status);
75
+ }
76
+ /** Payment option by asset id (e.g. "USDT_TRON"), or null. */
77
+ option(asset) {
78
+ return this.options.find((o) => o.asset === asset) ?? null;
79
+ }
80
+ }
81
+ export class CreateInvoiceResponse {
82
+ invoice;
83
+ idempotentReplay;
84
+ constructor(invoice,
85
+ /** True when the API returned a previously created invoice for the same
86
+ * Idempotency-Key (safe timeout/retry path). */
87
+ idempotentReplay) {
88
+ this.invoice = invoice;
89
+ this.idempotentReplay = idempotentReplay;
90
+ }
91
+ }
92
+ // --- runtime validation helpers (public types are the contract) -------------
93
+ const DECIMAL_STRING_RE = /^[0-9]+(?:\.[0-9]+)?$/;
94
+ function requiredString(d, field) {
95
+ const value = d[field];
96
+ if (typeof value !== "string" || value === "") {
97
+ throw new Error(`invoice response field '${field}' must be a non-empty string`);
98
+ }
99
+ return value;
100
+ }
101
+ function nullableString(value, field) {
102
+ if (value === undefined || value === null) {
103
+ return null;
104
+ }
105
+ if (typeof value !== "string") {
106
+ throw new Error(`invoice response field '${field}' must be a string or null`);
107
+ }
108
+ return value;
109
+ }
110
+ function requiredDecimalString(value, field) {
111
+ if (typeof value !== "string" || !DECIMAL_STRING_RE.test(value)) {
112
+ throw new Error(`invoice response field '${field}' must be a decimal string`);
113
+ }
114
+ return value;
115
+ }
116
+ function nullableDecimalString(value, field) {
117
+ if (value === undefined || value === null) {
118
+ return null;
119
+ }
120
+ if (typeof value !== "string" || !DECIMAL_STRING_RE.test(value)) {
121
+ throw new Error(`invoice response field '${field}' must be a decimal string or null`);
122
+ }
123
+ return value;
124
+ }
125
+ function requiredInteger(value, field) {
126
+ if (!Number.isInteger(value)) {
127
+ throw new Error(`invoice response field '${field}' must be an integer`);
128
+ }
129
+ return value;
130
+ }
131
+ function requiredObject(value, field) {
132
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
133
+ throw new Error(`invoice response field '${field}' must be a JSON object`);
134
+ }
135
+ return value;
136
+ }
137
+ function requiredArray(value, field) {
138
+ if (!Array.isArray(value)) {
139
+ throw new Error(`invoice response field '${field}' must be an array`);
140
+ }
141
+ return value;
142
+ }
143
+ function requiredOptions(value) {
144
+ return requiredArray(value, "options").map((raw, i) => {
145
+ const o = requiredObject(raw, `options[${i}]`);
146
+ return {
147
+ ...o,
148
+ asset: optionString(o, "asset", i),
149
+ network: optionString(o, "network", i),
150
+ payment_address: optionString(o, "payment_address", i),
151
+ amount_crypto: optionDecimal(o, "amount_crypto", i),
152
+ rate: optionString(o, "rate", i),
153
+ status: optionString(o, "status", i),
154
+ };
155
+ });
156
+ }
157
+ function optionString(o, field, i) {
158
+ const value = o[field];
159
+ if (typeof value !== "string" || value === "") {
160
+ throw new Error(`invoice response field 'options[${i}].${field}' must be a non-empty string`);
161
+ }
162
+ return value;
163
+ }
164
+ function optionDecimal(o, field, i) {
165
+ const value = o[field];
166
+ if (typeof value !== "string" || !DECIMAL_STRING_RE.test(value)) {
167
+ throw new Error(`invoice response field 'options[${i}].${field}' must be a decimal string`);
168
+ }
169
+ return value;
170
+ }
171
+ function requiredObservations(value) {
172
+ return requiredArray(value, "observations").map((raw, i) => {
173
+ const o = requiredObject(raw, `observations[${i}]`);
174
+ return {
175
+ ...o,
176
+ option_id: optionString(o, "option_id", i),
177
+ tx_hash: optionString(o, "tx_hash", i),
178
+ output_index: requiredInteger(o.output_index, `observations[${i}].output_index`),
179
+ amount_paid: optionDecimal(o, "amount_paid", i),
180
+ confirmations: requiredInteger(o.confirmations, `observations[${i}].confirmations`),
181
+ seen_at: optionString(o, "seen_at", i),
182
+ };
183
+ });
184
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Verified webhook event model.
3
+ *
4
+ * Production payloads keep the invoice id inside `data` and the paid asset as
5
+ * a TICKER nested in `data.option`; the docs test vector uses a simplified
6
+ * top-level shape. Both resolve transparently (same as the PHP/Python SDKs).
7
+ */
8
+ export interface WebhookEventData {
9
+ id: string;
10
+ type: string;
11
+ invoice_id?: string | null;
12
+ created_at?: string;
13
+ data?: Record<string, unknown>;
14
+ [key: string]: unknown;
15
+ }
16
+ export declare class WebhookEvent {
17
+ readonly id: string;
18
+ readonly type: string;
19
+ readonly invoiceId: string | null;
20
+ readonly data: Record<string, unknown>;
21
+ readonly raw: WebhookEventData;
22
+ private constructor();
23
+ static fromJson(payload: unknown): WebhookEvent;
24
+ get isPaymentConfirmed(): boolean;
25
+ /**
26
+ * Crypto amount actually received (payment.* events carry it under
27
+ * data.option.paid_amount; docs-vector shape: data.amount). For non-stable
28
+ * assets compare against the amount_crypto you invoiced, not the base total.
29
+ */
30
+ get paidAmount(): string | null;
31
+ /** Asset the payment arrived in: ticker ("USDT") or asset id ("USDT_TRON"). */
32
+ get paidAsset(): string | null;
33
+ /** Network of the paying option ("tron", "polygon", ...), when present. */
34
+ get paidNetwork(): string | null;
35
+ private option;
36
+ }
37
+ export interface VerificationResult {
38
+ valid: boolean;
39
+ reason: string | null;
40
+ event: WebhookEvent | null;
41
+ }
42
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/models/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,YAAY;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC;IAE/B,OAAO;IAUP,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY;IAsC/C,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED;;;;OAIG;IACH,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAO9B;IAED,+EAA+E;IAC/E,IAAI,SAAS,IAAI,MAAM,GAAG,IAAI,CAI7B;IAED,2EAA2E;IAC3E,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAI/B;IAED,OAAO,CAAC,MAAM;CAMf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CAC5B"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Verified webhook event model.
3
+ *
4
+ * Production payloads keep the invoice id inside `data` and the paid asset as
5
+ * a TICKER nested in `data.option`; the docs test vector uses a simplified
6
+ * top-level shape. Both resolve transparently (same as the PHP/Python SDKs).
7
+ */
8
+ export class WebhookEvent {
9
+ id;
10
+ type;
11
+ invoiceId;
12
+ data;
13
+ raw;
14
+ constructor(payload) {
15
+ this.id = payload.id;
16
+ this.type = payload.type;
17
+ const data = payload.data ?? {};
18
+ const invoiceId = payload.invoice_id ?? data["invoice_id"];
19
+ this.invoiceId = typeof invoiceId === "string" ? invoiceId : null;
20
+ this.data = data;
21
+ this.raw = payload;
22
+ }
23
+ static fromJson(payload) {
24
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
25
+ throw new Error("webhook payload is not a JSON object");
26
+ }
27
+ const p = payload;
28
+ // id/type are required non-empty strings - a signed `{}` must not pass
29
+ if (typeof p.id !== "string" || p.id === "") {
30
+ throw new Error("webhook payload is missing required field 'id'");
31
+ }
32
+ if (typeof p.type !== "string" || p.type === "") {
33
+ throw new Error("webhook payload is missing required field 'type'");
34
+ }
35
+ if (p.data !== undefined &&
36
+ (typeof p.data !== "object" || p.data === null || Array.isArray(p.data))) {
37
+ throw new Error("webhook payload field 'data' must be a JSON object");
38
+ }
39
+ for (const key of ["invoice_id"]) {
40
+ const value = p[key];
41
+ if (value !== undefined && value !== null &&
42
+ (typeof value !== "string" || value === "")) {
43
+ throw new Error(`webhook payload field '${key}' must be a non-empty string`);
44
+ }
45
+ }
46
+ const data = p.data ?? {};
47
+ const dataInvoiceId = data["invoice_id"];
48
+ if (dataInvoiceId !== undefined && dataInvoiceId !== null &&
49
+ (typeof dataInvoiceId !== "string" || dataInvoiceId === "")) {
50
+ throw new Error("webhook payload field 'data.invoice_id' must be a non-empty string");
51
+ }
52
+ const invoiceId = p.invoice_id ?? dataInvoiceId;
53
+ if (p.type === "payment.confirmed" &&
54
+ (typeof invoiceId !== "string" || invoiceId === "")) {
55
+ // crediting an order requires knowing which invoice was paid
56
+ throw new Error("payment.confirmed payload is missing the invoice id");
57
+ }
58
+ return new WebhookEvent(payload);
59
+ }
60
+ get isPaymentConfirmed() {
61
+ return this.type === "payment.confirmed";
62
+ }
63
+ /**
64
+ * Crypto amount actually received (payment.* events carry it under
65
+ * data.option.paid_amount; docs-vector shape: data.amount). For non-stable
66
+ * assets compare against the amount_crypto you invoiced, not the base total.
67
+ */
68
+ get paidAmount() {
69
+ const option = this.option();
70
+ const paid = option ? option["paid_amount"] : undefined;
71
+ const fallback = this.data["amount_paid"] ?? this.data["amount"] ?? undefined;
72
+ const value = paid ?? fallback;
73
+ return typeof value === "string" && value !== "" ? value : null;
74
+ }
75
+ /** Asset the payment arrived in: ticker ("USDT") or asset id ("USDT_TRON"). */
76
+ get paidAsset() {
77
+ const option = this.option();
78
+ const asset = (option ? option["asset"] : undefined) ?? this.data["asset"];
79
+ return typeof asset === "string" && asset !== "" ? asset : null;
80
+ }
81
+ /** Network of the paying option ("tron", "polygon", ...), when present. */
82
+ get paidNetwork() {
83
+ const option = this.option();
84
+ const network = option ? option["network"] : undefined;
85
+ return typeof network === "string" && network !== "" ? network : null;
86
+ }
87
+ option() {
88
+ const option = this.data["option"];
89
+ return typeof option === "object" && option !== null && !Array.isArray(option)
90
+ ? option
91
+ : null;
92
+ }
93
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * At-least-once delivery protection and payment matching (P0).
3
+ *
4
+ * `isDuplicate()` is a PURE lookup: it never mutates the store. An event is
5
+ * marked processed only via `markProcessed()` AFTER the local order update
6
+ * succeeded - marking earlier would turn any crash in between into a
7
+ * permanently unpaid order (the retry would be skipped as a duplicate).
8
+ *
9
+ * Concurrent deliveries of the same event can both pass `isDuplicate()`;
10
+ * apps needing strict single-processing should implement an atomic claim
11
+ * (INSERT ... ON CONFLICT / unique constraint) in their EventStore and use
12
+ * it as the source of truth.
13
+ *
14
+ * Monetary validation is fail-closed: an unparseable amount never compares
15
+ * as "enough" - `matchesOrder()` returns false and `compareDecimals()`
16
+ * throws instead of silently comparing garbage.
17
+ */
18
+ import type { WebhookEvent } from "./models/webhook.js";
19
+ /** Storage backed by your DB: sync (in-memory) or async - both awaited. */
20
+ export interface EventStore {
21
+ has(eventId: string): boolean | Promise<boolean>;
22
+ markProcessed(eventId: string): void | Promise<void>;
23
+ }
24
+ /** True only for plain non-negative decimal strings (no float arithmetic). */
25
+ export declare function isValidDecimal(value: string): boolean;
26
+ /**
27
+ * Compare two plain non-negative decimal strings WITHOUT floats.
28
+ * Throws TypeError on anything that is not a plain decimal - garbage must
29
+ * never compare silently (validate first; matchesOrder does).
30
+ */
31
+ export declare function compareDecimals(a: string, b: string): number;
32
+ export interface MatchOrderOptions {
33
+ minAmount?: string;
34
+ /** Bare ticker ("USDT": any network) or asset id ("USDT_TRON": the webhook
35
+ * MUST carry a matching network - a missing network is a mismatch). */
36
+ asset?: string;
37
+ }
38
+ export declare class ReplayGuard {
39
+ private readonly store;
40
+ constructor(store: EventStore);
41
+ /** Pure check: true when the event was already processed. No side effects. */
42
+ isDuplicate(eventId: string): Promise<boolean>;
43
+ /** Call only after the local order update has succeeded. */
44
+ markProcessed(eventId: string): Promise<void>;
45
+ /**
46
+ * Signature validity alone is NOT enough to credit an order: match the
47
+ * invoice id, the paid crypto amount (>= minAmount) and the asset.
48
+ * Fail-closed: invalid amount data means "not enough", never "paid".
49
+ */
50
+ matchesOrder(event: WebhookEvent, expectedInvoiceId: string, options?: MatchOrderOptions): boolean;
51
+ }
52
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD;AAMD,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAiB5D;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;4EACwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,UAAU;IAE9C,8EAA8E;IACxE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpD,4DAA4D;IACtD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD;;;;OAIG;IACH,YAAY,CACV,KAAK,EAAE,YAAY,EACnB,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,iBAAsB,GAC9B,OAAO;CAkBX"}