@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.
Files changed (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  4. package/dist/browser.d.mts +10 -4
  5. package/dist/browser.d.ts +10 -4
  6. package/dist/browser.js +48874 -18336
  7. package/dist/browser.mjs +674 -48
  8. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  9. package/dist/chunk-5D7A3L3W.mjs +717 -0
  10. package/dist/chunk-64AYA5F5.mjs +7834 -0
  11. package/dist/chunk-GMDGB22A.mjs +379 -0
  12. package/dist/chunk-I534WKN7.mjs +328 -0
  13. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  14. package/dist/chunk-PRRZAWJE.mjs +223 -0
  15. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  16. package/dist/chunk-YWGJ77A2.mjs +33806 -0
  17. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  18. package/dist/constants-LHAAUC2T.mjs +51 -0
  19. package/dist/dist-2OGQ7FED.mjs +3957 -0
  20. package/dist/dist-IFHPYLDX.mjs +254 -0
  21. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  22. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  23. package/dist/index-DXh2IGkz.d.ts +24681 -0
  24. package/dist/index-DeE1ZzA4.d.mts +24681 -0
  25. package/dist/index.d.mts +9 -3
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.js +48676 -17318
  28. package/dist/index.mjs +583 -19
  29. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  30. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  31. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  32. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  33. package/dist/proofs/halo2.d.mts +151 -0
  34. package/dist/proofs/halo2.d.ts +151 -0
  35. package/dist/proofs/halo2.js +350 -0
  36. package/dist/proofs/halo2.mjs +11 -0
  37. package/dist/proofs/kimchi.d.mts +160 -0
  38. package/dist/proofs/kimchi.d.ts +160 -0
  39. package/dist/proofs/kimchi.js +431 -0
  40. package/dist/proofs/kimchi.mjs +13 -0
  41. package/dist/proofs/noir.d.mts +1 -1
  42. package/dist/proofs/noir.d.ts +1 -1
  43. package/dist/proofs/noir.js +74 -18
  44. package/dist/proofs/noir.mjs +84 -24
  45. package/dist/solana-U3MEGU7W.mjs +280 -0
  46. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  47. package/package.json +54 -21
  48. package/src/adapters/index.ts +41 -0
  49. package/src/adapters/jupiter.ts +571 -0
  50. package/src/adapters/near-intents.ts +135 -0
  51. package/src/advisor/advisor.ts +653 -0
  52. package/src/advisor/index.ts +54 -0
  53. package/src/advisor/tools.ts +303 -0
  54. package/src/advisor/types.ts +164 -0
  55. package/src/chains/ethereum/announcement.ts +536 -0
  56. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  57. package/src/chains/ethereum/commitment.ts +522 -0
  58. package/src/chains/ethereum/constants.ts +462 -0
  59. package/src/chains/ethereum/deployment.ts +596 -0
  60. package/src/chains/ethereum/gas-estimation.ts +538 -0
  61. package/src/chains/ethereum/index.ts +268 -0
  62. package/src/chains/ethereum/optimizations.ts +614 -0
  63. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  64. package/src/chains/ethereum/registry.ts +584 -0
  65. package/src/chains/ethereum/rpc.ts +905 -0
  66. package/src/chains/ethereum/stealth.ts +491 -0
  67. package/src/chains/ethereum/token.ts +790 -0
  68. package/src/chains/ethereum/transfer.ts +637 -0
  69. package/src/chains/ethereum/types.ts +456 -0
  70. package/src/chains/ethereum/viewing-key.ts +455 -0
  71. package/src/chains/near/commitment.ts +608 -0
  72. package/src/chains/near/constants.ts +284 -0
  73. package/src/chains/near/function-call.ts +871 -0
  74. package/src/chains/near/history.ts +654 -0
  75. package/src/chains/near/implicit-account.ts +840 -0
  76. package/src/chains/near/index.ts +393 -0
  77. package/src/chains/near/native-transfer.ts +658 -0
  78. package/src/chains/near/nep141.ts +775 -0
  79. package/src/chains/near/privacy-adapter.ts +889 -0
  80. package/src/chains/near/resolver.ts +971 -0
  81. package/src/chains/near/rpc.ts +1016 -0
  82. package/src/chains/near/stealth.ts +419 -0
  83. package/src/chains/near/types.ts +317 -0
  84. package/src/chains/near/viewing-key.ts +876 -0
  85. package/src/chains/solana/anchor-transfer.ts +386 -0
  86. package/src/chains/solana/commitment.ts +577 -0
  87. package/src/chains/solana/constants.ts +126 -12
  88. package/src/chains/solana/ephemeral-keys.ts +543 -0
  89. package/src/chains/solana/index.ts +276 -1
  90. package/src/chains/solana/key-derivation.ts +418 -0
  91. package/src/chains/solana/kit-compat.ts +334 -0
  92. package/src/chains/solana/optimizations.ts +560 -0
  93. package/src/chains/solana/privacy-adapter.ts +605 -0
  94. package/src/chains/solana/providers/generic.ts +201 -0
  95. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  96. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  97. package/src/chains/solana/providers/helius.ts +402 -0
  98. package/src/chains/solana/providers/index.ts +85 -0
  99. package/src/chains/solana/providers/interface.ts +221 -0
  100. package/src/chains/solana/providers/quicknode.ts +409 -0
  101. package/src/chains/solana/providers/triton.ts +426 -0
  102. package/src/chains/solana/providers/webhook.ts +790 -0
  103. package/src/chains/solana/rpc-client.ts +1150 -0
  104. package/src/chains/solana/scan.ts +170 -73
  105. package/src/chains/solana/sol-transfer.ts +732 -0
  106. package/src/chains/solana/spl-transfer.ts +886 -0
  107. package/src/chains/solana/stealth-scanner.ts +703 -0
  108. package/src/chains/solana/sunspot-verifier.ts +453 -0
  109. package/src/chains/solana/transaction-builder.ts +755 -0
  110. package/src/chains/solana/transfer.ts +74 -5
  111. package/src/chains/solana/types.ts +77 -7
  112. package/src/chains/solana/utils.ts +110 -0
  113. package/src/chains/solana/viewing-key.ts +807 -0
  114. package/src/compliance/fireblocks.ts +921 -0
  115. package/src/compliance/index.ts +37 -0
  116. package/src/compliance/range-sas.ts +956 -0
  117. package/src/config/endpoints.ts +100 -0
  118. package/src/crypto.ts +11 -8
  119. package/src/errors.ts +82 -0
  120. package/src/evm/erc4337-relayer.ts +830 -0
  121. package/src/evm/index.ts +47 -0
  122. package/src/fees/calculator.ts +396 -0
  123. package/src/fees/index.ts +87 -0
  124. package/src/fees/near-contract.ts +429 -0
  125. package/src/fees/types.ts +268 -0
  126. package/src/index.ts +785 -1
  127. package/src/intent.ts +6 -3
  128. package/src/logger.ts +324 -0
  129. package/src/network/index.ts +80 -0
  130. package/src/network/proxy.ts +691 -0
  131. package/src/optimizations/index.ts +541 -0
  132. package/src/oracle/types.ts +1 -0
  133. package/src/privacy-backends/arcium-types.ts +727 -0
  134. package/src/privacy-backends/arcium.ts +719 -0
  135. package/src/privacy-backends/combined-privacy.ts +866 -0
  136. package/src/privacy-backends/cspl-token.ts +595 -0
  137. package/src/privacy-backends/cspl-types.ts +512 -0
  138. package/src/privacy-backends/cspl.ts +907 -0
  139. package/src/privacy-backends/health.ts +488 -0
  140. package/src/privacy-backends/inco-types.ts +323 -0
  141. package/src/privacy-backends/inco.ts +616 -0
  142. package/src/privacy-backends/index.ts +336 -0
  143. package/src/privacy-backends/interface.ts +906 -0
  144. package/src/privacy-backends/lru-cache.ts +343 -0
  145. package/src/privacy-backends/magicblock.ts +458 -0
  146. package/src/privacy-backends/mock.ts +258 -0
  147. package/src/privacy-backends/privacycash-types.ts +278 -0
  148. package/src/privacy-backends/privacycash.ts +456 -0
  149. package/src/privacy-backends/private-swap.ts +570 -0
  150. package/src/privacy-backends/rate-limiter.ts +683 -0
  151. package/src/privacy-backends/registry.ts +690 -0
  152. package/src/privacy-backends/router.ts +626 -0
  153. package/src/privacy-backends/shadowwire.ts +449 -0
  154. package/src/privacy-backends/sip-native.ts +256 -0
  155. package/src/privacy-logger.ts +191 -0
  156. package/src/production-safety.ts +373 -0
  157. package/src/proofs/aggregator.ts +1029 -0
  158. package/src/proofs/browser-composer.ts +1150 -0
  159. package/src/proofs/browser.ts +113 -25
  160. package/src/proofs/cache/index.ts +127 -0
  161. package/src/proofs/cache/interface.ts +545 -0
  162. package/src/proofs/cache/key-generator.ts +188 -0
  163. package/src/proofs/cache/lru-cache.ts +481 -0
  164. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  165. package/src/proofs/cache/persistent-cache.ts +788 -0
  166. package/src/proofs/compliance-proof.ts +872 -0
  167. package/src/proofs/composer/base.ts +923 -0
  168. package/src/proofs/composer/index.ts +25 -0
  169. package/src/proofs/composer/interface.ts +518 -0
  170. package/src/proofs/composer/types.ts +383 -0
  171. package/src/proofs/converters/halo2.ts +452 -0
  172. package/src/proofs/converters/index.ts +208 -0
  173. package/src/proofs/converters/interface.ts +363 -0
  174. package/src/proofs/converters/kimchi.ts +462 -0
  175. package/src/proofs/converters/noir.ts +451 -0
  176. package/src/proofs/fallback.ts +888 -0
  177. package/src/proofs/halo2.ts +42 -0
  178. package/src/proofs/index.ts +471 -0
  179. package/src/proofs/interface.ts +13 -0
  180. package/src/proofs/kimchi.ts +42 -0
  181. package/src/proofs/lazy.ts +1004 -0
  182. package/src/proofs/mock.ts +25 -1
  183. package/src/proofs/noir.ts +111 -30
  184. package/src/proofs/orchestrator.ts +960 -0
  185. package/src/proofs/parallel/concurrency.ts +297 -0
  186. package/src/proofs/parallel/dependency-graph.ts +602 -0
  187. package/src/proofs/parallel/executor.ts +420 -0
  188. package/src/proofs/parallel/index.ts +131 -0
  189. package/src/proofs/parallel/interface.ts +685 -0
  190. package/src/proofs/parallel/worker-pool.ts +644 -0
  191. package/src/proofs/providers/halo2.ts +560 -0
  192. package/src/proofs/providers/index.ts +34 -0
  193. package/src/proofs/providers/kimchi.ts +641 -0
  194. package/src/proofs/validator.ts +881 -0
  195. package/src/proofs/verifier.ts +867 -0
  196. package/src/quantum/index.ts +112 -0
  197. package/src/quantum/winternitz-vault.ts +639 -0
  198. package/src/quantum/wots.ts +611 -0
  199. package/src/settlement/backends/direct-chain.ts +1 -0
  200. package/src/settlement/index.ts +9 -0
  201. package/src/settlement/router.ts +732 -46
  202. package/src/solana/index.ts +72 -0
  203. package/src/solana/jito-relayer.ts +687 -0
  204. package/src/solana/noir-verifier-types.ts +430 -0
  205. package/src/solana/noir-verifier.ts +816 -0
  206. package/src/stealth/address-derivation.ts +193 -0
  207. package/src/stealth/ed25519.ts +431 -0
  208. package/src/stealth/index.ts +233 -0
  209. package/src/stealth/meta-address.ts +221 -0
  210. package/src/stealth/secp256k1.ts +368 -0
  211. package/src/stealth/utils.ts +194 -0
  212. package/src/stealth.ts +50 -1504
  213. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  214. package/src/surveillance/algorithms/cluster.ts +247 -0
  215. package/src/surveillance/algorithms/exchange.ts +295 -0
  216. package/src/surveillance/algorithms/temporal.ts +337 -0
  217. package/src/surveillance/analyzer.ts +442 -0
  218. package/src/surveillance/index.ts +64 -0
  219. package/src/surveillance/scoring.ts +372 -0
  220. package/src/surveillance/types.ts +264 -0
  221. package/src/sync/index.ts +106 -0
  222. package/src/sync/manager.ts +504 -0
  223. package/src/sync/mock-provider.ts +318 -0
  224. package/src/sync/oblivious.ts +625 -0
  225. package/src/tokens/index.ts +15 -0
  226. package/src/tokens/registry.ts +301 -0
  227. package/src/utils/deprecation.ts +94 -0
  228. package/src/utils/index.ts +9 -0
  229. package/src/wallet/ethereum/index.ts +68 -0
  230. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  231. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  232. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  233. package/src/wallet/ethereum/types.ts +3 -1
  234. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  235. package/src/wallet/hardware/index.ts +10 -0
  236. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  237. package/src/wallet/index.ts +71 -0
  238. package/src/wallet/near/adapter.ts +626 -0
  239. package/src/wallet/near/index.ts +86 -0
  240. package/src/wallet/near/meteor-wallet.ts +1153 -0
  241. package/src/wallet/near/my-near-wallet.ts +790 -0
  242. package/src/wallet/near/wallet-selector.ts +702 -0
  243. package/src/wallet/solana/adapter.ts +6 -4
  244. package/src/wallet/solana/index.ts +13 -0
  245. package/src/wallet/solana/privacy-adapter.ts +567 -0
  246. package/src/wallet/sui/types.ts +6 -4
  247. package/src/zcash/rpc-client.ts +13 -6
  248. package/dist/chunk-3INS3PR5.mjs +0 -884
  249. package/dist/chunk-3OVABDRH.mjs +0 -17096
  250. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  251. package/dist/chunk-E6SZWREQ.mjs +0 -57
  252. package/dist/chunk-G33LB27A.mjs +0 -16166
  253. package/dist/chunk-HGU6HZRC.mjs +0 -231
  254. package/dist/chunk-L2K34JCU.mjs +0 -1496
  255. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  256. package/dist/constants-VOI7BSLK.mjs +0 -27
  257. package/dist/index-BYZbDjal.d.ts +0 -11390
  258. package/dist/index-CHB3KuOB.d.mts +0 -11859
  259. package/dist/index-CzWPI6Le.d.ts +0 -11859
  260. package/dist/index-xbWjohNq.d.mts +0 -11390
  261. package/dist/solana-5EMCTPTS.mjs +0 -46
  262. 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
+ }