@profullstack/coinpay 0.6.11 → 0.8.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.
- package/README.md +64 -0
- package/bin/coinpay.js +946 -59
- package/package.json +20 -3
- package/src/auth.js +2 -1
- package/src/client.d.ts +52 -0
- package/src/client.js +53 -7
- package/src/index.d.ts +34 -1
- package/src/index.js +32 -0
- package/src/invoices.d.ts +77 -0
- package/src/invoices.js +237 -0
- package/src/tokens.d.ts +14 -0
- package/src/tokens.js +34 -0
- package/src/wallet.d.ts +11 -0
- package/src/wallet.js +229 -1
- package/src/webhooks.js +37 -3
- package/src/x402-browser.js +318 -0
- package/src/x402-pay-server.js +272 -0
- package/src/x402-v2.js +416 -0
- package/src/x402.js +164 -19
package/src/tokens.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CoinPayClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List checkout tokens for a business.
|
|
5
|
+
* @param {Object} params
|
|
6
|
+
* @param {string} [params.apiKey] - API key if not using client
|
|
7
|
+
* @param {CoinPayClient} [params.client] - Existing client
|
|
8
|
+
* @param {string} [params.businessId] - Business ID
|
|
9
|
+
* @param {boolean} [params.activeOnly] - Only return active wallets
|
|
10
|
+
* @returns {Promise<Object>} Tokens response
|
|
11
|
+
*/
|
|
12
|
+
export async function getTokens({ apiKey, client, businessId, activeOnly } = {}) {
|
|
13
|
+
const coinpay = client || new CoinPayClient({ apiKey });
|
|
14
|
+
return coinpay.getTokens({ businessId, activeOnly });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List supported coins for a business.
|
|
19
|
+
* @param {Object} params
|
|
20
|
+
* @param {string} [params.apiKey] - API key if not using client
|
|
21
|
+
* @param {CoinPayClient} [params.client] - Existing client
|
|
22
|
+
* @param {string} [params.businessId] - Business ID
|
|
23
|
+
* @param {boolean} [params.activeOnly] - Only return active wallets
|
|
24
|
+
* @returns {Promise<Object>} Supported coins response
|
|
25
|
+
*/
|
|
26
|
+
export async function getSupportedCoins({ apiKey, client, businessId, activeOnly } = {}) {
|
|
27
|
+
const coinpay = client || new CoinPayClient({ apiKey });
|
|
28
|
+
return coinpay.getSupportedCoins({ businessId, activeOnly });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
getTokens,
|
|
33
|
+
getSupportedCoins,
|
|
34
|
+
};
|
package/src/wallet.d.ts
CHANGED
|
@@ -251,6 +251,17 @@ export declare function validateMnemonic(mnemonic: string): boolean;
|
|
|
251
251
|
*/
|
|
252
252
|
export declare function getDerivationPath(chain: string, index?: number): string;
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Derive the address for a chain from a raw BIP-39 seed.
|
|
256
|
+
* (Standalone counterpart to `WalletClient.deriveAddress`; exported from
|
|
257
|
+
* wallet.js but previously missing from these type declarations.)
|
|
258
|
+
*
|
|
259
|
+
* @param seed - Raw seed bytes, e.g. bip39.mnemonicToSeedSync(mnemonic)
|
|
260
|
+
* @param chain - Chain id (e.g. 'BTC', 'ETH', 'SOL', 'POL', 'BCH')
|
|
261
|
+
* @param index - Address index (default 0)
|
|
262
|
+
*/
|
|
263
|
+
export declare function deriveAddress(seed: Uint8Array, chain: string, index?: number): string;
|
|
264
|
+
|
|
254
265
|
/**
|
|
255
266
|
* Restore a seed from encrypted backup
|
|
256
267
|
*/
|
package/src/wallet.js
CHANGED
|
@@ -33,6 +33,7 @@ export const WalletChain = {
|
|
|
33
33
|
USDC_ETH: 'USDC_ETH',
|
|
34
34
|
USDC_POL: 'USDC_POL',
|
|
35
35
|
USDC_SOL: 'USDC_SOL',
|
|
36
|
+
USDC_BASE: 'USDC_BASE',
|
|
36
37
|
USDT_ETH: 'USDT_ETH',
|
|
37
38
|
USDT_POL: 'USDT_POL',
|
|
38
39
|
USDT_SOL: 'USDT_SOL',
|
|
@@ -56,6 +57,7 @@ const COIN_TYPES = {
|
|
|
56
57
|
USDC_ETH: 60,
|
|
57
58
|
USDC_POL: 60,
|
|
58
59
|
USDC_SOL: 501,
|
|
60
|
+
USDC_BASE: 60, // Base is EVM, uses ETH path — same address as USDC_ETH
|
|
59
61
|
USDT_ETH: 60,
|
|
60
62
|
USDT_POL: 60,
|
|
61
63
|
USDT_SOL: 501,
|
|
@@ -64,7 +66,7 @@ const COIN_TYPES = {
|
|
|
64
66
|
/**
|
|
65
67
|
* Chains that use secp256k1 curve (vs ed25519 for Solana)
|
|
66
68
|
*/
|
|
67
|
-
const SECP256K1_CHAINS = ['BTC', 'BCH', 'ETH', 'POL', 'BNB', 'USDC_ETH', 'USDC_POL', 'USDT_ETH', 'USDT_POL'];
|
|
69
|
+
const SECP256K1_CHAINS = ['BTC', 'BCH', 'ETH', 'POL', 'BNB', 'USDC_ETH', 'USDC_POL', 'USDC_BASE', 'USDT_ETH', 'USDT_POL'];
|
|
68
70
|
|
|
69
71
|
// ============================================================
|
|
70
72
|
// Address derivation helpers
|
|
@@ -305,6 +307,7 @@ export function deriveAddress(seed, chain, index = 0) {
|
|
|
305
307
|
case 'BNB':
|
|
306
308
|
case 'USDC_ETH':
|
|
307
309
|
case 'USDC_POL':
|
|
310
|
+
case 'USDC_BASE':
|
|
308
311
|
case 'USDT_ETH':
|
|
309
312
|
case 'USDT_POL':
|
|
310
313
|
return deriveETHAddress(publicKey);
|
|
@@ -419,6 +422,160 @@ function signMessage(message, privateKey) {
|
|
|
419
422
|
return bytesToHex(signature.toCompactRawBytes());
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
// ================================================================
|
|
426
|
+
// Arbitrary tx / message signing
|
|
427
|
+
// ----------------------------------------------------------------
|
|
428
|
+
// These primitives let callers (like b1dz.com's DEX engine) sign
|
|
429
|
+
// EVM contract calls and Solana transactions that CoinPay's built-in
|
|
430
|
+
// `send` flow doesn't cover (e.g., Uniswap swaps, Jupiter routes).
|
|
431
|
+
//
|
|
432
|
+
// The API intentionally stops at the cryptographic primitive:
|
|
433
|
+
// - EVM: caller builds the unsigned tx with their own library
|
|
434
|
+
// (viem / ethers / ethers-v5), hashes it per EIP-1559 or
|
|
435
|
+
// EIP-155, hands us the 32-byte digest, gets back a
|
|
436
|
+
// recoverable signature
|
|
437
|
+
// - Solana: caller builds the unsigned Transaction / VersionedMessage,
|
|
438
|
+
// hands us the raw message bytes, gets back an ed25519
|
|
439
|
+
// signature they can attach to the tx
|
|
440
|
+
//
|
|
441
|
+
// This keeps the crypto surface minimal and correct. We don't try to
|
|
442
|
+
// re-implement RLP, EIP-712, or Solana's transaction encoder inside
|
|
443
|
+
// CoinPay — those live in the caller's toolchain.
|
|
444
|
+
// ================================================================
|
|
445
|
+
|
|
446
|
+
/** Chains that sign via secp256k1 (32-byte digest → recoverable sig). */
|
|
447
|
+
function isSecp256k1Chain(chain) {
|
|
448
|
+
return SECP256K1_CHAINS.includes(chain);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Chains that sign via ed25519 (arbitrary message bytes → 64-byte sig). */
|
|
452
|
+
function isEd25519Chain(chain) {
|
|
453
|
+
return chain === 'SOL' || chain === 'USDC_SOL' || chain === 'USDT_SOL';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeHexInput(hex, label = 'hex') {
|
|
457
|
+
if (typeof hex !== 'string') throw new Error(`${label} must be a string`);
|
|
458
|
+
const stripped = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
459
|
+
if (stripped.length === 0) throw new Error(`${label} is empty`);
|
|
460
|
+
if (stripped.length % 2 !== 0) throw new Error(`${label} has odd length`);
|
|
461
|
+
if (!/^[0-9a-fA-F]+$/.test(stripped)) throw new Error(`${label} contains non-hex chars`);
|
|
462
|
+
return hexToBytes(stripped);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Sign a 32-byte digest with a secp256k1 chain's derived key.
|
|
467
|
+
* Returns a 65-byte recoverable signature formatted as `r(32) || s(32) || v(1)`
|
|
468
|
+
* where v is the low recovery id (0 or 1). This is the layout every
|
|
469
|
+
* EVM client (viem, ethers v6) expects for post-typed-transaction
|
|
470
|
+
* (EIP-1559 / EIP-2930) signatures.
|
|
471
|
+
*
|
|
472
|
+
* For legacy / EIP-155 transactions the caller is responsible for
|
|
473
|
+
* converting v to `chainId * 2 + 35 + recId` after receiving the sig.
|
|
474
|
+
* For EIP-191 (personal_sign) the caller adds 27.
|
|
475
|
+
*
|
|
476
|
+
* @param {Uint8Array} seed - Master seed
|
|
477
|
+
* @param {string} chain - BIP44 chain code (ETH, POL, BNB, ...)
|
|
478
|
+
* @param {Uint8Array} digest - Exactly 32 bytes
|
|
479
|
+
* @returns {string} `0x`-prefixed 65-byte hex signature
|
|
480
|
+
*/
|
|
481
|
+
export function signDigestSecp256k1(seed, chain, digest) {
|
|
482
|
+
if (!isSecp256k1Chain(chain)) {
|
|
483
|
+
throw new Error(`chain ${chain} does not use secp256k1`);
|
|
484
|
+
}
|
|
485
|
+
if (!(digest instanceof Uint8Array) || digest.length !== 32) {
|
|
486
|
+
throw new Error('digest must be a 32-byte Uint8Array');
|
|
487
|
+
}
|
|
488
|
+
const { privateKey } = deriveKeyPair(seed, chain, 0);
|
|
489
|
+
|
|
490
|
+
// noble-curves has two incompatible signing APIs across major versions:
|
|
491
|
+
// v1.x: sign(hash, key) → Signature object with .r, .s, .recovery
|
|
492
|
+
// v2.x: sign(hash, key, { format: 'recovered' }) → 65-byte Uint8Array
|
|
493
|
+
// [recovery, r(32), s(32)]
|
|
494
|
+
// Detect at runtime so this works with whichever version is pinned.
|
|
495
|
+
// Requesting 'recovered' on v1 is harmless — v1 ignores the option
|
|
496
|
+
// and still returns a Signature object.
|
|
497
|
+
const raw = secp256k1.sign(digest, privateKey, { format: 'recovered' });
|
|
498
|
+
let r, s, recovery;
|
|
499
|
+
if (raw instanceof Uint8Array && raw.length === 65) {
|
|
500
|
+
recovery = raw[0] & 0x01;
|
|
501
|
+
r = raw.subarray(1, 33);
|
|
502
|
+
s = raw.subarray(33, 65);
|
|
503
|
+
} else if (raw && typeof raw === 'object' && 'r' in raw && 's' in raw) {
|
|
504
|
+
if (typeof raw.recovery !== 'number') {
|
|
505
|
+
throw new Error('signature missing recovery id');
|
|
506
|
+
}
|
|
507
|
+
recovery = raw.recovery & 0x01;
|
|
508
|
+
r = bigIntToBytes32(raw.r);
|
|
509
|
+
s = bigIntToBytes32(raw.s);
|
|
510
|
+
} else {
|
|
511
|
+
throw new Error(`unexpected signature shape (type=${raw?.constructor?.name})`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const evmSig = new Uint8Array(65);
|
|
515
|
+
evmSig.set(r, 0);
|
|
516
|
+
evmSig.set(s, 32);
|
|
517
|
+
evmSig[64] = recovery;
|
|
518
|
+
return '0x' + bytesToHex(evmSig);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Big-endian 32-byte encoding of a bigint. */
|
|
522
|
+
function bigIntToBytes32(n) {
|
|
523
|
+
const out = new Uint8Array(32);
|
|
524
|
+
let x = BigInt(n);
|
|
525
|
+
for (let i = 31; i >= 0; i--) {
|
|
526
|
+
out[i] = Number(x & 0xffn);
|
|
527
|
+
x >>= 8n;
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Sign an EIP-191 "personal_sign" message. Wraps the message with the
|
|
534
|
+
* `\x19Ethereum Signed Message:\n<length>` prefix, keccak256-hashes,
|
|
535
|
+
* then signs the digest. Returns a 65-byte sig with v bumped to 27/28
|
|
536
|
+
* per EIP-191 convention (compatible with Solidity's ecrecover).
|
|
537
|
+
*
|
|
538
|
+
* @param {Uint8Array} seed
|
|
539
|
+
* @param {string} chain
|
|
540
|
+
* @param {string|Uint8Array} message - UTF-8 string or raw bytes
|
|
541
|
+
* @returns {string}
|
|
542
|
+
*/
|
|
543
|
+
export function signEip191Message(seed, chain, message) {
|
|
544
|
+
if (!isSecp256k1Chain(chain)) {
|
|
545
|
+
throw new Error(`chain ${chain} does not use secp256k1`);
|
|
546
|
+
}
|
|
547
|
+
const bytes = typeof message === 'string'
|
|
548
|
+
? new TextEncoder().encode(message)
|
|
549
|
+
: message;
|
|
550
|
+
const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${bytes.length}`);
|
|
551
|
+
const payload = new Uint8Array(prefix.length + bytes.length);
|
|
552
|
+
payload.set(prefix, 0);
|
|
553
|
+
payload.set(bytes, prefix.length);
|
|
554
|
+
const digest = keccak_256(payload);
|
|
555
|
+
const sig = hexToBytes(signDigestSecp256k1(seed, chain, digest).slice(2));
|
|
556
|
+
sig[64] = sig[64] + 27; // EIP-191 uses 27/28 for v
|
|
557
|
+
return '0x' + bytesToHex(sig);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Sign arbitrary Solana transaction message bytes with the derived
|
|
562
|
+
* ed25519 key. Returns a 64-byte signature that the caller attaches
|
|
563
|
+
* to the `VersionedTransaction.signatures` array.
|
|
564
|
+
*
|
|
565
|
+
* @param {Uint8Array} seed
|
|
566
|
+
* @param {Uint8Array} messageBytes - The serialized Solana tx message
|
|
567
|
+
* @param {number} [index=0] - Derivation index
|
|
568
|
+
* @returns {string} 64-byte hex signature
|
|
569
|
+
*/
|
|
570
|
+
export function signSolanaMessage(seed, messageBytes, index = 0) {
|
|
571
|
+
if (!(messageBytes instanceof Uint8Array)) {
|
|
572
|
+
throw new Error('messageBytes must be a Uint8Array');
|
|
573
|
+
}
|
|
574
|
+
const { privateKey } = deriveSOLAddress(seed, index);
|
|
575
|
+
const signature = ed25519.sign(messageBytes, privateKey);
|
|
576
|
+
return '0x' + bytesToHex(signature);
|
|
577
|
+
}
|
|
578
|
+
|
|
422
579
|
/**
|
|
423
580
|
* Derive address placeholder - actual address derivation happens on client
|
|
424
581
|
* This returns the public key that the server can use to verify ownership
|
|
@@ -864,6 +1021,77 @@ export class WalletClient {
|
|
|
864
1021
|
return broadcastResult;
|
|
865
1022
|
}
|
|
866
1023
|
|
|
1024
|
+
// ==========================================================
|
|
1025
|
+
// Arbitrary-transaction signing (PR: arbitrary-tx-signing)
|
|
1026
|
+
//
|
|
1027
|
+
// These methods expose raw cryptographic signing so callers can
|
|
1028
|
+
// use CoinPay as a wallet provider for flows CoinPay itself
|
|
1029
|
+
// doesn't implement (Uniswap swaps, Jupiter routes, EIP-712
|
|
1030
|
+
// typed data, etc.). The caller builds the unsigned tx with
|
|
1031
|
+
// their own library (viem / ethers / @solana/web3.js), passes
|
|
1032
|
+
// the digest or message bytes to CoinPay, and receives the
|
|
1033
|
+
// signature back. CoinPay never learns the tx semantics — it
|
|
1034
|
+
// just produces cryptographic output.
|
|
1035
|
+
//
|
|
1036
|
+
// SECURITY: these methods are intentionally unprivileged from
|
|
1037
|
+
// CoinPay's perspective. If you expose them to untrusted
|
|
1038
|
+
// callers you're handing over signing authority for the wallet.
|
|
1039
|
+
// The CLI `wallet sign-digest` / `sign-message` commands
|
|
1040
|
+
// require the GPG-encrypted wallet to be unlocked, same as
|
|
1041
|
+
// `wallet send`.
|
|
1042
|
+
// ==========================================================
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Sign a 32-byte digest with the wallet's secp256k1 key for the
|
|
1046
|
+
* given EVM chain. Returns a 65-byte recoverable signature laid
|
|
1047
|
+
* out as `r(32) || s(32) || v(1)`, where v is the raw recovery
|
|
1048
|
+
* bit (0 or 1). Suitable for EIP-1559 / EIP-2930 typed txs.
|
|
1049
|
+
*
|
|
1050
|
+
* For legacy EIP-155 signatures the caller converts v to
|
|
1051
|
+
* `chainId * 2 + 35 + v`. For EIP-191 personal_sign, use
|
|
1052
|
+
* `signMessage()` below instead.
|
|
1053
|
+
*
|
|
1054
|
+
* @param {{chain: string, digest: string|Uint8Array}} params
|
|
1055
|
+
* @returns {string} 0x-prefixed 65-byte signature hex
|
|
1056
|
+
*/
|
|
1057
|
+
signDigest({ chain, digest }) {
|
|
1058
|
+
if (!this.#seed) throw new Error('Wallet not initialized');
|
|
1059
|
+
const digestBytes = typeof digest === 'string'
|
|
1060
|
+
? normalizeHexInput(digest, 'digest')
|
|
1061
|
+
: digest;
|
|
1062
|
+
return signDigestSecp256k1(this.#seed, chain, digestBytes);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Sign an EIP-191 "personal_sign" message with the wallet's
|
|
1067
|
+
* secp256k1 key. Handles the `\x19Ethereum Signed Message:\n`
|
|
1068
|
+
* prefix internally.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {{chain: string, message: string|Uint8Array}} params
|
|
1071
|
+
* @returns {string} 0x-prefixed 65-byte signature hex
|
|
1072
|
+
*/
|
|
1073
|
+
signMessage({ chain, message }) {
|
|
1074
|
+
if (!this.#seed) throw new Error('Wallet not initialized');
|
|
1075
|
+
return signEip191Message(this.#seed, chain, message);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Sign raw Solana transaction message bytes with the derived
|
|
1080
|
+
* ed25519 key. Returns a 64-byte signature the caller attaches
|
|
1081
|
+
* to their `VersionedTransaction.signatures` array.
|
|
1082
|
+
*
|
|
1083
|
+
* @param {{message: string|Uint8Array, index?: number}} params
|
|
1084
|
+
* `message` is either hex-encoded bytes or a raw Uint8Array.
|
|
1085
|
+
* @returns {string} 0x-prefixed 64-byte signature hex
|
|
1086
|
+
*/
|
|
1087
|
+
signSolanaMessage({ message, index = 0 }) {
|
|
1088
|
+
if (!this.#seed) throw new Error('Wallet not initialized');
|
|
1089
|
+
const bytes = typeof message === 'string'
|
|
1090
|
+
? normalizeHexInput(message, 'message')
|
|
1091
|
+
: message;
|
|
1092
|
+
return signSolanaMessage(this.#seed, bytes, index);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
867
1095
|
/**
|
|
868
1096
|
* Get transaction history
|
|
869
1097
|
* @param {Object} [options] - Query options
|
package/src/webhooks.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Webhook utilities for CoinPay SDK
|
|
3
|
+
*
|
|
4
|
+
* Contract:
|
|
5
|
+
* - Each business has ONE webhook URL + ONE webhook secret. Both crypto
|
|
6
|
+
* and card payment events route to the same endpoint, signed with the
|
|
7
|
+
* same secret. Distinguish rails via data.metadata.payment_rail
|
|
8
|
+
* ("crypto" or "card").
|
|
9
|
+
* - Header: X-CoinPay-Signature: t=<unix_seconds>,v1=<hex_hmac>
|
|
10
|
+
* - HMAC body: `${timestamp}.${rawBody}`
|
|
11
|
+
* - Algorithm: HMAC-SHA256
|
|
12
|
+
* - Tolerance: 300 seconds
|
|
13
|
+
*
|
|
14
|
+
* The server-side signer (src/lib/webhooks/service.ts) and the d0rz
|
|
15
|
+
* verifier (lib/coinpay-client.ts) are kept in lockstep with this file
|
|
16
|
+
* by src/lib/webhooks/contract.test.ts in the main repo. Don't drift.
|
|
3
17
|
*/
|
|
4
18
|
|
|
5
19
|
import { createHmac, timingSafeEqual } from 'crypto';
|
|
@@ -44,7 +58,12 @@ export function verifyWebhookSignature({
|
|
|
44
58
|
const signatureParts = {};
|
|
45
59
|
|
|
46
60
|
for (const part of parts) {
|
|
47
|
-
|
|
61
|
+
// Split on the FIRST '=' only. Splitting on every one truncates any value
|
|
62
|
+
// that legitimately contains '=' (base64 padding, for instance).
|
|
63
|
+
const separator = part.indexOf('=');
|
|
64
|
+
if (separator === -1) continue;
|
|
65
|
+
const key = part.slice(0, separator).trim();
|
|
66
|
+
const value = part.slice(separator + 1).trim();
|
|
48
67
|
signatureParts[key] = value;
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -55,8 +74,23 @@ export function verifyWebhookSignature({
|
|
|
55
74
|
return false;
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
// Check timestamp tolerance
|
|
59
|
-
|
|
77
|
+
// Check timestamp tolerance.
|
|
78
|
+
//
|
|
79
|
+
// parseInt('abc', 10) is NaN, and every comparison against NaN is false —
|
|
80
|
+
// so `Math.abs(NaN) > tolerance` was false and the freshness check silently
|
|
81
|
+
// PASSED for any non-numeric timestamp. A security check that no-ops on
|
|
82
|
+
// malformed input is worse than no check, because it reads as one. Parse
|
|
83
|
+
// strictly and reject anything that is not a plain integer.
|
|
84
|
+
if (!/^\d+$/.test(String(timestamp))) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const signedAt = Number(timestamp);
|
|
89
|
+
if (!Number.isSafeInteger(signedAt) || signedAt <= 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const timestampAge = Math.floor(Date.now() / 1000) - signedAt;
|
|
60
94
|
if (Math.abs(timestampAge) > tolerance) {
|
|
61
95
|
return false;
|
|
62
96
|
}
|