@txnod/sdk 1.0.1

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 (121) hide show
  1. package/AGENTS.md +29 -0
  2. package/CHANGELOG.md +22 -0
  3. package/LICENSE +21 -0
  4. package/README.md +434 -0
  5. package/dist/_shared/index.d.ts +68 -0
  6. package/dist/client-sandbox.d.ts +396 -0
  7. package/dist/client-sandbox.d.ts.map +1 -0
  8. package/dist/client-sandbox.js +448 -0
  9. package/dist/client-sandbox.js.map +1 -0
  10. package/dist/client.d.ts +429 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +588 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/env.d.ts +29 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +44 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/errors.d.ts +1887 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +2107 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/index.d.ts +35 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +32 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/internals/error-ctor-map.d.ts +11 -0
  27. package/dist/internals/error-ctor-map.d.ts.map +1 -0
  28. package/dist/internals/error-ctor-map.js +75 -0
  29. package/dist/internals/error-ctor-map.js.map +1 -0
  30. package/dist/internals/fetch-with-retry.d.ts +34 -0
  31. package/dist/internals/fetch-with-retry.d.ts.map +1 -0
  32. package/dist/internals/fetch-with-retry.js +233 -0
  33. package/dist/internals/fetch-with-retry.js.map +1 -0
  34. package/dist/internals/hmac.d.ts +2 -0
  35. package/dist/internals/hmac.d.ts.map +1 -0
  36. package/dist/internals/hmac.js +10 -0
  37. package/dist/internals/hmac.js.map +1 -0
  38. package/dist/internals/logger.d.ts +9 -0
  39. package/dist/internals/logger.d.ts.map +1 -0
  40. package/dist/internals/logger.js +16 -0
  41. package/dist/internals/logger.js.map +1 -0
  42. package/dist/internals/parse-problem-details.d.ts +3 -0
  43. package/dist/internals/parse-problem-details.d.ts.map +1 -0
  44. package/dist/internals/parse-problem-details.js +76 -0
  45. package/dist/internals/parse-problem-details.js.map +1 -0
  46. package/dist/internals/synthetic-details.d.ts +12 -0
  47. package/dist/internals/synthetic-details.d.ts.map +1 -0
  48. package/dist/internals/synthetic-details.js +19 -0
  49. package/dist/internals/synthetic-details.js.map +1 -0
  50. package/dist/verify/chains/bsc.d.ts +17 -0
  51. package/dist/verify/chains/bsc.d.ts.map +1 -0
  52. package/dist/verify/chains/bsc.js +15 -0
  53. package/dist/verify/chains/bsc.js.map +1 -0
  54. package/dist/verify/chains/btc.d.ts +22 -0
  55. package/dist/verify/chains/btc.d.ts.map +1 -0
  56. package/dist/verify/chains/btc.js +55 -0
  57. package/dist/verify/chains/btc.js.map +1 -0
  58. package/dist/verify/chains/cardano.d.ts +73 -0
  59. package/dist/verify/chains/cardano.d.ts.map +1 -0
  60. package/dist/verify/chains/cardano.js +175 -0
  61. package/dist/verify/chains/cardano.js.map +1 -0
  62. package/dist/verify/chains/evm.d.ts +21 -0
  63. package/dist/verify/chains/evm.d.ts.map +1 -0
  64. package/dist/verify/chains/evm.js +46 -0
  65. package/dist/verify/chains/evm.js.map +1 -0
  66. package/dist/verify/chains/polygon.d.ts +17 -0
  67. package/dist/verify/chains/polygon.d.ts.map +1 -0
  68. package/dist/verify/chains/polygon.js +15 -0
  69. package/dist/verify/chains/polygon.js.map +1 -0
  70. package/dist/verify/chains/secp256k1-bip32.d.ts +20 -0
  71. package/dist/verify/chains/secp256k1-bip32.d.ts.map +1 -0
  72. package/dist/verify/chains/secp256k1-bip32.js +88 -0
  73. package/dist/verify/chains/secp256k1-bip32.js.map +1 -0
  74. package/dist/verify/chains/ton-cell.d.ts +179 -0
  75. package/dist/verify/chains/ton-cell.d.ts.map +1 -0
  76. package/dist/verify/chains/ton-cell.js +614 -0
  77. package/dist/verify/chains/ton-cell.js.map +1 -0
  78. package/dist/verify/chains/ton.d.ts +84 -0
  79. package/dist/verify/chains/ton.d.ts.map +1 -0
  80. package/dist/verify/chains/ton.js +131 -0
  81. package/dist/verify/chains/ton.js.map +1 -0
  82. package/dist/verify/chains/tron.d.ts +21 -0
  83. package/dist/verify/chains/tron.d.ts.map +1 -0
  84. package/dist/verify/chains/tron.js +42 -0
  85. package/dist/verify/chains/tron.js.map +1 -0
  86. package/dist/verify/config.d.ts +41 -0
  87. package/dist/verify/config.d.ts.map +1 -0
  88. package/dist/verify/config.js +120 -0
  89. package/dist/verify/config.js.map +1 -0
  90. package/dist/verify/errors.d.ts +56 -0
  91. package/dist/verify/errors.d.ts.map +1 -0
  92. package/dist/verify/errors.js +58 -0
  93. package/dist/verify/errors.js.map +1 -0
  94. package/dist/verify/index.d.ts +119 -0
  95. package/dist/verify/index.d.ts.map +1 -0
  96. package/dist/verify/index.js +166 -0
  97. package/dist/verify/index.js.map +1 -0
  98. package/dist/verify/xpub-safety.d.ts +33 -0
  99. package/dist/verify/xpub-safety.d.ts.map +1 -0
  100. package/dist/verify/xpub-safety.js +54 -0
  101. package/dist/verify/xpub-safety.js.map +1 -0
  102. package/dist/verify-webhook-signature.d.ts +30 -0
  103. package/dist/verify-webhook-signature.d.ts.map +1 -0
  104. package/dist/verify-webhook-signature.js +84 -0
  105. package/dist/verify-webhook-signature.js.map +1 -0
  106. package/docs/00-getting-started.md +135 -0
  107. package/docs/01-authentication.md +114 -0
  108. package/docs/02-invoices.md +216 -0
  109. package/docs/03-rates-and-quotes.md +82 -0
  110. package/docs/04-webhooks.md +126 -0
  111. package/docs/05-errors.md +199 -0
  112. package/docs/05-sandbox.md +159 -0
  113. package/docs/06-idempotency.md +132 -0
  114. package/docs/examples/express-webhook-receiver.md +97 -0
  115. package/docs/examples/nextjs-route-handler.md +206 -0
  116. package/docs/examples/sandbox-vitest-suite.md +263 -0
  117. package/docs/index.md +66 -0
  118. package/docs/reference/client.md +392 -0
  119. package/docs/reference/errors.md +161 -0
  120. package/docs/reference/types.md +400 -0
  121. package/package.json +53 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Multi-chain address-verification dispatcher consumed by TxnodClient.createInvoice.
3
+ * Routes the invoice's coin → chain → per-chain verifier with multi-xpub
4
+ * iteration (newest-first per AD-10) and selective error rethrow.
5
+ */
6
+ import type { Chain, InvoiceResponse, WalletEdgeKind } from '../_shared/index.js';
7
+ import type { TonXpubConfig } from './config.js';
8
+ /**
9
+ * Input shape for {@link verifyAddress}. The `invoice` field is a `Pick` of
10
+ * the fields actually consumed (`coin`, `address`, `derivation_path`, plus
11
+ * optional `id` for log correlation), so test fixtures are trivial.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { verifyAddress, type VerifyAddressInput } from '@txnod/sdk';
16
+ *
17
+ * const input: VerifyAddressInput = {
18
+ * invoice: {
19
+ * id: '01HK8MAR2QEXAMPLE000000000',
20
+ * coin: 'btc',
21
+ * address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
22
+ * derivation_path: "m/84'/0'/0'/0/0",
23
+ * },
24
+ * config: {
25
+ * btc: ['zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'],
26
+ * eth: [],
27
+ * tron: [],
28
+ * ada: [],
29
+ * polygon: [],
30
+ * bsc: [],
31
+ * },
32
+ * };
33
+ * verifyAddress(input);
34
+ * ```
35
+ */
36
+ export interface VerifyAddressInput {
37
+ invoice: Pick<InvoiceResponse, 'coin' | 'address' | 'derivation_path'> & {
38
+ id?: string;
39
+ /**
40
+ * Optional explicit project kind override. The invoice envelope no longer
41
+ * carries a `network` discriminator — kind is implied from the project a
42
+ * given API key is bound to. Pass `kind` here only if the partner is
43
+ * verifying a TON invoice and needs to assert the user-friendly address
44
+ * encoding flag. Defaults to `'production'`.
45
+ */
46
+ kind?: WalletEdgeKind;
47
+ };
48
+ config: Record<Chain, string[]>;
49
+ /**
50
+ * Optional TON-specific config. When the invoice's chain resolves to `ton`
51
+ * and this is set, the SDK calls {@link verifyTonAddress} automatically.
52
+ * When `undefined`, TON verification is skipped silently — same behaviour as
53
+ * an HD chain with no `TXNOD_<chain>_XPUB` configured. Populate via
54
+ * {@link parseTonConfig} (env-driven).
55
+ */
56
+ tonConfig?: TonXpubConfig;
57
+ }
58
+ /**
59
+ * Result type for {@link verifyAddress}. The function returns void on success
60
+ * or skip and throws on mismatch — this alias documents the void contract.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { verifyAddress, type VerificationResult } from '@txnod/sdk';
65
+ *
66
+ * const result: VerificationResult = verifyAddress({
67
+ * invoice: {
68
+ * coin: 'btc',
69
+ * address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
70
+ * derivation_path: "m/84'/0'/0'/0/0",
71
+ * },
72
+ * config: { btc: [], eth: [], tron: [], ada: [], polygon: [], bsc: [] },
73
+ * });
74
+ * console.log(result); // undefined
75
+ * ```
76
+ */
77
+ export type VerificationResult = void;
78
+ /**
79
+ * Verify that the API-returned invoice address derives from one of the
80
+ * partner's configured xpubs for the invoice's chain. Returns void on
81
+ * success, on edge case (a) (missing `derivation_path` → log+skip), or on
82
+ * edge case (b) (no xpub configured → silent skip). Throws
83
+ * `AddressVerificationError` when all configured xpubs fail to derive the
84
+ * address. Rethrows `TxnodInvalidXpubFormatError` (and other non-
85
+ * `AddressVerificationError`) immediately — a malformed xpub is a config
86
+ * bug, not a "try the next one" signal.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { verifyAddress, AddressVerificationError } from '@txnod/sdk';
91
+ *
92
+ * try {
93
+ * verifyAddress({
94
+ * invoice: {
95
+ * id: '01HK8MAR2QEXAMPLE000000000',
96
+ * coin: 'btc',
97
+ * address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
98
+ * derivation_path: "m/84'/0'/0'/0/0",
99
+ * },
100
+ * config: {
101
+ * btc: ['zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'],
102
+ * eth: [],
103
+ * tron: [],
104
+ * ada: [],
105
+ * polygon: [],
106
+ * bsc: [],
107
+ * },
108
+ * });
109
+ * } catch (err) {
110
+ * if (err instanceof AddressVerificationError) {
111
+ * console.error('verify failed', err.chain, err.expected_address, err.derived_address);
112
+ * } else {
113
+ * throw err;
114
+ * }
115
+ * }
116
+ * ```
117
+ */
118
+ export declare function verifyAddress(input: VerifyAddressInput): VerificationResult;
119
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/verify/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAQ,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAUlF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqBjD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,CAAC,GAAG;QACvE,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ;;;;;;WAMG;QACH,IAAI,CAAC,EAAE,cAAc,CAAC;KACvB,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAqCtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CA+D3E"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Multi-chain address-verification dispatcher consumed by TxnodClient.createInvoice.
3
+ * Routes the invoice's coin → chain → per-chain verifier with multi-xpub
4
+ * iteration (newest-first per AD-10) and selective error rethrow.
5
+ */
6
+ import { logger } from '../internals/logger.js';
7
+ import { AddressVerificationError } from './errors.js';
8
+ import { verifyBtc } from './chains/btc.js';
9
+ import { verifyEvm } from './chains/evm.js';
10
+ import { verifyTron } from './chains/tron.js';
11
+ import { verifyCardano } from './chains/cardano.js';
12
+ import { verifyPolygon } from './chains/polygon.js';
13
+ import { verifyBsc } from './chains/bsc.js';
14
+ import { verifyTonAddress } from './chains/ton.js';
15
+ // Mirrors packages/shared/src/schemas/coin.ts COIN_TO_CHAIN — drift is coupled with the shared table.
16
+ const COIN_TO_CHAIN_LITERAL = {
17
+ btc: 'btc',
18
+ eth: 'eth',
19
+ usdt_erc20: 'eth',
20
+ usdc_erc20: 'eth',
21
+ trx: 'tron',
22
+ usdt_trc20: 'tron',
23
+ ada: 'ada',
24
+ pol: 'polygon',
25
+ usdt_polygon: 'polygon',
26
+ usdc_polygon: 'polygon',
27
+ bnb: 'bsc',
28
+ usdt_bep20: 'bsc',
29
+ usdc_bep20: 'bsc',
30
+ ton: 'ton',
31
+ usdt_ton: 'ton',
32
+ };
33
+ function dispatchByChain(chain, xpub, derivation_path, expected_address) {
34
+ switch (chain) {
35
+ case 'btc':
36
+ verifyBtc({ xpub, derivation_path, expected_address });
37
+ return;
38
+ case 'eth':
39
+ verifyEvm({ xpub, derivation_path, expected_address, chain: 'eth' });
40
+ return;
41
+ case 'tron':
42
+ verifyTron({ xpub, derivation_path, expected_address });
43
+ return;
44
+ case 'ada':
45
+ verifyCardano({ xpub, derivation_path, expected_address });
46
+ return;
47
+ case 'polygon':
48
+ verifyPolygon({ xpub, derivation_path, expected_address });
49
+ return;
50
+ case 'bsc':
51
+ verifyBsc({ xpub, derivation_path, expected_address });
52
+ return;
53
+ case 'ton':
54
+ // Unreachable — TON is handled in verifyAddress() before dispatchByChain.
55
+ return;
56
+ default: {
57
+ const _exhaustive = chain;
58
+ throw new Error(`unreachable chain: ${_exhaustive}`);
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Verify that the API-returned invoice address derives from one of the
64
+ * partner's configured xpubs for the invoice's chain. Returns void on
65
+ * success, on edge case (a) (missing `derivation_path` → log+skip), or on
66
+ * edge case (b) (no xpub configured → silent skip). Throws
67
+ * `AddressVerificationError` when all configured xpubs fail to derive the
68
+ * address. Rethrows `TxnodInvalidXpubFormatError` (and other non-
69
+ * `AddressVerificationError`) immediately — a malformed xpub is a config
70
+ * bug, not a "try the next one" signal.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * import { verifyAddress, AddressVerificationError } from '@txnod/sdk';
75
+ *
76
+ * try {
77
+ * verifyAddress({
78
+ * invoice: {
79
+ * id: '01HK8MAR2QEXAMPLE000000000',
80
+ * coin: 'btc',
81
+ * address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
82
+ * derivation_path: "m/84'/0'/0'/0/0",
83
+ * },
84
+ * config: {
85
+ * btc: ['zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'],
86
+ * eth: [],
87
+ * tron: [],
88
+ * ada: [],
89
+ * polygon: [],
90
+ * bsc: [],
91
+ * },
92
+ * });
93
+ * } catch (err) {
94
+ * if (err instanceof AddressVerificationError) {
95
+ * console.error('verify failed', err.chain, err.expected_address, err.derived_address);
96
+ * } else {
97
+ * throw err;
98
+ * }
99
+ * }
100
+ * ```
101
+ */
102
+ export function verifyAddress(input) {
103
+ const { invoice, config, tonConfig } = input;
104
+ const chain = COIN_TO_CHAIN_LITERAL[invoice.coin];
105
+ // TON has no xpub/derivation — operator pubkey + wallet metadata are
106
+ // supplied via TonXpubConfig (TXNOD_TON_PUBKEY + siblings). When unset, the
107
+ // SDK silently skips verification (parity with omitting TXNOD_<chain>_XPUB
108
+ // for HD chains); `createInvoice` still surfaces a single warn so partners
109
+ // are not silently relying on server trust.
110
+ if (chain === 'ton') {
111
+ if (tonConfig === undefined) {
112
+ logger.warn({
113
+ msg: 'SDK verify: TON address verification skipped — set TXNOD_TON_PUBKEY to enable',
114
+ invoice_id: invoice.id,
115
+ });
116
+ return;
117
+ }
118
+ const kind = invoice.kind ?? 'production';
119
+ try {
120
+ verifyTonAddress({
121
+ chain: 'ton',
122
+ publicKeyHex: tonConfig.publicKeyHex,
123
+ walletVersion: tonConfig.walletVersion,
124
+ subwalletId: tonConfig.subwalletId,
125
+ workchain: tonConfig.workchain,
126
+ kind,
127
+ expectedAddress: invoice.address,
128
+ });
129
+ }
130
+ catch (err) {
131
+ if (err instanceof AddressVerificationError)
132
+ throw err;
133
+ throw err;
134
+ }
135
+ return;
136
+ }
137
+ const xpubs = config[chain] ?? [];
138
+ if (xpubs.length === 0)
139
+ return;
140
+ if (!invoice.derivation_path) {
141
+ logger.warn({
142
+ msg: 'SDK verify: derivation_path absent on response; skipping verification',
143
+ chain,
144
+ invoice_id: invoice.id,
145
+ });
146
+ return;
147
+ }
148
+ for (const xpub of xpubs) {
149
+ try {
150
+ dispatchByChain(chain, xpub, invoice.derivation_path, invoice.address);
151
+ return;
152
+ }
153
+ catch (err) {
154
+ if (!(err instanceof AddressVerificationError))
155
+ throw err;
156
+ }
157
+ }
158
+ // Sentinel: count of attempted xpubs is encoded in derived_address to keep AddressVerificationError shape unchanged.
159
+ throw new AddressVerificationError({
160
+ chain,
161
+ derivation_path: invoice.derivation_path,
162
+ expected_address: invoice.address,
163
+ derived_address: `<no match across ${xpubs.length} configured xpub${xpubs.length === 1 ? '' : 's'} for chain '${chain}'>`,
164
+ });
165
+ }
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/verify/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { Chain } from '../_shared/index.js';
2
+ /**
3
+ * Returns the 4-char prefix of an xpub-shaped string. Used by the xpub-prefix
4
+ * guard to detect testnet-shape xpubs (`tpub`/`vpub`/`upub`) configured for
5
+ * production environments.
6
+ */
7
+ export declare function detectXpubPrefix(xpub: string): string;
8
+ /**
9
+ * Synchronous guard called from `TxnodClient.constructor` and
10
+ * `refreshXpubConfig()`. Iterates every chain × xpub in `xpubConfig`; throws
11
+ * `TxnodSandboxXpubInProductionError` for the first testnet-shape prefix
12
+ * found when `env === 'production'`.
13
+ *
14
+ * Carve-outs (no-ops, by spec §7.2 layer 3 + §4.6):
15
+ * - **Cardano (`ada`)**: CIP-5 hrp does not distinguish testnet at the
16
+ * account-pubkey level. Network safety lives at the address-level
17
+ * NetworkId byte, enforced server-side by the chain watcher and at the
18
+ * SDK boundary by `verify/chains/cardano.ts`. The xpub prefix is
19
+ * structurally non-discriminating for ADA.
20
+ * - **TON (`ton`)**: there is no xpub concept — `TXNOD_TON_PUBKEY` is a
21
+ * 32-byte ed25519 public key, never an extended key. The guard does not
22
+ * inspect `TonXpubConfig` at all (that's why the function takes only
23
+ * `xpubConfig`).
24
+ * - **EVM-family `xpub` ambiguity (`eth`/`polygon`/`bsc`)**: mainnet and
25
+ * testnet on EVM chains both commonly use `xpub`-prefixed extended keys;
26
+ * network differentiation lives in the per-address checksum and the chain
27
+ * id, not in the xpub prefix. The guard ONLY catches `tpub`/`vpub`/`upub`
28
+ * — an `xpub` configured for a testnet EVM chain falls through to the
29
+ * server-side cross-mode check (§7.2 layer 4) and the webhook envelope
30
+ * `mode` discriminator (§7.2 layer 5).
31
+ */
32
+ export declare function assertNoTestnetXpubsInProduction(xpubConfig: Record<Chain, string[]>, env: 'production' | 'non-production' | 'unknown'): void;
33
+ //# sourceMappingURL=xpub-safety.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xpub-safety.d.ts","sourceRoot":"","sources":["../../src/verify/xpub-safety.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAM3C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EACnC,GAAG,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,GAC/C,IAAI,CAYN"}
@@ -0,0 +1,54 @@
1
+ import { TxnodSandboxXpubInProductionError } from '../errors.js';
2
+ const TESTNET_XPUB_PREFIXES = ['tpub', 'vpub', 'upub'];
3
+ const PREFIX_LENGTH = 4;
4
+ /**
5
+ * Returns the 4-char prefix of an xpub-shaped string. Used by the xpub-prefix
6
+ * guard to detect testnet-shape xpubs (`tpub`/`vpub`/`upub`) configured for
7
+ * production environments.
8
+ */
9
+ export function detectXpubPrefix(xpub) {
10
+ return xpub.slice(0, PREFIX_LENGTH);
11
+ }
12
+ function isTestnetPrefix(prefix) {
13
+ return TESTNET_XPUB_PREFIXES.includes(prefix);
14
+ }
15
+ /**
16
+ * Synchronous guard called from `TxnodClient.constructor` and
17
+ * `refreshXpubConfig()`. Iterates every chain × xpub in `xpubConfig`; throws
18
+ * `TxnodSandboxXpubInProductionError` for the first testnet-shape prefix
19
+ * found when `env === 'production'`.
20
+ *
21
+ * Carve-outs (no-ops, by spec §7.2 layer 3 + §4.6):
22
+ * - **Cardano (`ada`)**: CIP-5 hrp does not distinguish testnet at the
23
+ * account-pubkey level. Network safety lives at the address-level
24
+ * NetworkId byte, enforced server-side by the chain watcher and at the
25
+ * SDK boundary by `verify/chains/cardano.ts`. The xpub prefix is
26
+ * structurally non-discriminating for ADA.
27
+ * - **TON (`ton`)**: there is no xpub concept — `TXNOD_TON_PUBKEY` is a
28
+ * 32-byte ed25519 public key, never an extended key. The guard does not
29
+ * inspect `TonXpubConfig` at all (that's why the function takes only
30
+ * `xpubConfig`).
31
+ * - **EVM-family `xpub` ambiguity (`eth`/`polygon`/`bsc`)**: mainnet and
32
+ * testnet on EVM chains both commonly use `xpub`-prefixed extended keys;
33
+ * network differentiation lives in the per-address checksum and the chain
34
+ * id, not in the xpub prefix. The guard ONLY catches `tpub`/`vpub`/`upub`
35
+ * — an `xpub` configured for a testnet EVM chain falls through to the
36
+ * server-side cross-mode check (§7.2 layer 4) and the webhook envelope
37
+ * `mode` discriminator (§7.2 layer 5).
38
+ */
39
+ export function assertNoTestnetXpubsInProduction(xpubConfig, env) {
40
+ if (env !== 'production')
41
+ return;
42
+ for (const chain of Object.keys(xpubConfig)) {
43
+ if (chain === 'ada' || chain === 'ton')
44
+ continue;
45
+ const xpubs = xpubConfig[chain] ?? [];
46
+ for (const xpub of xpubs) {
47
+ const prefix = detectXpubPrefix(xpub);
48
+ if (isTestnetPrefix(prefix)) {
49
+ throw new TxnodSandboxXpubInProductionError(chain, prefix);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ //# sourceMappingURL=xpub-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xpub-safety.js","sourceRoot":"","sources":["../../src/verify/xpub-safety.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iCAAiC,EAAE,MAAM,cAAc,CAAC;AAEjE,MAAM,qBAAqB,GAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9E,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,gCAAgC,CAC9C,UAAmC,EACnC,GAAgD;IAEhD,IAAI,GAAG,KAAK,YAAY;QAAE,OAAO;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAY,EAAE,CAAC;QACvD,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK;YAAE,SAAS;QACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,iCAAiC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { WebhookEvent } from './_shared/index.js';
2
+ /**
3
+ * Verifies an inbound webhook signature and returns a typed event. Parses
4
+ * the `X-Txnod-Signature: t=<unix>,v1=<hex>` header, recomputes HMAC-SHA256
5
+ * over `${timestamp}.${rawBody}` using the project secret, compares in
6
+ * constant time, and enforces a ±300s timestamp window. Throws typed errors
7
+ * on any failure.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { verifyWebhookSignature, TxnodHmacError, TxnodTimestampError } from '@txnod/sdk';
12
+ *
13
+ * export async function POST(req: Request) {
14
+ * const rawBody = await req.text();
15
+ * try {
16
+ * const event = verifyWebhookSignature(req.headers, rawBody, process.env.TXNOD_WEBHOOK_SECRET!);
17
+ * if (event.event_type === 'invoice.paid') {
18
+ * console.log('paid', event.event_id);
19
+ * }
20
+ * return Response.json({ ok: true });
21
+ * } catch (err) {
22
+ * if (err instanceof TxnodHmacError) return new Response('bad sig', { status: 401 });
23
+ * if (err instanceof TxnodTimestampError) return new Response('stale', { status: 401 });
24
+ * throw err;
25
+ * }
26
+ * }
27
+ * ```
28
+ */
29
+ export declare function verifyWebhookSignature(headers: Headers | Record<string, string> | Record<string, string[]>, rawBody: string, secret: string): WebhookEvent;
30
+ //# sourceMappingURL=verify-webhook-signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-webhook-signature.d.ts","sourceRoot":"","sources":["../src/verify-webhook-signature.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA8BlD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACpE,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,YAAY,CAqCd"}
@@ -0,0 +1,84 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { TxnodHmacError, TxnodSignatureFormatError, TxnodTimestampError, TxnodWebhookPayloadParseError, } from './errors.js';
3
+ const TIMESTAMP_WINDOW_SECONDS = 300;
4
+ const DIGEST_BYTES = 32;
5
+ const SIGNATURE_HEADER = 'x-txnod-signature';
6
+ const HEADER_PATTERN = /^t=(\d+),v1=([0-9a-fA-F]+)$/;
7
+ function getHeader(headers, name) {
8
+ if (headers instanceof Headers) {
9
+ return headers.get(name) ?? undefined;
10
+ }
11
+ for (const [key, value] of Object.entries(headers)) {
12
+ if (key.toLowerCase() !== name)
13
+ continue;
14
+ if (Array.isArray(value)) {
15
+ return value[0];
16
+ }
17
+ return value;
18
+ }
19
+ return undefined;
20
+ }
21
+ /**
22
+ * Verifies an inbound webhook signature and returns a typed event. Parses
23
+ * the `X-Txnod-Signature: t=<unix>,v1=<hex>` header, recomputes HMAC-SHA256
24
+ * over `${timestamp}.${rawBody}` using the project secret, compares in
25
+ * constant time, and enforces a ±300s timestamp window. Throws typed errors
26
+ * on any failure.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { verifyWebhookSignature, TxnodHmacError, TxnodTimestampError } from '@txnod/sdk';
31
+ *
32
+ * export async function POST(req: Request) {
33
+ * const rawBody = await req.text();
34
+ * try {
35
+ * const event = verifyWebhookSignature(req.headers, rawBody, process.env.TXNOD_WEBHOOK_SECRET!);
36
+ * if (event.event_type === 'invoice.paid') {
37
+ * console.log('paid', event.event_id);
38
+ * }
39
+ * return Response.json({ ok: true });
40
+ * } catch (err) {
41
+ * if (err instanceof TxnodHmacError) return new Response('bad sig', { status: 401 });
42
+ * if (err instanceof TxnodTimestampError) return new Response('stale', { status: 401 });
43
+ * throw err;
44
+ * }
45
+ * }
46
+ * ```
47
+ */
48
+ export function verifyWebhookSignature(headers, rawBody, secret) {
49
+ const raw = getHeader(headers, SIGNATURE_HEADER);
50
+ if (raw === undefined) {
51
+ throw new TxnodSignatureFormatError();
52
+ }
53
+ const match = HEADER_PATTERN.exec(raw);
54
+ if (match === null) {
55
+ throw new TxnodSignatureFormatError();
56
+ }
57
+ const timestampStr = match[1];
58
+ const providedHex = match[2];
59
+ if (providedHex.length !== DIGEST_BYTES * 2) {
60
+ throw new TxnodSignatureFormatError();
61
+ }
62
+ const timestamp = Number(timestampStr);
63
+ const nowSec = Math.floor(Date.now() / 1000);
64
+ const skewSeconds = nowSec - timestamp;
65
+ if (Math.abs(skewSeconds) > TIMESTAMP_WINDOW_SECONDS) {
66
+ throw new TxnodTimestampError(skewSeconds);
67
+ }
68
+ const expected = createHmac('sha256', secret)
69
+ .update(`${timestamp}.${rawBody}`)
70
+ .digest();
71
+ // `provided` is structurally `DIGEST_BYTES` long: HEADER_PATTERN + the
72
+ // `providedHex.length === DIGEST_BYTES * 2` check above guarantee 64 hex
73
+ // chars in, which `Buffer.from(_, 'hex')` decodes to exactly 32 bytes.
74
+ if (!timingSafeEqual(Buffer.from(providedHex, 'hex'), expected)) {
75
+ throw new TxnodHmacError();
76
+ }
77
+ try {
78
+ return JSON.parse(rawBody);
79
+ }
80
+ catch (cause) {
81
+ throw new TxnodWebhookPayloadParseError(cause);
82
+ }
83
+ }
84
+ //# sourceMappingURL=verify-webhook-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-webhook-signature.js","sourceRoot":"","sources":["../src/verify-webhook-signature.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EACL,cAAc,EACd,yBAAyB,EACzB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,aAAa,CAAC;AAErB,MAAM,wBAAwB,GAAG,GAAG,CAAC;AACrC,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAC7C,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD,SAAS,SAAS,CAChB,OAAoE,EACpE,IAAY;IAEZ,IAAI,OAAO,YAAY,OAAO,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;IACxC,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI;YAAE,SAAS;QACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAoE,EACpE,OAAe,EACf,MAAc;IAEd,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IACjD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,yBAAyB,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,yBAAyB,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IAC/B,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IAC9B,IAAI,WAAW,CAAC,MAAM,KAAK,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,yBAAyB,EAAE,CAAC;IACxC,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC;IACvC,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,wBAAwB,EAAE,CAAC;QACrD,MAAM,IAAI,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC1C,MAAM,CAAC,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC;SACjC,MAAM,EAAE,CAAC;IACZ,uEAAuE;IACvE,yEAAyE;IACzE,uEAAuE;IACvE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,cAAc,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiB,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,6BAA6B,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;AACH,CAAC"}
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: "Getting started"
3
+ description: "Install @txnod/sdk, configure env vars, create the first invoice, verify the first webhook."
4
+ sdk_version: 1.0.0
5
+ ---
6
+
7
+ # Getting started
8
+
9
+ ## Runtime requirements
10
+
11
+ - Node ≥ 20 (the SDK uses `node:crypto`, `globalThis.fetch`, and the `Headers` Web API).
12
+ - Server-side only. Do not import `@txnod/sdk` into browser bundles — the `apiSecret` must never leave the server.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @txnod/sdk
18
+ # or
19
+ pnpm add @txnod/sdk
20
+ # or
21
+ yarn add @txnod/sdk
22
+ # or
23
+ bun add @txnod/sdk
24
+ ```
25
+
26
+ The tarball ships **zero dependencies** — runtime or type-level. The compiled JS has no external imports beyond `node:crypto`. Published `.d.ts` types are fully expanded to plain TypeScript at build time (no `zod` type constructs, no `@txnod/*` references), so the SDK coexists with any version of zod your project may already use — or none at all. Bundle size is enforced in CI.
27
+
28
+ ## Configure environment variables
29
+
30
+ ```bash
31
+ # .env.local (never commit)
32
+ TXNOD_PROJECT_ID=01JXXXXXXXXXXXXXXXXXXXXXXX
33
+ TXNOD_API_SECRET=<64-hex-character secret>
34
+ TXNOD_WEBHOOK_SECRET=<same value as TXNOD_API_SECRET>
35
+
36
+ # Optional — omit unless targeting a non-default endpoint.
37
+ # TXNOD_BASE_URL=https://txnod.com
38
+ ```
39
+
40
+ - `TXNOD_PROJECT_ID` is a ULID surfaced when the operator creates a project in the dashboard.
41
+ - `TXNOD_API_SECRET` is shown **exactly once** on key generation. Copy into secrets manager immediately; the server stores only a hash.
42
+ - `TXNOD_WEBHOOK_SECRET` intentionally equals `TXNOD_API_SECRET`. Two names let callers wire distinct env vars in their framework without re-deriving the relationship.
43
+ - `TXNOD_BASE_URL` is optional and defaults to `https://txnod.com`. Set it to point the SDK at a self-hosted txnod-compatible API (for example when someone runs the project under a different domain) or at a staging instance. The HMAC scheme and all endpoints are endpoint-agnostic — only the origin changes.
44
+
45
+ See [`01-authentication.md`](./01-authentication.md) for how these values are consumed.
46
+
47
+ ## First invoice
48
+
49
+ ```ts
50
+ import { TxnodClient } from '@txnod/sdk';
51
+
52
+ const client = new TxnodClient({
53
+ projectId: process.env.TXNOD_PROJECT_ID!,
54
+ apiSecret: process.env.TXNOD_API_SECRET!,
55
+ // Omit baseUrl (defaults to https://txnod.com) unless you are pointing at
56
+ // a self-hosted instance or a staging environment. Passing process.env
57
+ // conditionally keeps prod and dev wiring in the same file:
58
+ ...(process.env.TXNOD_BASE_URL ? { baseUrl: process.env.TXNOD_BASE_URL } : {}),
59
+ });
60
+
61
+ const invoice = await client.createInvoice({
62
+ amount_usd: 9.99,
63
+ coin: 'usdt_trc20',
64
+ external_id: 'order-42',
65
+ callback_url: 'https://your-site.com/api/txnod-webhook',
66
+ });
67
+
68
+ // Show these to the payer:
69
+ // invoice.address — deposit address for this invoice
70
+ // invoice.amount_crypto — decimal string, e.g. "9.990000"
71
+ // invoice.amount_crypto_units — integer string in base units (sats, wei, ...)
72
+ // invoice.payment_uri — BIP-21 / EIP-681 / Cardano URI for QR generation
73
+ // invoice.expires_at_iso — invoice auto-expires at this ISO-8601 UTC timestamp
74
+ ```
75
+
76
+ `external_id` is your side's idempotency key — see [`06-idempotency.md`](./06-idempotency.md). `coin` must be one of the 15 supported coins listed in [`reference/types.md`](./reference/types.md#coin).
77
+
78
+ For staging or development, ask the operator to spin up a separate `kind='testnet'` project (dashboard → New project → Testnet). The SDK code path is identical — partner code talks to the testnet project's API key and the SDK accepts testnet-prefix xpubs (`tpub`/`vpub`/`zpub`, `addr_test1...` for Cardano) for address verification automatically. See [`02-invoices.md`](./02-invoices.md#testnet) for the canonical testnet setup.
79
+
80
+ For deterministic integration tests, the SDK ships a sandbox surface (`client.sandbox.*`) that walks the invoice state machine without touching a chain. See [`05-sandbox.md`](./05-sandbox.md) for the full surface, environment-detection guards, and the [`examples/sandbox-vitest-suite.md`](./examples/sandbox-vitest-suite.md) end-to-end harness.
81
+
82
+ ## First verified webhook
83
+
84
+ ```ts
85
+ // Next.js 16 App Router — app/api/txnod-webhook/route.ts
86
+ import {
87
+ verifyWebhookSignature,
88
+ TxnodHmacError,
89
+ TxnodTimestampError,
90
+ } from '@txnod/sdk';
91
+
92
+ export async function POST(request: Request): Promise<Response> {
93
+ const rawBody = await request.text(); // must be the exact bytes, pre-parsing
94
+ try {
95
+ const event = verifyWebhookSignature(
96
+ request.headers,
97
+ rawBody,
98
+ process.env.TXNOD_WEBHOOK_SECRET!,
99
+ );
100
+ if (event.event_type === 'invoice.paid') {
101
+ // event is narrowed to the paid variant. See 04-webhooks.md for the discriminants.
102
+ }
103
+ return Response.json({ ok: true });
104
+ } catch (err) {
105
+ if (err instanceof TxnodHmacError) return new Response('bad sig', { status: 401 });
106
+ if (err instanceof TxnodTimestampError) return new Response('stale', { status: 401 });
107
+ throw err;
108
+ }
109
+ }
110
+ ```
111
+
112
+ **Critical:** do not parse the body before signing verification. HMAC is computed over the raw bytes as sent; any middleware that re-serializes JSON breaks the signature.
113
+
114
+ ## Self-hosted or alternative-origin deployments
115
+
116
+ `TxnodClient` accepts `baseUrl?: string` explicitly, which always wins over `TXNOD_BASE_URL`. The SDK's HMAC scheme, retry logic, and `verifyWebhookSignature` helper are endpoint-agnostic — they work unchanged against any server that implements the TxNod API contract. Typical uses:
117
+
118
+ - **Default:** omit `baseUrl`. The SDK targets `https://txnod.com`.
119
+ - **Staging:** set `TXNOD_BASE_URL=https://staging.txnod.com` in the staging environment only.
120
+ - **Self-hosted TxNod:** if someone operates their own non-custodial txnod-compatible gateway, their partners point the SDK at that origin the same way (`baseUrl: 'https://pay.mycompany.com'`). No code changes, no rebuild — the SDK is origin-neutral.
121
+
122
+ ```ts
123
+ const client = new TxnodClient({
124
+ projectId: process.env.TXNOD_PROJECT_ID!,
125
+ apiSecret: process.env.TXNOD_API_SECRET!,
126
+ baseUrl: 'https://pay.mycompany.com', // trailing slash optional; SDK normalises it.
127
+ });
128
+ ```
129
+
130
+ ## Where to go next
131
+
132
+ - To understand the HMAC scheme and headers → [`01-authentication.md`](./01-authentication.md).
133
+ - To branch on every invoice status and error_code → [`02-invoices.md`](./02-invoices.md) and [`05-errors.md`](./05-errors.md).
134
+ - To read each webhook event shape → [`04-webhooks.md`](./04-webhooks.md).
135
+ - Full working code → [`examples/nextjs-route-handler.md`](./examples/nextjs-route-handler.md).