@sip-protocol/sdk 0.7.2 → 0.7.4
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/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +48874 -18336
- package/dist/browser.mjs +674 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/chunk-YWGJ77A2.mjs +33806 -0
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/index-DXh2IGkz.d.ts +24681 -0
- package/dist/index-DeE1ZzA4.d.mts +24681 -0
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48676 -17318
- package/dist/index.mjs +583 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +54 -21
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +276 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +201 -0
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +402 -0
- package/src/chains/solana/providers/index.ts +85 -0
- package/src/chains/solana/providers/interface.ts +221 -0
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +790 -0
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +170 -73
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +77 -7
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +37 -0
- package/src/compliance/range-sas.ts +956 -0
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +785 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +336 -0
- package/src/privacy-backends/interface.ts +906 -0
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash-types.ts +278 -0
- package/src/privacy-backends/privacycash.ts +456 -0
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +690 -0
- package/src/privacy-backends/router.ts +626 -0
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +256 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +111 -30
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/surveillance/algorithms/address-reuse.ts +143 -0
- package/src/surveillance/algorithms/cluster.ts +247 -0
- package/src/surveillance/algorithms/exchange.ts +295 -0
- package/src/surveillance/algorithms/temporal.ts +337 -0
- package/src/surveillance/analyzer.ts +442 -0
- package/src/surveillance/index.ts +64 -0
- package/src/surveillance/scoring.ts +372 -0
- package/src/surveillance/types.ts +264 -0
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-Q4NAVBTS.mjs +0 -46
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter for Privacy Backends
|
|
3
|
+
*
|
|
4
|
+
* Implements token bucket rate limiting with configurable limits per backend.
|
|
5
|
+
* Supports graceful degradation with queue/reject modes.
|
|
6
|
+
*
|
|
7
|
+
* ## Token Bucket Algorithm
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
11
|
+
* │ Token Bucket │
|
|
12
|
+
* │ ┌─────────────────────────────────────────────────┐ │
|
|
13
|
+
* │ │ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ (maxTokens) │ │
|
|
14
|
+
* │ │ ← tokens refill at refillRate per interval → │ │
|
|
15
|
+
* │ └─────────────────────────────────────────────────┘ │
|
|
16
|
+
* │ │ │
|
|
17
|
+
* │ [request] │
|
|
18
|
+
* │ │ │
|
|
19
|
+
* │ ▼ │
|
|
20
|
+
* │ tokens > 0 ? consume : reject/queue │
|
|
21
|
+
* └─────────────────────────────────────────────────────────┘
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const limiter = new RateLimiter({
|
|
27
|
+
* defaultConfig: {
|
|
28
|
+
* maxTokens: 10,
|
|
29
|
+
* refillRate: 1,
|
|
30
|
+
* refillIntervalMs: 1000, // 1 token per second
|
|
31
|
+
* },
|
|
32
|
+
* backendOverrides: {
|
|
33
|
+
* 'arcium': { maxTokens: 5 }, // Slower backend
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // Check if request is allowed
|
|
38
|
+
* if (limiter.tryAcquire('sip-native')) {
|
|
39
|
+
* // Make request
|
|
40
|
+
* } else {
|
|
41
|
+
* // Rate limited
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* // Or use async with queueing
|
|
45
|
+
* await limiter.acquire('sip-native', { timeout: 5000 })
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @see https://en.wikipedia.org/wiki/Token_bucket
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { deepFreeze } from './interface'
|
|
52
|
+
|
|
53
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configuration for a single rate limit bucket
|
|
57
|
+
*/
|
|
58
|
+
export interface RateLimitConfig {
|
|
59
|
+
/**
|
|
60
|
+
* Maximum number of tokens in the bucket
|
|
61
|
+
* @default 10
|
|
62
|
+
*/
|
|
63
|
+
maxTokens: number
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Number of tokens to add per refill interval
|
|
67
|
+
* @default 1
|
|
68
|
+
*/
|
|
69
|
+
refillRate: number
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Interval between token refills in milliseconds
|
|
73
|
+
* @default 1000 (1 second)
|
|
74
|
+
*/
|
|
75
|
+
refillIntervalMs: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Configuration for the rate limiter
|
|
80
|
+
*/
|
|
81
|
+
export interface RateLimiterConfig {
|
|
82
|
+
/**
|
|
83
|
+
* Default rate limit configuration for all backends
|
|
84
|
+
*/
|
|
85
|
+
defaultConfig?: Partial<RateLimitConfig>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per-backend rate limit overrides
|
|
89
|
+
*/
|
|
90
|
+
backendOverrides?: Record<string, Partial<RateLimitConfig>>
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Behavior when rate limit is exceeded
|
|
94
|
+
* - 'reject': Immediately return false/throw
|
|
95
|
+
* - 'queue': Wait for token availability (with timeout)
|
|
96
|
+
* @default 'reject'
|
|
97
|
+
*/
|
|
98
|
+
onLimitExceeded?: 'reject' | 'queue'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Maximum queue size per backend (when onLimitExceeded is 'queue')
|
|
102
|
+
* @default 100
|
|
103
|
+
*/
|
|
104
|
+
maxQueueSize?: number
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Default timeout for queued requests in milliseconds
|
|
108
|
+
* @default 30000 (30 seconds)
|
|
109
|
+
*/
|
|
110
|
+
defaultTimeoutMs?: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Options for acquire operations
|
|
115
|
+
*/
|
|
116
|
+
export interface AcquireOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Timeout for waiting for token availability (ms)
|
|
119
|
+
* Only applies when onLimitExceeded is 'queue'
|
|
120
|
+
*/
|
|
121
|
+
timeout?: number
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Number of tokens to acquire
|
|
125
|
+
* @default 1
|
|
126
|
+
*/
|
|
127
|
+
tokens?: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* State of a token bucket
|
|
132
|
+
*/
|
|
133
|
+
interface BucketState {
|
|
134
|
+
/** Current number of available tokens */
|
|
135
|
+
tokens: number
|
|
136
|
+
/** Last refill timestamp */
|
|
137
|
+
lastRefill: number
|
|
138
|
+
/** Effective configuration for this bucket */
|
|
139
|
+
config: Required<RateLimitConfig>
|
|
140
|
+
/** Queue of waiting requests (resolve callbacks) */
|
|
141
|
+
queue: Array<{
|
|
142
|
+
resolve: (acquired: boolean) => void
|
|
143
|
+
tokens: number
|
|
144
|
+
expiresAt: number
|
|
145
|
+
}>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Statistics for a rate-limited backend
|
|
150
|
+
*/
|
|
151
|
+
export interface RateLimitStats {
|
|
152
|
+
/** Backend name */
|
|
153
|
+
name: string
|
|
154
|
+
/** Current available tokens */
|
|
155
|
+
availableTokens: number
|
|
156
|
+
/** Maximum tokens */
|
|
157
|
+
maxTokens: number
|
|
158
|
+
/** Requests allowed */
|
|
159
|
+
allowed: number
|
|
160
|
+
/** Requests rejected due to rate limit */
|
|
161
|
+
rejected: number
|
|
162
|
+
/** Requests currently queued */
|
|
163
|
+
queued: number
|
|
164
|
+
/** Total tokens consumed */
|
|
165
|
+
tokensConsumed: number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Default rate limit configuration
|
|
172
|
+
*/
|
|
173
|
+
export const DEFAULT_RATE_LIMIT_CONFIG: Required<RateLimitConfig> = {
|
|
174
|
+
maxTokens: 10,
|
|
175
|
+
refillRate: 1,
|
|
176
|
+
refillIntervalMs: 1000,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Default rate limiter configuration
|
|
181
|
+
*/
|
|
182
|
+
export const DEFAULT_RATE_LIMITER_CONFIG: Required<Omit<RateLimiterConfig, 'backendOverrides'>> = {
|
|
183
|
+
defaultConfig: DEFAULT_RATE_LIMIT_CONFIG,
|
|
184
|
+
onLimitExceeded: 'reject',
|
|
185
|
+
maxQueueSize: 100,
|
|
186
|
+
defaultTimeoutMs: 30000,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Errors ─────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Error thrown when rate limit is exceeded
|
|
193
|
+
*/
|
|
194
|
+
export class RateLimitExceededError extends Error {
|
|
195
|
+
readonly name = 'RateLimitExceededError'
|
|
196
|
+
|
|
197
|
+
constructor(
|
|
198
|
+
readonly backend: string,
|
|
199
|
+
readonly availableTokens: number,
|
|
200
|
+
readonly requestedTokens: number,
|
|
201
|
+
readonly retryAfterMs?: number
|
|
202
|
+
) {
|
|
203
|
+
const retryMsg = retryAfterMs ? ` Retry after ${retryAfterMs}ms.` : ''
|
|
204
|
+
super(
|
|
205
|
+
`Rate limit exceeded for backend '${backend}'. ` +
|
|
206
|
+
`Available: ${availableTokens}, requested: ${requestedTokens}.${retryMsg}`
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Error thrown when queue is full
|
|
213
|
+
*/
|
|
214
|
+
export class QueueFullError extends Error {
|
|
215
|
+
readonly name = 'QueueFullError'
|
|
216
|
+
|
|
217
|
+
constructor(
|
|
218
|
+
readonly backend: string,
|
|
219
|
+
readonly queueSize: number,
|
|
220
|
+
readonly maxQueueSize: number
|
|
221
|
+
) {
|
|
222
|
+
super(
|
|
223
|
+
`Queue full for backend '${backend}'. ` +
|
|
224
|
+
`Current size: ${queueSize}, max: ${maxQueueSize}.`
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Error thrown when acquire times out
|
|
231
|
+
*/
|
|
232
|
+
export class AcquireTimeoutError extends Error {
|
|
233
|
+
readonly name = 'AcquireTimeoutError'
|
|
234
|
+
|
|
235
|
+
constructor(
|
|
236
|
+
readonly backend: string,
|
|
237
|
+
readonly timeoutMs: number
|
|
238
|
+
) {
|
|
239
|
+
super(`Acquire timed out for backend '${backend}' after ${timeoutMs}ms.`)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Rate Limiter ───────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Rate Limiter for Privacy Backends
|
|
247
|
+
*
|
|
248
|
+
* Implements per-backend rate limiting using the token bucket algorithm.
|
|
249
|
+
* Supports configurable limits, graceful degradation, and request queueing.
|
|
250
|
+
*/
|
|
251
|
+
export class RateLimiter {
|
|
252
|
+
private buckets: Map<string, BucketState> = new Map()
|
|
253
|
+
private config: {
|
|
254
|
+
defaultConfig: Required<RateLimitConfig>
|
|
255
|
+
onLimitExceeded: 'reject' | 'queue'
|
|
256
|
+
maxQueueSize: number
|
|
257
|
+
defaultTimeoutMs: number
|
|
258
|
+
}
|
|
259
|
+
private overrides: Record<string, Partial<RateLimitConfig>>
|
|
260
|
+
private stats: Map<string, { allowed: number; rejected: number; tokensConsumed: number }> = new Map()
|
|
261
|
+
private processQueueInterval: ReturnType<typeof setInterval> | null = null
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create a new rate limiter
|
|
265
|
+
*
|
|
266
|
+
* @param config - Rate limiter configuration
|
|
267
|
+
*/
|
|
268
|
+
constructor(config: RateLimiterConfig = {}) {
|
|
269
|
+
this.config = {
|
|
270
|
+
defaultConfig: { ...DEFAULT_RATE_LIMIT_CONFIG, ...config.defaultConfig },
|
|
271
|
+
onLimitExceeded: config.onLimitExceeded ?? 'reject',
|
|
272
|
+
maxQueueSize: config.maxQueueSize ?? 100,
|
|
273
|
+
defaultTimeoutMs: config.defaultTimeoutMs ?? 30000,
|
|
274
|
+
}
|
|
275
|
+
this.overrides = config.backendOverrides ?? {}
|
|
276
|
+
|
|
277
|
+
// Start queue processor if queueing is enabled
|
|
278
|
+
if (this.config.onLimitExceeded === 'queue') {
|
|
279
|
+
this.startQueueProcessor()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Try to acquire tokens for a backend (non-blocking)
|
|
285
|
+
*
|
|
286
|
+
* @param backend - Backend name
|
|
287
|
+
* @param tokens - Number of tokens to acquire (default: 1)
|
|
288
|
+
* @returns true if tokens were acquired, false if rate limited
|
|
289
|
+
*/
|
|
290
|
+
tryAcquire(backend: string, tokens: number = 1): boolean {
|
|
291
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
292
|
+
this.refillBucket(bucket)
|
|
293
|
+
|
|
294
|
+
if (bucket.tokens >= tokens) {
|
|
295
|
+
bucket.tokens -= tokens
|
|
296
|
+
this.recordAllowed(backend, tokens)
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.recordRejected(backend)
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Acquire tokens for a backend (blocking with optional queue)
|
|
306
|
+
*
|
|
307
|
+
* In 'reject' mode, throws RateLimitExceededError if tokens not available.
|
|
308
|
+
* In 'queue' mode, waits for tokens to become available (with timeout).
|
|
309
|
+
*
|
|
310
|
+
* @param backend - Backend name
|
|
311
|
+
* @param options - Acquire options
|
|
312
|
+
* @returns Promise that resolves when tokens are acquired
|
|
313
|
+
* @throws RateLimitExceededError if tokens not available (reject mode)
|
|
314
|
+
* @throws AcquireTimeoutError if timeout exceeded (queue mode)
|
|
315
|
+
* @throws QueueFullError if queue is full (queue mode)
|
|
316
|
+
*/
|
|
317
|
+
async acquire(backend: string, options: AcquireOptions = {}): Promise<void> {
|
|
318
|
+
const tokens = options.tokens ?? 1
|
|
319
|
+
const timeout = options.timeout ?? this.config.defaultTimeoutMs
|
|
320
|
+
|
|
321
|
+
// Try immediate acquisition
|
|
322
|
+
if (this.tryAcquire(backend, tokens)) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Handle based on mode
|
|
327
|
+
if (this.config.onLimitExceeded === 'reject') {
|
|
328
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
329
|
+
const retryAfterMs = this.estimateRefillTime(bucket, tokens)
|
|
330
|
+
throw new RateLimitExceededError(backend, bucket.tokens, tokens, retryAfterMs)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Queue mode - wait for tokens
|
|
334
|
+
return this.waitForTokens(backend, tokens, timeout)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check if tokens are available without consuming them
|
|
339
|
+
*
|
|
340
|
+
* @param backend - Backend name
|
|
341
|
+
* @param tokens - Number of tokens to check (default: 1)
|
|
342
|
+
* @returns true if tokens are available
|
|
343
|
+
*/
|
|
344
|
+
canAcquire(backend: string, tokens: number = 1): boolean {
|
|
345
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
346
|
+
this.refillBucket(bucket)
|
|
347
|
+
return bucket.tokens >= tokens
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get number of available tokens for a backend
|
|
352
|
+
*
|
|
353
|
+
* @param backend - Backend name
|
|
354
|
+
* @returns Number of available tokens
|
|
355
|
+
*/
|
|
356
|
+
getAvailableTokens(backend: string): number {
|
|
357
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
358
|
+
this.refillBucket(bucket)
|
|
359
|
+
return bucket.tokens
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get estimated time until tokens are available
|
|
364
|
+
*
|
|
365
|
+
* @param backend - Backend name
|
|
366
|
+
* @param tokens - Number of tokens needed (default: 1)
|
|
367
|
+
* @returns Estimated milliseconds until tokens available, or 0 if available now
|
|
368
|
+
*/
|
|
369
|
+
getTimeUntilAvailable(backend: string, tokens: number = 1): number {
|
|
370
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
371
|
+
this.refillBucket(bucket)
|
|
372
|
+
|
|
373
|
+
if (bucket.tokens >= tokens) {
|
|
374
|
+
return 0
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return this.estimateRefillTime(bucket, tokens)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get rate limit statistics for a backend
|
|
382
|
+
*
|
|
383
|
+
* @param backend - Backend name
|
|
384
|
+
* @returns Rate limit statistics
|
|
385
|
+
*/
|
|
386
|
+
getStats(backend: string): RateLimitStats {
|
|
387
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
388
|
+
this.refillBucket(bucket)
|
|
389
|
+
const stats = this.stats.get(backend) ?? { allowed: 0, rejected: 0, tokensConsumed: 0 }
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
name: backend,
|
|
393
|
+
availableTokens: bucket.tokens,
|
|
394
|
+
maxTokens: bucket.config.maxTokens,
|
|
395
|
+
allowed: stats.allowed,
|
|
396
|
+
rejected: stats.rejected,
|
|
397
|
+
queued: bucket.queue.length,
|
|
398
|
+
tokensConsumed: stats.tokensConsumed,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get statistics for all tracked backends
|
|
404
|
+
*
|
|
405
|
+
* @returns Map of backend name to statistics
|
|
406
|
+
*/
|
|
407
|
+
getAllStats(): Map<string, RateLimitStats> {
|
|
408
|
+
const allStats = new Map<string, RateLimitStats>()
|
|
409
|
+
for (const backend of this.buckets.keys()) {
|
|
410
|
+
allStats.set(backend, this.getStats(backend))
|
|
411
|
+
}
|
|
412
|
+
return allStats
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get rate limit configuration for a backend
|
|
417
|
+
*
|
|
418
|
+
* @param backend - Backend name
|
|
419
|
+
* @returns Effective rate limit configuration
|
|
420
|
+
*/
|
|
421
|
+
getBackendConfig(backend: string): Readonly<Required<RateLimitConfig>> {
|
|
422
|
+
return deepFreeze(this.getEffectiveConfig(backend))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Update rate limit configuration for a specific backend
|
|
427
|
+
*
|
|
428
|
+
* @param backend - Backend name
|
|
429
|
+
* @param config - Partial configuration to merge
|
|
430
|
+
*/
|
|
431
|
+
setBackendConfig(backend: string, config: Partial<RateLimitConfig>): void {
|
|
432
|
+
this.overrides[backend] = { ...this.overrides[backend], ...config }
|
|
433
|
+
|
|
434
|
+
// Update existing bucket if it exists
|
|
435
|
+
if (this.buckets.has(backend)) {
|
|
436
|
+
const bucket = this.buckets.get(backend)!
|
|
437
|
+
bucket.config = this.getEffectiveConfig(backend)
|
|
438
|
+
// Cap current tokens to new max
|
|
439
|
+
if (bucket.tokens > bucket.config.maxTokens) {
|
|
440
|
+
bucket.tokens = bucket.config.maxTokens
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Reset rate limit state for a backend
|
|
447
|
+
*
|
|
448
|
+
* Refills tokens to max and clears queue.
|
|
449
|
+
*
|
|
450
|
+
* @param backend - Backend name
|
|
451
|
+
*/
|
|
452
|
+
reset(backend: string): void {
|
|
453
|
+
if (this.buckets.has(backend)) {
|
|
454
|
+
const bucket = this.buckets.get(backend)!
|
|
455
|
+
bucket.tokens = bucket.config.maxTokens
|
|
456
|
+
bucket.lastRefill = Date.now()
|
|
457
|
+
|
|
458
|
+
// Reject all queued requests
|
|
459
|
+
for (const waiter of bucket.queue) {
|
|
460
|
+
waiter.resolve(false)
|
|
461
|
+
}
|
|
462
|
+
bucket.queue = []
|
|
463
|
+
}
|
|
464
|
+
this.stats.delete(backend)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Reset all rate limit state
|
|
469
|
+
*/
|
|
470
|
+
resetAll(): void {
|
|
471
|
+
for (const backend of this.buckets.keys()) {
|
|
472
|
+
this.reset(backend)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Clear tracking for a backend
|
|
478
|
+
*
|
|
479
|
+
* @param backend - Backend name
|
|
480
|
+
* @returns true if backend was tracked
|
|
481
|
+
*/
|
|
482
|
+
unregister(backend: string): boolean {
|
|
483
|
+
const bucket = this.buckets.get(backend)
|
|
484
|
+
if (bucket) {
|
|
485
|
+
// Reject all queued requests
|
|
486
|
+
for (const waiter of bucket.queue) {
|
|
487
|
+
waiter.resolve(false)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
this.stats.delete(backend)
|
|
491
|
+
return this.buckets.delete(backend)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get current configuration (deeply frozen copy)
|
|
496
|
+
*/
|
|
497
|
+
getConfig(): Readonly<RateLimiterConfig> {
|
|
498
|
+
return deepFreeze({
|
|
499
|
+
...this.config,
|
|
500
|
+
backendOverrides: { ...this.overrides },
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get names of all tracked backends
|
|
506
|
+
*/
|
|
507
|
+
getTrackedBackends(): string[] {
|
|
508
|
+
return Array.from(this.buckets.keys())
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Dispose of the rate limiter
|
|
513
|
+
*
|
|
514
|
+
* Stops queue processor and rejects all pending requests.
|
|
515
|
+
*/
|
|
516
|
+
dispose(): void {
|
|
517
|
+
if (this.processQueueInterval) {
|
|
518
|
+
clearInterval(this.processQueueInterval)
|
|
519
|
+
this.processQueueInterval = null
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Reject all queued requests
|
|
523
|
+
for (const bucket of this.buckets.values()) {
|
|
524
|
+
for (const waiter of bucket.queue) {
|
|
525
|
+
waiter.resolve(false)
|
|
526
|
+
}
|
|
527
|
+
bucket.queue = []
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─── Private Methods ────────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get or create a bucket for a backend
|
|
535
|
+
*/
|
|
536
|
+
private getOrCreateBucket(backend: string): BucketState {
|
|
537
|
+
if (!this.buckets.has(backend)) {
|
|
538
|
+
const config = this.getEffectiveConfig(backend)
|
|
539
|
+
this.buckets.set(backend, {
|
|
540
|
+
tokens: config.maxTokens,
|
|
541
|
+
lastRefill: Date.now(),
|
|
542
|
+
config,
|
|
543
|
+
queue: [],
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
return this.buckets.get(backend)!
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Get effective configuration for a backend
|
|
551
|
+
*/
|
|
552
|
+
private getEffectiveConfig(backend: string): Required<RateLimitConfig> {
|
|
553
|
+
const override = this.overrides[backend] ?? {}
|
|
554
|
+
return {
|
|
555
|
+
maxTokens: override.maxTokens ?? this.config.defaultConfig.maxTokens,
|
|
556
|
+
refillRate: override.refillRate ?? this.config.defaultConfig.refillRate,
|
|
557
|
+
refillIntervalMs: override.refillIntervalMs ?? this.config.defaultConfig.refillIntervalMs,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Refill tokens based on elapsed time
|
|
563
|
+
*/
|
|
564
|
+
private refillBucket(bucket: BucketState): void {
|
|
565
|
+
const now = Date.now()
|
|
566
|
+
const elapsed = now - bucket.lastRefill
|
|
567
|
+
|
|
568
|
+
if (elapsed >= bucket.config.refillIntervalMs) {
|
|
569
|
+
const intervals = Math.floor(elapsed / bucket.config.refillIntervalMs)
|
|
570
|
+
const tokensToAdd = intervals * bucket.config.refillRate
|
|
571
|
+
bucket.tokens = Math.min(bucket.config.maxTokens, bucket.tokens + tokensToAdd)
|
|
572
|
+
bucket.lastRefill = now - (elapsed % bucket.config.refillIntervalMs)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Estimate time until requested tokens are available
|
|
578
|
+
*/
|
|
579
|
+
private estimateRefillTime(bucket: BucketState, tokens: number): number {
|
|
580
|
+
const tokensNeeded = tokens - bucket.tokens
|
|
581
|
+
if (tokensNeeded <= 0) return 0
|
|
582
|
+
|
|
583
|
+
const intervalsNeeded = Math.ceil(tokensNeeded / bucket.config.refillRate)
|
|
584
|
+
const timeUntilNextRefill = bucket.config.refillIntervalMs -
|
|
585
|
+
(Date.now() - bucket.lastRefill) % bucket.config.refillIntervalMs
|
|
586
|
+
|
|
587
|
+
return timeUntilNextRefill + (intervalsNeeded - 1) * bucket.config.refillIntervalMs
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Wait for tokens to become available (queue mode)
|
|
592
|
+
*/
|
|
593
|
+
private async waitForTokens(
|
|
594
|
+
backend: string,
|
|
595
|
+
tokens: number,
|
|
596
|
+
timeout: number
|
|
597
|
+
): Promise<void> {
|
|
598
|
+
const bucket = this.getOrCreateBucket(backend)
|
|
599
|
+
|
|
600
|
+
// Check queue size limit
|
|
601
|
+
if (bucket.queue.length >= this.config.maxQueueSize) {
|
|
602
|
+
throw new QueueFullError(backend, bucket.queue.length, this.config.maxQueueSize)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return new Promise<void>((resolve, reject) => {
|
|
606
|
+
const expiresAt = Date.now() + timeout
|
|
607
|
+
|
|
608
|
+
const waiter = {
|
|
609
|
+
resolve: (acquired: boolean) => {
|
|
610
|
+
if (acquired) {
|
|
611
|
+
resolve()
|
|
612
|
+
} else {
|
|
613
|
+
reject(new AcquireTimeoutError(backend, timeout))
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
tokens,
|
|
617
|
+
expiresAt,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
bucket.queue.push(waiter)
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Start the queue processor interval
|
|
626
|
+
*/
|
|
627
|
+
private startQueueProcessor(): void {
|
|
628
|
+
// Process queues every 100ms
|
|
629
|
+
this.processQueueInterval = setInterval(() => {
|
|
630
|
+
this.processQueues()
|
|
631
|
+
}, 100)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Process all backend queues
|
|
636
|
+
*/
|
|
637
|
+
private processQueues(): void {
|
|
638
|
+
const now = Date.now()
|
|
639
|
+
|
|
640
|
+
for (const [backend, bucket] of this.buckets) {
|
|
641
|
+
this.refillBucket(bucket)
|
|
642
|
+
|
|
643
|
+
// Process expired waiters first
|
|
644
|
+
bucket.queue = bucket.queue.filter(waiter => {
|
|
645
|
+
if (waiter.expiresAt <= now) {
|
|
646
|
+
waiter.resolve(false)
|
|
647
|
+
return false
|
|
648
|
+
}
|
|
649
|
+
return true
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
// Try to fulfill waiting requests
|
|
653
|
+
while (bucket.queue.length > 0 && bucket.tokens >= bucket.queue[0].tokens) {
|
|
654
|
+
const waiter = bucket.queue.shift()!
|
|
655
|
+
bucket.tokens -= waiter.tokens
|
|
656
|
+
this.recordAllowed(backend, waiter.tokens)
|
|
657
|
+
waiter.resolve(true)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Record an allowed request in stats
|
|
664
|
+
*/
|
|
665
|
+
private recordAllowed(backend: string, tokens: number): void {
|
|
666
|
+
if (!this.stats.has(backend)) {
|
|
667
|
+
this.stats.set(backend, { allowed: 0, rejected: 0, tokensConsumed: 0 })
|
|
668
|
+
}
|
|
669
|
+
const stats = this.stats.get(backend)!
|
|
670
|
+
stats.allowed++
|
|
671
|
+
stats.tokensConsumed += tokens
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Record a rejected request in stats
|
|
676
|
+
*/
|
|
677
|
+
private recordRejected(backend: string): void {
|
|
678
|
+
if (!this.stats.has(backend)) {
|
|
679
|
+
this.stats.set(backend, { allowed: 0, rejected: 0, tokensConsumed: 0 })
|
|
680
|
+
}
|
|
681
|
+
this.stats.get(backend)!.rejected++
|
|
682
|
+
}
|
|
683
|
+
}
|