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,39 @@
1
+ "use strict";
2
+ /**
3
+ * Official Node.js/TypeScript SDK for the ZeroKYC Pay crypto payment gateway.
4
+ *
5
+ * Server-side only: API keys and webhook verification must never run in a
6
+ * browser.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.VERSION = exports.WebhookVerificationError = exports.NetworkError = exports.APIError = exports.RateLimitError = exports.ValidationError = exports.AuthenticationError = exports.ZeroKYCError = exports.isTerminalStatus = exports.StatusMapper = exports.compareDecimals = exports.ReplayGuard = exports.DEFAULT_TOLERANCE_SECONDS = exports.WebhookVerifier = exports.WebhookEvent = exports.CreateInvoiceResponse = exports.Invoice = exports.DEFAULT_BASE_URL = exports.Config = exports.idempotencyKey = exports.ZeroKYC = void 0;
10
+ var client_js_1 = require("./client.js");
11
+ Object.defineProperty(exports, "ZeroKYC", { enumerable: true, get: function () { return client_js_1.ZeroKYC; } });
12
+ Object.defineProperty(exports, "idempotencyKey", { enumerable: true, get: function () { return client_js_1.idempotencyKey; } });
13
+ var config_js_1 = require("./config.js");
14
+ Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return config_js_1.Config; } });
15
+ Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return config_js_1.DEFAULT_BASE_URL; } });
16
+ var invoice_js_1 = require("./models/invoice.js");
17
+ Object.defineProperty(exports, "Invoice", { enumerable: true, get: function () { return invoice_js_1.Invoice; } });
18
+ Object.defineProperty(exports, "CreateInvoiceResponse", { enumerable: true, get: function () { return invoice_js_1.CreateInvoiceResponse; } });
19
+ var webhook_js_1 = require("./models/webhook.js");
20
+ Object.defineProperty(exports, "WebhookEvent", { enumerable: true, get: function () { return webhook_js_1.WebhookEvent; } });
21
+ var verifier_js_1 = require("./webhooks/verifier.js");
22
+ Object.defineProperty(exports, "WebhookVerifier", { enumerable: true, get: function () { return verifier_js_1.WebhookVerifier; } });
23
+ Object.defineProperty(exports, "DEFAULT_TOLERANCE_SECONDS", { enumerable: true, get: function () { return verifier_js_1.DEFAULT_TOLERANCE_SECONDS; } });
24
+ var replay_js_1 = require("./replay.js");
25
+ Object.defineProperty(exports, "ReplayGuard", { enumerable: true, get: function () { return replay_js_1.ReplayGuard; } });
26
+ Object.defineProperty(exports, "compareDecimals", { enumerable: true, get: function () { return replay_js_1.compareDecimals; } });
27
+ var status_js_1 = require("./status.js");
28
+ Object.defineProperty(exports, "StatusMapper", { enumerable: true, get: function () { return status_js_1.StatusMapper; } });
29
+ Object.defineProperty(exports, "isTerminalStatus", { enumerable: true, get: function () { return status_js_1.isTerminalStatus; } });
30
+ var errors_js_1 = require("./errors.js");
31
+ Object.defineProperty(exports, "ZeroKYCError", { enumerable: true, get: function () { return errors_js_1.ZeroKYCError; } });
32
+ Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_js_1.AuthenticationError; } });
33
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_js_1.ValidationError; } });
34
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_js_1.RateLimitError; } });
35
+ Object.defineProperty(exports, "APIError", { enumerable: true, get: function () { return errors_js_1.APIError; } });
36
+ Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return errors_js_1.NetworkError; } });
37
+ Object.defineProperty(exports, "WebhookVerificationError", { enumerable: true, get: function () { return errors_js_1.WebhookVerificationError; } });
38
+ var version_js_1 = require("./version.js");
39
+ Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return version_js_1.VERSION; } });
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * Invoice creation options + payload building with local validation.
4
+ *
5
+ * Amounts are decimal STRINGS end to end (never JS numbers). Validation
6
+ * accepts only plain finite positive decimals - the strict ^[0-9]+(\.[0-9]+)?$
7
+ * grammar (no sign, exponent, NaN/Infinity spellings) with a redundant
8
+ * finiteness/positivity check, so obvious mistakes never leave the process.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.buildCreatePayload = buildCreatePayload;
12
+ const errors_js_1 = require("./errors.js");
13
+ /** Plain decimal only: no sign, no exponent, no NaN/Infinity spellings. */
14
+ const AMOUNT_RE = /^[0-9]+(?:\.[0-9]+)?$/;
15
+ function buildCreatePayload(options) {
16
+ const { amount } = options;
17
+ if (typeof amount !== "string" || !AMOUNT_RE.test(amount)) {
18
+ throw new errors_js_1.ValidationError(`amount must be a plain positive decimal string, got ${JSON.stringify(amount)}`);
19
+ }
20
+ // belt and suspenders: the regex already rejects NaN/Infinity/0/negatives
21
+ if (!isFinitePositiveDecimal(amount)) {
22
+ throw new errors_js_1.ValidationError(`amount must be a positive finite decimal, got ${JSON.stringify(amount)}`);
23
+ }
24
+ const currency = options.currency ?? "USD";
25
+ if (!currency) {
26
+ throw new errors_js_1.ValidationError("currency must not be empty");
27
+ }
28
+ if (options.ttlMinutes !== undefined &&
29
+ (!Number.isInteger(options.ttlMinutes) ||
30
+ options.ttlMinutes < 10 || options.ttlMinutes > 4320)) {
31
+ throw new errors_js_1.ValidationError("ttlMinutes must be an integer between 10 and 4320");
32
+ }
33
+ if (options.orderId !== undefined && options.orderId.length > 255) {
34
+ throw new errors_js_1.ValidationError("orderId must be at most 255 characters");
35
+ }
36
+ if (options.description !== undefined && options.description.length > 500) {
37
+ throw new errors_js_1.ValidationError("description must be at most 500 characters");
38
+ }
39
+ if (options.webhookUrl !== undefined && options.webhookUrl.length > 2000) {
40
+ throw new errors_js_1.ValidationError("webhookUrl must be at most 2000 characters");
41
+ }
42
+ if (options.successUrl !== undefined && options.successUrl.length > 2000) {
43
+ throw new errors_js_1.ValidationError("successUrl must be at most 2000 characters");
44
+ }
45
+ const payload = {
46
+ amount, // stays a string - no float round-trip
47
+ base_currency: currency,
48
+ payment_currency: options.paymentCurrency ?? "any",
49
+ };
50
+ if (options.orderId !== undefined) {
51
+ payload.order_id = options.orderId;
52
+ }
53
+ if (options.description !== undefined) {
54
+ payload.description = options.description;
55
+ }
56
+ if (options.ttlMinutes !== undefined) {
57
+ payload.ttl_minutes = options.ttlMinutes;
58
+ }
59
+ if (options.webhookUrl !== undefined) {
60
+ payload.webhook_url = options.webhookUrl;
61
+ }
62
+ if (options.successUrl !== undefined) {
63
+ payload.success_url = options.successUrl;
64
+ }
65
+ if (options.metadata && Object.keys(options.metadata).length > 0) {
66
+ payload.metadata = options.metadata;
67
+ }
68
+ return payload;
69
+ }
70
+ /** Rejects "0", "0.000", all-zero decimals; grammar already excludes the rest. */
71
+ function isFinitePositiveDecimal(amount) {
72
+ const trimmed = amount.replace(/^0+(?=\d)/, "");
73
+ return !/^0*(\.0+)?$/.test(trimmed);
74
+ }
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ /** Typed invoice models (create + get responses share the shape). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CreateInvoiceResponse = exports.Invoice = void 0;
5
+ const status_js_1 = require("../status.js");
6
+ class Invoice {
7
+ id;
8
+ orderId;
9
+ description;
10
+ /** Decimal string - never a JS number. */
11
+ amount;
12
+ baseCurrency;
13
+ paymentCurrency;
14
+ rawStatus;
15
+ status;
16
+ ttlMinutes;
17
+ expiresAt;
18
+ createdAt;
19
+ checkoutUrl;
20
+ metadata;
21
+ options;
22
+ observations;
23
+ paidAmount;
24
+ paidAsset;
25
+ /** Full decoded response (unknown fields preserved). */
26
+ raw;
27
+ constructor(data, raw) {
28
+ this.id = data.id;
29
+ this.orderId = data.order_id ?? null;
30
+ this.description = data.description ?? null;
31
+ this.amount = data.amount;
32
+ this.baseCurrency = data.base_currency;
33
+ this.paymentCurrency = data.payment_currency;
34
+ this.rawStatus = data.status;
35
+ this.status = status_js_1.StatusMapper.normalize(data.status) ?? "FAILED";
36
+ this.ttlMinutes = data.ttl_minutes;
37
+ this.expiresAt = data.expires_at;
38
+ this.createdAt = data.created_at;
39
+ this.checkoutUrl = data.checkout_url;
40
+ this.metadata = data.metadata ?? {};
41
+ this.options = data.options ?? [];
42
+ this.observations = data.observations ?? [];
43
+ this.paidAmount = data.paid_amount ?? null;
44
+ this.paidAsset = data.paid_asset ?? null;
45
+ this.raw = raw;
46
+ }
47
+ static fromJson(data) {
48
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
49
+ throw new Error("invoice response is not a JSON object");
50
+ }
51
+ const d = data;
52
+ // Every declared field is validated and built explicitly; unknown extras
53
+ // live in `raw` only and can never overwrite a checked/normalized value.
54
+ return new Invoice({
55
+ id: requiredString(d, "id"),
56
+ order_id: nullableString(d.order_id, "order_id"),
57
+ description: nullableString(d.description, "description"),
58
+ amount: requiredDecimalString(d.amount, "amount"),
59
+ base_currency: requiredString(d, "base_currency"),
60
+ payment_currency: requiredString(d, "payment_currency"),
61
+ status: requiredString(d, "status"),
62
+ ttl_minutes: requiredInteger(d.ttl_minutes, "ttl_minutes"),
63
+ expires_at: requiredString(d, "expires_at"),
64
+ created_at: requiredString(d, "created_at"),
65
+ checkout_url: requiredString(d, "checkout_url"),
66
+ metadata: requiredObject(d.metadata, "metadata"),
67
+ options: requiredOptions(d.options),
68
+ observations: requiredObservations(d.observations),
69
+ paid_amount: nullableDecimalString(d.paid_amount, "paid_amount"),
70
+ paid_asset: nullableString(d.paid_asset, "paid_asset"),
71
+ }, d);
72
+ }
73
+ get isPaid() {
74
+ return this.status === "PAID";
75
+ }
76
+ get isTerminal() {
77
+ return (0, status_js_1.isTerminalStatus)(this.status);
78
+ }
79
+ /** Payment option by asset id (e.g. "USDT_TRON"), or null. */
80
+ option(asset) {
81
+ return this.options.find((o) => o.asset === asset) ?? null;
82
+ }
83
+ }
84
+ exports.Invoice = Invoice;
85
+ class CreateInvoiceResponse {
86
+ invoice;
87
+ idempotentReplay;
88
+ constructor(invoice,
89
+ /** True when the API returned a previously created invoice for the same
90
+ * Idempotency-Key (safe timeout/retry path). */
91
+ idempotentReplay) {
92
+ this.invoice = invoice;
93
+ this.idempotentReplay = idempotentReplay;
94
+ }
95
+ }
96
+ exports.CreateInvoiceResponse = CreateInvoiceResponse;
97
+ // --- runtime validation helpers (public types are the contract) -------------
98
+ const DECIMAL_STRING_RE = /^[0-9]+(?:\.[0-9]+)?$/;
99
+ function requiredString(d, field) {
100
+ const value = d[field];
101
+ if (typeof value !== "string" || value === "") {
102
+ throw new Error(`invoice response field '${field}' must be a non-empty string`);
103
+ }
104
+ return value;
105
+ }
106
+ function nullableString(value, field) {
107
+ if (value === undefined || value === null) {
108
+ return null;
109
+ }
110
+ if (typeof value !== "string") {
111
+ throw new Error(`invoice response field '${field}' must be a string or null`);
112
+ }
113
+ return value;
114
+ }
115
+ function requiredDecimalString(value, field) {
116
+ if (typeof value !== "string" || !DECIMAL_STRING_RE.test(value)) {
117
+ throw new Error(`invoice response field '${field}' must be a decimal string`);
118
+ }
119
+ return value;
120
+ }
121
+ function nullableDecimalString(value, field) {
122
+ if (value === undefined || value === null) {
123
+ return null;
124
+ }
125
+ if (typeof value !== "string" || !DECIMAL_STRING_RE.test(value)) {
126
+ throw new Error(`invoice response field '${field}' must be a decimal string or null`);
127
+ }
128
+ return value;
129
+ }
130
+ function requiredInteger(value, field) {
131
+ if (!Number.isInteger(value)) {
132
+ throw new Error(`invoice response field '${field}' must be an integer`);
133
+ }
134
+ return value;
135
+ }
136
+ function requiredObject(value, field) {
137
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
138
+ throw new Error(`invoice response field '${field}' must be a JSON object`);
139
+ }
140
+ return value;
141
+ }
142
+ function requiredArray(value, field) {
143
+ if (!Array.isArray(value)) {
144
+ throw new Error(`invoice response field '${field}' must be an array`);
145
+ }
146
+ return value;
147
+ }
148
+ function requiredOptions(value) {
149
+ return requiredArray(value, "options").map((raw, i) => {
150
+ const o = requiredObject(raw, `options[${i}]`);
151
+ return {
152
+ ...o,
153
+ asset: optionString(o, "asset", i),
154
+ network: optionString(o, "network", i),
155
+ payment_address: optionString(o, "payment_address", i),
156
+ amount_crypto: optionDecimal(o, "amount_crypto", i),
157
+ rate: optionString(o, "rate", i),
158
+ status: optionString(o, "status", i),
159
+ };
160
+ });
161
+ }
162
+ function optionString(o, field, i) {
163
+ const value = o[field];
164
+ if (typeof value !== "string" || value === "") {
165
+ throw new Error(`invoice response field 'options[${i}].${field}' must be a non-empty string`);
166
+ }
167
+ return value;
168
+ }
169
+ function optionDecimal(o, field, i) {
170
+ const value = o[field];
171
+ if (typeof value !== "string" || !DECIMAL_STRING_RE.test(value)) {
172
+ throw new Error(`invoice response field 'options[${i}].${field}' must be a decimal string`);
173
+ }
174
+ return value;
175
+ }
176
+ function requiredObservations(value) {
177
+ return requiredArray(value, "observations").map((raw, i) => {
178
+ const o = requiredObject(raw, `observations[${i}]`);
179
+ return {
180
+ ...o,
181
+ option_id: optionString(o, "option_id", i),
182
+ tx_hash: optionString(o, "tx_hash", i),
183
+ output_index: requiredInteger(o.output_index, `observations[${i}].output_index`),
184
+ amount_paid: optionDecimal(o, "amount_paid", i),
185
+ confirmations: requiredInteger(o.confirmations, `observations[${i}].confirmations`),
186
+ seen_at: optionString(o, "seen_at", i),
187
+ };
188
+ });
189
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Verified webhook event model.
4
+ *
5
+ * Production payloads keep the invoice id inside `data` and the paid asset as
6
+ * a TICKER nested in `data.option`; the docs test vector uses a simplified
7
+ * top-level shape. Both resolve transparently (same as the PHP/Python SDKs).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.WebhookEvent = void 0;
11
+ class WebhookEvent {
12
+ id;
13
+ type;
14
+ invoiceId;
15
+ data;
16
+ raw;
17
+ constructor(payload) {
18
+ this.id = payload.id;
19
+ this.type = payload.type;
20
+ const data = payload.data ?? {};
21
+ const invoiceId = payload.invoice_id ?? data["invoice_id"];
22
+ this.invoiceId = typeof invoiceId === "string" ? invoiceId : null;
23
+ this.data = data;
24
+ this.raw = payload;
25
+ }
26
+ static fromJson(payload) {
27
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
28
+ throw new Error("webhook payload is not a JSON object");
29
+ }
30
+ const p = payload;
31
+ // id/type are required non-empty strings - a signed `{}` must not pass
32
+ if (typeof p.id !== "string" || p.id === "") {
33
+ throw new Error("webhook payload is missing required field 'id'");
34
+ }
35
+ if (typeof p.type !== "string" || p.type === "") {
36
+ throw new Error("webhook payload is missing required field 'type'");
37
+ }
38
+ if (p.data !== undefined &&
39
+ (typeof p.data !== "object" || p.data === null || Array.isArray(p.data))) {
40
+ throw new Error("webhook payload field 'data' must be a JSON object");
41
+ }
42
+ for (const key of ["invoice_id"]) {
43
+ const value = p[key];
44
+ if (value !== undefined && value !== null &&
45
+ (typeof value !== "string" || value === "")) {
46
+ throw new Error(`webhook payload field '${key}' must be a non-empty string`);
47
+ }
48
+ }
49
+ const data = p.data ?? {};
50
+ const dataInvoiceId = data["invoice_id"];
51
+ if (dataInvoiceId !== undefined && dataInvoiceId !== null &&
52
+ (typeof dataInvoiceId !== "string" || dataInvoiceId === "")) {
53
+ throw new Error("webhook payload field 'data.invoice_id' must be a non-empty string");
54
+ }
55
+ const invoiceId = p.invoice_id ?? dataInvoiceId;
56
+ if (p.type === "payment.confirmed" &&
57
+ (typeof invoiceId !== "string" || invoiceId === "")) {
58
+ // crediting an order requires knowing which invoice was paid
59
+ throw new Error("payment.confirmed payload is missing the invoice id");
60
+ }
61
+ return new WebhookEvent(payload);
62
+ }
63
+ get isPaymentConfirmed() {
64
+ return this.type === "payment.confirmed";
65
+ }
66
+ /**
67
+ * Crypto amount actually received (payment.* events carry it under
68
+ * data.option.paid_amount; docs-vector shape: data.amount). For non-stable
69
+ * assets compare against the amount_crypto you invoiced, not the base total.
70
+ */
71
+ get paidAmount() {
72
+ const option = this.option();
73
+ const paid = option ? option["paid_amount"] : undefined;
74
+ const fallback = this.data["amount_paid"] ?? this.data["amount"] ?? undefined;
75
+ const value = paid ?? fallback;
76
+ return typeof value === "string" && value !== "" ? value : null;
77
+ }
78
+ /** Asset the payment arrived in: ticker ("USDT") or asset id ("USDT_TRON"). */
79
+ get paidAsset() {
80
+ const option = this.option();
81
+ const asset = (option ? option["asset"] : undefined) ?? this.data["asset"];
82
+ return typeof asset === "string" && asset !== "" ? asset : null;
83
+ }
84
+ /** Network of the paying option ("tron", "polygon", ...), when present. */
85
+ get paidNetwork() {
86
+ const option = this.option();
87
+ const network = option ? option["network"] : undefined;
88
+ return typeof network === "string" && network !== "" ? network : null;
89
+ }
90
+ option() {
91
+ const option = this.data["option"];
92
+ return typeof option === "object" && option !== null && !Array.isArray(option)
93
+ ? option
94
+ : null;
95
+ }
96
+ }
97
+ exports.WebhookEvent = WebhookEvent;
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * At-least-once delivery protection and payment matching (P0).
4
+ *
5
+ * `isDuplicate()` is a PURE lookup: it never mutates the store. An event is
6
+ * marked processed only via `markProcessed()` AFTER the local order update
7
+ * succeeded - marking earlier would turn any crash in between into a
8
+ * permanently unpaid order (the retry would be skipped as a duplicate).
9
+ *
10
+ * Concurrent deliveries of the same event can both pass `isDuplicate()`;
11
+ * apps needing strict single-processing should implement an atomic claim
12
+ * (INSERT ... ON CONFLICT / unique constraint) in their EventStore and use
13
+ * it as the source of truth.
14
+ *
15
+ * Monetary validation is fail-closed: an unparseable amount never compares
16
+ * as "enough" - `matchesOrder()` returns false and `compareDecimals()`
17
+ * throws instead of silently comparing garbage.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.ReplayGuard = void 0;
21
+ exports.isValidDecimal = isValidDecimal;
22
+ exports.compareDecimals = compareDecimals;
23
+ /** Plain non-negative decimal only: no sign, exponent, spaces, extra dots or
24
+ * NaN/Infinity spellings. Zero is a syntactically valid amount. */
25
+ const DECIMAL_RE = /^[0-9]+(?:\.[0-9]+)?$/;
26
+ /** True only for plain non-negative decimal strings (no float arithmetic). */
27
+ function isValidDecimal(value) {
28
+ return typeof value === "string" && DECIMAL_RE.test(value);
29
+ }
30
+ /**
31
+ * Compare two plain non-negative decimal strings WITHOUT floats.
32
+ * Throws TypeError on anything that is not a plain decimal - garbage must
33
+ * never compare silently (validate first; matchesOrder does).
34
+ */
35
+ function compareDecimals(a, b) {
36
+ for (const value of [a, b]) {
37
+ if (!DECIMAL_RE.test(value)) {
38
+ throw new TypeError(`not a plain non-negative decimal string: ${JSON.stringify(value)}`);
39
+ }
40
+ }
41
+ const [ai = "0", af = ""] = a.split(".");
42
+ const [bi = "0", bf = ""] = b.split(".");
43
+ // normalize BEFORE comparing: leading zeros must not affect length/value
44
+ // ("0002" is 2, not a 4-digit number greater than "10")
45
+ const norm = (s) => s.replace(/^0+(?=\d)/, "");
46
+ const nAi = norm(ai);
47
+ const nBi = norm(bi);
48
+ const intCmp = nAi.length !== nBi.length ? nAi.length - nBi.length : nAi.localeCompare(nBi);
49
+ const fracCmp = af.padEnd(bf.length, "0").localeCompare(bf.padEnd(af.length, "0"));
50
+ return intCmp || fracCmp; // never -0 (0 stays 0 via ||)
51
+ }
52
+ class ReplayGuard {
53
+ store;
54
+ constructor(store) {
55
+ this.store = store;
56
+ }
57
+ /** Pure check: true when the event was already processed. No side effects. */
58
+ async isDuplicate(eventId) {
59
+ return await this.store.has(eventId);
60
+ }
61
+ /** Call only after the local order update has succeeded. */
62
+ async markProcessed(eventId) {
63
+ await this.store.markProcessed(eventId);
64
+ }
65
+ /**
66
+ * Signature validity alone is NOT enough to credit an order: match the
67
+ * invoice id, the paid crypto amount (>= minAmount) and the asset.
68
+ * Fail-closed: invalid amount data means "not enough", never "paid".
69
+ */
70
+ matchesOrder(event, expectedInvoiceId, options = {}) {
71
+ if (event.invoiceId !== expectedInvoiceId) {
72
+ return false;
73
+ }
74
+ if (options.asset !== undefined && !assetMatches(event, options.asset)) {
75
+ return false;
76
+ }
77
+ if (options.minAmount !== undefined) {
78
+ const paid = event.paidAmount;
79
+ if (paid === null || !isValidDecimal(paid) || !isValidDecimal(options.minAmount)) {
80
+ return false;
81
+ }
82
+ if (compareDecimals(paid, options.minAmount) < 0) {
83
+ return false;
84
+ }
85
+ }
86
+ return true;
87
+ }
88
+ }
89
+ exports.ReplayGuard = ReplayGuard;
90
+ function assetMatches(event, expected) {
91
+ const paidRaw = (event.paidAsset ?? "").toLowerCase();
92
+ if (!paidRaw) {
93
+ return false;
94
+ }
95
+ const [expTicker, expNetwork] = expected.toLowerCase().split("_");
96
+ const [paidTicker] = paidRaw.split("_");
97
+ if (paidTicker !== expTicker) {
98
+ return false;
99
+ }
100
+ if (expNetwork !== undefined && expNetwork !== "") {
101
+ // asset id expected: the webhook MUST carry the network explicitly;
102
+ // missing or different network is a mismatch (fail-closed)
103
+ const paidNetwork = (event.paidNetwork ?? "").toLowerCase();
104
+ if (paidNetwork !== expNetwork) {
105
+ return false;
106
+ }
107
+ }
108
+ return true;
109
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * Normalized invoice status + raw-API status mapping.
4
+ *
5
+ * Raw statuses (created/pending/detecting/confirmed/underpaid/expired/canceled)
6
+ * never leak into billing logic: platforms consume InvoiceStatus
7
+ * (mirrors the PHP/Python SDKs).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.StatusMapper = void 0;
11
+ exports.isTerminalStatus = isTerminalStatus;
12
+ const MAP = {
13
+ created: "PENDING",
14
+ pending: "PENDING",
15
+ detecting: "CONFIRMING",
16
+ confirmed: "PAID",
17
+ underpaid: "UNDERPAID",
18
+ expired: "EXPIRED",
19
+ canceled: "CANCELLED",
20
+ // overpayment is credited and confirmed server-side; kept for forward
21
+ // compatibility and local bookkeeping
22
+ overpaid: "OVERPAID",
23
+ };
24
+ const RAW_TERMINAL = new Set(["confirmed", "underpaid", "expired", "canceled"]);
25
+ class StatusMapper {
26
+ static normalize(raw) {
27
+ return MAP[raw] ?? null;
28
+ }
29
+ static normalizeOrFail(raw) {
30
+ const status = MAP[raw];
31
+ if (!status) {
32
+ throw new Error(`unknown invoice status '${raw}'`);
33
+ }
34
+ return status;
35
+ }
36
+ static isTerminal(raw) {
37
+ return RAW_TERMINAL.has(raw);
38
+ }
39
+ static isPaid(raw) {
40
+ return raw === "confirmed";
41
+ }
42
+ }
43
+ exports.StatusMapper = StatusMapper;
44
+ function isTerminalStatus(status) {
45
+ return status !== "PENDING" && status !== "CONFIRMING";
46
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ /** Single source of truth for the SDK version. */
5
+ exports.VERSION = "0.1.0";
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Webhook signature verification - security critical (P0).
4
+ *
5
+ * Every delivery carries one header:
6
+ *
7
+ * X-ZKP-Signature: t=<unix seconds>,v1=<64-char lowercase hex>
8
+ *
9
+ * v1 = HMAC-SHA256(webhookSecret, "{t}.{rawBody}") where rawBody is the exact
10
+ * request body as received - never re-serialized JSON. Verification is strict
11
+ * (header format, ±tolerance window) and constant-time (timingSafeEqual).
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.WebhookVerifier = exports.DEFAULT_TOLERANCE_SECONDS = void 0;
15
+ const node_crypto_1 = require("node:crypto");
16
+ const errors_js_1 = require("../errors.js");
17
+ const webhook_js_1 = require("../models/webhook.js");
18
+ const HEADER_RE = /^t=(\d{1,12}),v1=([0-9a-f]{64})$/;
19
+ exports.DEFAULT_TOLERANCE_SECONDS = 300;
20
+ const MESSAGES = {
21
+ [errors_js_1.WebhookVerificationError.MISSING_HEADER]: "X-ZKP-Signature header is missing",
22
+ [errors_js_1.WebhookVerificationError.MALFORMED_HEADER]: "signature header is malformed (expected t=<int>,v1=<64 lowercase hex>)",
23
+ [errors_js_1.WebhookVerificationError.STALE_TIMESTAMP]: "signature timestamp is outside the tolerance window (stale)",
24
+ [errors_js_1.WebhookVerificationError.FUTURE_TIMESTAMP]: "signature timestamp is outside the tolerance window (future)",
25
+ [errors_js_1.WebhookVerificationError.SIGNATURE_MISMATCH]: "signature does not match the raw body",
26
+ [errors_js_1.WebhookVerificationError.MALFORMED_PAYLOAD]: "verified body is not a JSON object",
27
+ };
28
+ class WebhookVerifier {
29
+ secret;
30
+ toleranceSeconds;
31
+ constructor(webhookSecret, toleranceSeconds = exports.DEFAULT_TOLERANCE_SECONDS) {
32
+ if (!webhookSecret) {
33
+ throw new Error("webhook secret must not be empty");
34
+ }
35
+ if (!Number.isInteger(toleranceSeconds) || toleranceSeconds < 1) {
36
+ throw new Error("tolerance must be at least 1 second");
37
+ }
38
+ this.secret = webhookSecret;
39
+ this.toleranceSeconds = toleranceSeconds;
40
+ }
41
+ /** Verify and decode; throws WebhookVerificationError on any failure. */
42
+ verify(rawBody, signatureHeader, options = {}) {
43
+ const result = this.check(rawBody, signatureHeader, options);
44
+ if (!result.event) {
45
+ throw new errors_js_1.WebhookVerificationError(MESSAGES[result.reason ?? ""] ?? "verification failed", result.reason ?? "invalid");
46
+ }
47
+ return result.event;
48
+ }
49
+ /** Non-throwing variant returning a VerificationResult. */
50
+ check(rawBody, signatureHeader, options = {}) {
51
+ if (!signatureHeader) {
52
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.MISSING_HEADER, event: null };
53
+ }
54
+ const match = HEADER_RE.exec(signatureHeader);
55
+ if (!match) {
56
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.MALFORMED_HEADER, event: null };
57
+ }
58
+ const [, timestamp, signature] = match;
59
+ const now = options.now ?? Math.floor(Date.now() / 1000);
60
+ const skew = now - Number(timestamp);
61
+ if (skew > this.toleranceSeconds) {
62
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.STALE_TIMESTAMP, event: null };
63
+ }
64
+ if (-skew > this.toleranceSeconds) {
65
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.FUTURE_TIMESTAMP, event: null };
66
+ }
67
+ const expected = this.signPayload(timestamp, rawBody);
68
+ if (expected.length !== signature.length ||
69
+ !(0, node_crypto_1.timingSafeEqual)(Buffer.from(expected, "utf8"), Buffer.from(signature, "utf8"))) {
70
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.SIGNATURE_MISMATCH, event: null };
71
+ }
72
+ let payload;
73
+ try {
74
+ const text = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
75
+ payload = JSON.parse(text);
76
+ }
77
+ catch {
78
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.MALFORMED_PAYLOAD, event: null };
79
+ }
80
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
81
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.MALFORMED_PAYLOAD, event: null };
82
+ }
83
+ try {
84
+ return { valid: true, reason: null, event: webhook_js_1.WebhookEvent.fromJson(payload) };
85
+ }
86
+ catch {
87
+ return { valid: false, reason: errors_js_1.WebhookVerificationError.MALFORMED_PAYLOAD, event: null };
88
+ }
89
+ }
90
+ /** Build a signature header for a body (tests and local replay tooling). */
91
+ sign(rawBody, timestamp) {
92
+ const t = timestamp ?? Math.floor(Date.now() / 1000);
93
+ return `t=${t},v1=${this.signPayload(String(t), rawBody)}`;
94
+ }
95
+ signPayload(timestamp, rawBody) {
96
+ const body = typeof rawBody === "string" ? Buffer.from(rawBody, "utf8") : Buffer.from(rawBody);
97
+ return (0, node_crypto_1.createHmac)("sha256", this.secret).update(timestamp + ".").update(body).digest("hex");
98
+ }
99
+ }
100
+ exports.WebhookVerifier = WebhookVerifier;