@elizaos/plugin-wallet 2.0.0-beta.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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/auto-enable.ts +76 -0
  4. package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
  5. package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
  6. package/dist/aerodrome-CfnESC32.mjs +890 -0
  7. package/dist/chunk-hT5z_Zn9.mjs +35 -0
  8. package/dist/index.d.mts +34727 -0
  9. package/dist/index.mjs +21590 -0
  10. package/dist/lib/server-wallet-trade.d.mts +34 -0
  11. package/dist/lib/server-wallet-trade.mjs +306 -0
  12. package/dist/meteora-BPX39hZo.mjs +22640 -0
  13. package/dist/orca-Bybp1HXO.mjs +249 -0
  14. package/dist/pancakeswp-CkEXlXti.mjs +604 -0
  15. package/dist/plugin-ZO_MTyd0.mjs +529 -0
  16. package/dist/raydium-rfaM9yEf.mjs +539 -0
  17. package/dist/sdk/index.d.mts +32492 -0
  18. package/dist/sdk/index.mjs +6415 -0
  19. package/dist/types-D5252NZk.mjs +487 -0
  20. package/dist/uniswap-CReXgXVN.mjs +573 -0
  21. package/dist/wallet-action.d.mts +6 -0
  22. package/dist/wallet-action.mjs +820 -0
  23. package/package.json +152 -0
  24. package/src/actions/failure-codes.ts +79 -0
  25. package/src/actions/index.ts +1 -0
  26. package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
  27. package/src/analytics/birdeye/birdeye-task.ts +175 -0
  28. package/src/analytics/birdeye/birdeye.ts +813 -0
  29. package/src/analytics/birdeye/constants.ts +74 -0
  30. package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
  31. package/src/analytics/birdeye/providers/market.ts +227 -0
  32. package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
  33. package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
  34. package/src/analytics/birdeye/providers/trending.ts +365 -0
  35. package/src/analytics/birdeye/providers/wallet.ts +14 -0
  36. package/src/analytics/birdeye/search-category.test.ts +207 -0
  37. package/src/analytics/birdeye/search-category.ts +506 -0
  38. package/src/analytics/birdeye/service.ts +992 -0
  39. package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
  40. package/src/analytics/birdeye/types/api/common.ts +305 -0
  41. package/src/analytics/birdeye/types/api/defi.ts +220 -0
  42. package/src/analytics/birdeye/types/api/pair.ts +200 -0
  43. package/src/analytics/birdeye/types/api/search.ts +86 -0
  44. package/src/analytics/birdeye/types/api/token.ts +635 -0
  45. package/src/analytics/birdeye/types/api/trader.ts +76 -0
  46. package/src/analytics/birdeye/types/api/wallet.ts +181 -0
  47. package/src/analytics/birdeye/types/shared.ts +106 -0
  48. package/src/analytics/birdeye/utils.ts +700 -0
  49. package/src/analytics/dexscreener/errors.ts +28 -0
  50. package/src/analytics/dexscreener/index.ts +3 -0
  51. package/src/analytics/dexscreener/search-category.test.ts +49 -0
  52. package/src/analytics/dexscreener/search-category.ts +42 -0
  53. package/src/analytics/dexscreener/service.ts +595 -0
  54. package/src/analytics/dexscreener/types.ts +128 -0
  55. package/src/analytics/lpinfo/index.d.ts +7 -0
  56. package/src/analytics/lpinfo/index.ts +52 -0
  57. package/src/analytics/lpinfo/kamino/README.md +102 -0
  58. package/src/analytics/lpinfo/kamino/index.ts +24 -0
  59. package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
  60. package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
  61. package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
  62. package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
  63. package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
  64. package/src/analytics/lpinfo/steer/README.md +169 -0
  65. package/src/analytics/lpinfo/steer/index.ts +23 -0
  66. package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
  67. package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
  68. package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
  69. package/src/analytics/news/index.ts +52 -0
  70. package/src/analytics/news/interfaces/types.ts +222 -0
  71. package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
  72. package/src/analytics/news/services/newsDataService.ts +332 -0
  73. package/src/analytics/news/utils/formatters.ts +151 -0
  74. package/src/analytics/token-info/action.ts +240 -0
  75. package/src/analytics/token-info/index.ts +3 -0
  76. package/src/analytics/token-info/params.ts +215 -0
  77. package/src/analytics/token-info/providers.ts +681 -0
  78. package/src/analytics/token-info/service.ts +168 -0
  79. package/src/analytics/token-info/types.ts +74 -0
  80. package/src/audit/audit-log.ts +45 -0
  81. package/src/browser-shim/build-shim.ts +123 -0
  82. package/src/browser-shim/index.ts +5 -0
  83. package/src/browser-shim/shim.template.js +563 -0
  84. package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
  85. package/src/chains/evm/LICENSE +21 -0
  86. package/src/chains/evm/README.md +106 -0
  87. package/src/chains/evm/actions/helpers.ts +147 -0
  88. package/src/chains/evm/actions/swap.ts +839 -0
  89. package/src/chains/evm/actions/transfer.ts +254 -0
  90. package/src/chains/evm/biome.json +61 -0
  91. package/src/chains/evm/bridge-router.ts +660 -0
  92. package/src/chains/evm/build.ts +89 -0
  93. package/src/chains/evm/chain-handler.ts +416 -0
  94. package/src/chains/evm/constants.ts +23 -0
  95. package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
  96. package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
  97. package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
  98. package/src/chains/evm/dex/aerodrome/index.ts +34 -0
  99. package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
  100. package/src/chains/evm/dex/aerodrome/types.ts +318 -0
  101. package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
  102. package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
  103. package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
  104. package/src/chains/evm/dex/uniswap/index.ts +35 -0
  105. package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
  106. package/src/chains/evm/dex/uniswap/types.ts +390 -0
  107. package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
  108. package/src/chains/evm/generated/specs/specs.ts +151 -0
  109. package/src/chains/evm/gov-router.ts +250 -0
  110. package/src/chains/evm/index.browser.ts +16 -0
  111. package/src/chains/evm/index.ts +31 -0
  112. package/src/chains/evm/prompts.ts +193 -0
  113. package/src/chains/evm/providers/get-balance.ts +123 -0
  114. package/src/chains/evm/providers/wallet.ts +715 -0
  115. package/src/chains/evm/routes/sign.ts +333 -0
  116. package/src/chains/evm/rpc-providers.ts +410 -0
  117. package/src/chains/evm/service.ts +140 -0
  118. package/src/chains/evm/templates/index.ts +10 -0
  119. package/src/chains/evm/types/index.ts +432 -0
  120. package/src/chains/evm/vitest.config.ts +18 -0
  121. package/src/chains/registry.ts +668 -0
  122. package/src/chains/solana/README.md +367 -0
  123. package/src/chains/wallet-action.ts +533 -0
  124. package/src/chains/wallet-router.test.ts +296 -0
  125. package/src/contracts.ts +65 -0
  126. package/src/core-augmentation.ts +10 -0
  127. package/src/index.ts +71 -0
  128. package/src/lib/server-wallet-trade.ts +192 -0
  129. package/src/lib/wallet-export-guard.ts +330 -0
  130. package/src/lp/actions/liquidity.ts +827 -0
  131. package/src/lp/e2e/real-token-tests.ts +428 -0
  132. package/src/lp/e2e/scenarios.ts +470 -0
  133. package/src/lp/e2e/test-utils.ts +145 -0
  134. package/src/lp/lp-manager-entry.ts +303 -0
  135. package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
  136. package/src/lp/services/DexInteractionService.ts +226 -0
  137. package/src/lp/services/LpManagementService.test.ts +148 -0
  138. package/src/lp/services/LpManagementService.ts +632 -0
  139. package/src/lp/services/UserLpProfileService.ts +163 -0
  140. package/src/lp/services/VaultService.ts +153 -0
  141. package/src/lp/services/YieldOptimizationService.ts +344 -0
  142. package/src/lp/services/__tests__/MockLpService.ts +146 -0
  143. package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
  144. package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
  145. package/src/lp/types.ts +582 -0
  146. package/src/lp/utils/solanaClient.ts +143 -0
  147. package/src/plugin.ts +125 -0
  148. package/src/policy/policy.ts +19 -0
  149. package/src/providers/canonical-provider.ts +27 -0
  150. package/src/providers/unified-wallet-provider.ts +79 -0
  151. package/src/register-routes.ts +11 -0
  152. package/src/routes/plugin.ts +47 -0
  153. package/src/routes/wallet-market-overview-route.ts +869 -0
  154. package/src/sdk/abi.ts +258 -0
  155. package/src/sdk/bridge/abis.ts +126 -0
  156. package/src/sdk/bridge/client.ts +518 -0
  157. package/src/sdk/bridge/index.ts +56 -0
  158. package/src/sdk/bridge/solana.ts +604 -0
  159. package/src/sdk/bridge/types.ts +202 -0
  160. package/src/sdk/convenience.ts +347 -0
  161. package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
  162. package/src/sdk/escrow/types.ts +64 -0
  163. package/src/sdk/escrow/verifiers.ts +73 -0
  164. package/src/sdk/identity/erc8004.ts +692 -0
  165. package/src/sdk/identity/reputation.ts +449 -0
  166. package/src/sdk/identity/uaid.ts +497 -0
  167. package/src/sdk/identity/validation.ts +372 -0
  168. package/src/sdk/index.ts +763 -0
  169. package/src/sdk/policy/SpendingPolicy.ts +260 -0
  170. package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
  171. package/src/sdk/router/PaymentRouter.ts +215 -0
  172. package/src/sdk/router/index.ts +8 -0
  173. package/src/sdk/swap/SwapModule.ts +310 -0
  174. package/src/sdk/swap/abi.ts +117 -0
  175. package/src/sdk/swap/index.ts +34 -0
  176. package/src/sdk/swap/types.ts +135 -0
  177. package/src/sdk/tokens/decimals.ts +140 -0
  178. package/src/sdk/tokens/registry.ts +911 -0
  179. package/src/sdk/tokens/solana.ts +419 -0
  180. package/src/sdk/tokens/transfers.ts +327 -0
  181. package/src/sdk/types.ts +158 -0
  182. package/src/sdk/wallet-core.ts +115 -0
  183. package/src/sdk/x402/budget.ts +168 -0
  184. package/src/sdk/x402/chains/abstract/index.ts +280 -0
  185. package/src/sdk/x402/client.ts +320 -0
  186. package/src/sdk/x402/index.ts +46 -0
  187. package/src/sdk/x402/middleware.ts +92 -0
  188. package/src/sdk/x402/multi-asset.ts +144 -0
  189. package/src/sdk/x402/types.ts +156 -0
  190. package/src/services/wallet-backend-service.ts +328 -0
  191. package/src/types/wallet-router.ts +227 -0
  192. package/src/utils/intent-trajectory.ts +106 -0
  193. package/src/wallet/backend.ts +62 -0
  194. package/src/wallet/errors.ts +49 -0
  195. package/src/wallet/index.ts +27 -0
  196. package/src/wallet/local-eoa-backend.ts +201 -0
  197. package/src/wallet/pending.ts +60 -0
  198. package/src/wallet/select-backend.ts +47 -0
  199. package/src/wallet/steward-backend.ts +161 -0
  200. package/src/wallet-action.ts +1 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Hardened wallet private key export guard.
3
+ *
4
+ * Wraps the upstream resolveWalletExportRejection with:
5
+ * 1. Per-IP rate limiting (1 successful export per 10 minutes)
6
+ * 2. Audit logging with IP, User-Agent, and timestamp
7
+ * 3. Forced confirmation delay (10s countdown)
8
+ *
9
+ * The upstream function validates the export token. This module adds
10
+ * defence-in-depth so a compromised session cannot instantly extract
11
+ * keys without leaving an audit trail and hitting rate limits.
12
+ *
13
+ * Exported from the `@elizaos/plugin-wallet` barrel for package consumers.
14
+ */
15
+
16
+ import crypto from "node:crypto";
17
+ import type http from "node:http";
18
+ import type {
19
+ WalletExportRejection,
20
+ WalletExportRequestBody,
21
+ } from "../contracts.js";
22
+
23
+ export type { WalletExportRejection };
24
+
25
+ type UpstreamRejectionFn = (
26
+ req: http.IncomingMessage,
27
+ body: WalletExportRequestBody,
28
+ ) => WalletExportRejection | null;
29
+
30
+ // ── Rate limiter state ───────────────────────────────────────────────────────
31
+
32
+ interface RateLimitEntry {
33
+ lastExportAt: number;
34
+ }
35
+
36
+ const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
37
+ const RATE_LIMIT_SWEEP_INTERVAL_MS = 15 * 60 * 1000; // sweep stale entries
38
+
39
+ const rateLimitMap = new Map<string, RateLimitEntry>();
40
+
41
+ // Periodic sweep to prevent unbounded memory growth
42
+ const sweepTimer = setInterval(() => {
43
+ const now = Date.now();
44
+ for (const [key, entry] of rateLimitMap) {
45
+ if (now - entry.lastExportAt > RATE_LIMIT_WINDOW_MS * 2) {
46
+ rateLimitMap.delete(key);
47
+ }
48
+ }
49
+ }, RATE_LIMIT_SWEEP_INTERVAL_MS);
50
+
51
+ // Allow the process to exit without this timer holding it
52
+ if (typeof sweepTimer === "object" && "unref" in sweepTimer) {
53
+ sweepTimer.unref();
54
+ }
55
+
56
+ // ── Helpers ──────────────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Get client IP from the socket directly. X-Forwarded-For is not trusted
60
+ * because this is a local server — trusting XFF would let attackers spoof
61
+ * IPs to bypass rate limits and nonce IP binding.
62
+ */
63
+ function getClientIp(req: http.IncomingMessage): string | null {
64
+ return req.socket?.remoteAddress ?? null;
65
+ }
66
+
67
+ function getUserAgent(req: http.IncomingMessage): string {
68
+ return (req.headers["user-agent"] as string) ?? "unknown";
69
+ }
70
+
71
+ // ── Audit log ────────────────────────────────────────────────────────────────
72
+
73
+ export interface WalletExportAuditEntry {
74
+ timestamp: string;
75
+ ip: string;
76
+ userAgent: string;
77
+ outcome: "allowed" | "rate-limited" | "rejected";
78
+ reason?: string;
79
+ }
80
+
81
+ // Keep last 100 entries in memory for diagnostics; also write to logger
82
+ const auditLog: WalletExportAuditEntry[] = [];
83
+ const MAX_AUDIT_ENTRIES = 100;
84
+
85
+ function recordAudit(entry: WalletExportAuditEntry): void {
86
+ auditLog.push(entry);
87
+ if (auditLog.length > MAX_AUDIT_ENTRIES) {
88
+ auditLog.shift();
89
+ }
90
+
91
+ const logLine = `[wallet-export-audit] ${entry.outcome} ip=${entry.ip} ua="${entry.userAgent}"${entry.reason ? ` reason="${entry.reason}"` : ""}`;
92
+ console.warn(logLine);
93
+ }
94
+
95
+ /** Read-only snapshot of the audit log for diagnostics endpoints. */
96
+ export function getWalletExportAuditLog(): ReadonlyArray<WalletExportAuditEntry> {
97
+ return [...auditLog];
98
+ }
99
+
100
+ /** Reset all internal state (rate limits, nonces, audit log). Test-only. */
101
+ export function _resetForTesting(): void {
102
+ if (process.env.NODE_ENV === "production") return;
103
+ rateLimitMap.clear();
104
+ pendingExportNonces.clear();
105
+ auditLog.length = 0;
106
+ }
107
+
108
+ // ── Confirmation delay ───────────────────────────────────────────────────────
109
+
110
+ const EXPORT_DELAY_MS = 10_000; // 10 seconds
111
+ const MAX_PENDING_NONCES_PER_IP = 3;
112
+
113
+ /**
114
+ * Issue a time-limited export nonce. The client must wait at least
115
+ * EXPORT_DELAY_MS before submitting the actual export request with this nonce.
116
+ */
117
+ const pendingExportNonces = new Map<string, { issuedAt: number; ip: string }>();
118
+ const NONCE_TTL_MS = 5 * 60 * 1000; // 5 minutes
119
+
120
+ function issueExportNonce(ip: string): string | null {
121
+ // Sweep expired nonces
122
+ const now = Date.now();
123
+ for (const [key, value] of pendingExportNonces) {
124
+ if (now - value.issuedAt > NONCE_TTL_MS) {
125
+ pendingExportNonces.delete(key);
126
+ }
127
+ }
128
+
129
+ // Cap pending nonces per IP to prevent unbounded growth from repeated
130
+ // requestNonce calls (which are rate-limit-exempt).
131
+ let countForIp = 0;
132
+ for (const entry of pendingExportNonces.values()) {
133
+ if (entry.ip === ip) countForIp++;
134
+ }
135
+ if (countForIp >= MAX_PENDING_NONCES_PER_IP) {
136
+ return null;
137
+ }
138
+
139
+ const nonce = `wxn_${crypto.randomBytes(16).toString("hex")}`;
140
+ pendingExportNonces.set(nonce, { issuedAt: Date.now(), ip });
141
+
142
+ return nonce;
143
+ }
144
+
145
+ function validateExportNonce(
146
+ nonce: string,
147
+ ip: string,
148
+ ): { valid: true } | { valid: false; reason: string } {
149
+ const entry = pendingExportNonces.get(nonce);
150
+ if (!entry) {
151
+ return { valid: false, reason: "Invalid or expired export nonce." };
152
+ }
153
+
154
+ if (entry.ip !== ip) {
155
+ return {
156
+ valid: false,
157
+ reason: "Export nonce was issued to a different client.",
158
+ };
159
+ }
160
+
161
+ const elapsed = Date.now() - entry.issuedAt;
162
+ if (elapsed < EXPORT_DELAY_MS) {
163
+ const remaining = Math.ceil((EXPORT_DELAY_MS - elapsed) / 1000);
164
+ return {
165
+ valid: false,
166
+ reason: `Export confirmation delay not met. Wait ${remaining} more seconds.`,
167
+ };
168
+ }
169
+
170
+ // Nonce consumed — delete it
171
+ pendingExportNonces.delete(nonce);
172
+ return { valid: true };
173
+ }
174
+
175
+ // ── Extended request body (adds nonce field) ─────────────────────────────────
176
+
177
+ interface HardenedExportRequestBody extends WalletExportRequestBody {
178
+ exportNonce?: string;
179
+ /** Client sends requestNonce: true to start the countdown flow. */
180
+ requestNonce?: boolean;
181
+ }
182
+
183
+ // ── Main guard ───────────────────────────────────────────────────────────────
184
+
185
+ /**
186
+ * Create a hardened wallet export rejection function that wraps the upstream
187
+ * token validation with rate limiting, audit logging, and a forced delay.
188
+ *
189
+ * Two-phase export flow:
190
+ * 1. POST /api/wallet/export { confirm: true, exportToken: "...", requestNonce: true }
191
+ * → 403 with { nonce, delaySeconds } — client must wait
192
+ * 2. POST /api/wallet/export { confirm: true, exportToken: "...", exportNonce: "wxn_..." }
193
+ * → 200 with keys (if delay elapsed and rate limit not hit)
194
+ */
195
+ export function createHardenedExportGuard(
196
+ upstream: UpstreamRejectionFn,
197
+ ): (
198
+ req: http.IncomingMessage,
199
+ body: HardenedExportRequestBody,
200
+ ) => WalletExportRejection | null {
201
+ return (
202
+ req: http.IncomingMessage,
203
+ body: HardenedExportRequestBody,
204
+ ): WalletExportRejection | null => {
205
+ const ip = getClientIp(req);
206
+ const ua = getUserAgent(req);
207
+
208
+ // Reject requests with no identifiable client IP — without an IP,
209
+ // rate-limit and nonce-binding keys collapse, letting unrelated
210
+ // requests share a single bucket.
211
+ if (!ip) {
212
+ recordAudit({
213
+ timestamp: new Date().toISOString(),
214
+ ip: "unknown",
215
+ userAgent: ua,
216
+ outcome: "rejected",
217
+ reason: "No client IP available on socket",
218
+ });
219
+ return {
220
+ status: 400,
221
+ reason: "Unable to determine client IP; request rejected.",
222
+ };
223
+ }
224
+
225
+ // 1. Run upstream validation first (token check, confirm flag)
226
+ const upstreamRejection = upstream(req, body);
227
+ if (upstreamRejection) {
228
+ recordAudit({
229
+ timestamp: new Date().toISOString(),
230
+ ip,
231
+ userAgent: ua,
232
+ outcome: "rejected",
233
+ reason: upstreamRejection.reason,
234
+ });
235
+ return upstreamRejection;
236
+ }
237
+
238
+ // 2. Nonce/delay flow — nonce requests are always allowed (no rate limit)
239
+ if (body.requestNonce) {
240
+ const nonce = issueExportNonce(ip);
241
+ if (!nonce) {
242
+ recordAudit({
243
+ timestamp: new Date().toISOString(),
244
+ ip,
245
+ userAgent: ua,
246
+ outcome: "rejected",
247
+ reason: "Too many pending nonces for this IP",
248
+ });
249
+ return {
250
+ status: 429,
251
+ reason: `Too many pending export requests. Complete or wait for existing nonces to expire.`,
252
+ };
253
+ }
254
+ recordAudit({
255
+ timestamp: new Date().toISOString(),
256
+ ip,
257
+ userAgent: ua,
258
+ outcome: "rejected",
259
+ reason: "Nonce issued, waiting for confirmation delay",
260
+ });
261
+ return {
262
+ status: 403,
263
+ reason: JSON.stringify({
264
+ countdown: true,
265
+ nonce,
266
+ delaySeconds: EXPORT_DELAY_MS / 1000,
267
+ message: `Export nonce issued. Wait ${EXPORT_DELAY_MS / 1000} seconds, then re-submit with exportNonce: "${nonce}".`,
268
+ }),
269
+ };
270
+ }
271
+
272
+ if (!body.exportNonce) {
273
+ recordAudit({
274
+ timestamp: new Date().toISOString(),
275
+ ip,
276
+ userAgent: ua,
277
+ outcome: "rejected",
278
+ reason: "Missing export nonce",
279
+ });
280
+ return {
281
+ status: 403,
282
+ reason:
283
+ 'Export requires a confirmation delay. First send { "confirm": true, "exportToken": "...", "requestNonce": true } to start the countdown.',
284
+ };
285
+ }
286
+
287
+ const nonceResult = validateExportNonce(body.exportNonce, ip);
288
+ if (!nonceResult.valid) {
289
+ recordAudit({
290
+ timestamp: new Date().toISOString(),
291
+ ip,
292
+ userAgent: ua,
293
+ outcome: "rejected",
294
+ reason: nonceResult.reason,
295
+ });
296
+ return { status: 403, reason: nonceResult.reason };
297
+ }
298
+
299
+ // 3. Rate limit check (after nonce validation, before key export)
300
+ const rateLimitEntry = rateLimitMap.get(ip);
301
+ if (rateLimitEntry) {
302
+ const elapsed = Date.now() - rateLimitEntry.lastExportAt;
303
+ if (elapsed < RATE_LIMIT_WINDOW_MS) {
304
+ const retryAfter = Math.ceil((RATE_LIMIT_WINDOW_MS - elapsed) / 1000);
305
+ recordAudit({
306
+ timestamp: new Date().toISOString(),
307
+ ip,
308
+ userAgent: ua,
309
+ outcome: "rate-limited",
310
+ reason: `Rate limited, retry after ${retryAfter}s`,
311
+ });
312
+ return {
313
+ status: 429,
314
+ reason: `Rate limit exceeded. One export per ${RATE_LIMIT_WINDOW_MS / 60_000} minutes. Retry after ${retryAfter} seconds.`,
315
+ };
316
+ }
317
+ }
318
+
319
+ // 4. All checks passed — record rate limit + audit
320
+ rateLimitMap.set(ip, { lastExportAt: Date.now() });
321
+ recordAudit({
322
+ timestamp: new Date().toISOString(),
323
+ ip,
324
+ userAgent: ua,
325
+ outcome: "allowed",
326
+ });
327
+
328
+ return null; // allow export
329
+ };
330
+ }