@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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/auto-enable.ts +76 -0
- package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
- package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
- package/dist/aerodrome-CfnESC32.mjs +890 -0
- package/dist/chunk-hT5z_Zn9.mjs +35 -0
- package/dist/index.d.mts +34727 -0
- package/dist/index.mjs +21590 -0
- package/dist/lib/server-wallet-trade.d.mts +34 -0
- package/dist/lib/server-wallet-trade.mjs +306 -0
- package/dist/meteora-BPX39hZo.mjs +22640 -0
- package/dist/orca-Bybp1HXO.mjs +249 -0
- package/dist/pancakeswp-CkEXlXti.mjs +604 -0
- package/dist/plugin-ZO_MTyd0.mjs +529 -0
- package/dist/raydium-rfaM9yEf.mjs +539 -0
- package/dist/sdk/index.d.mts +32492 -0
- package/dist/sdk/index.mjs +6415 -0
- package/dist/types-D5252NZk.mjs +487 -0
- package/dist/uniswap-CReXgXVN.mjs +573 -0
- package/dist/wallet-action.d.mts +6 -0
- package/dist/wallet-action.mjs +820 -0
- package/package.json +152 -0
- package/src/actions/failure-codes.ts +79 -0
- package/src/actions/index.ts +1 -0
- package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
- package/src/analytics/birdeye/birdeye-task.ts +175 -0
- package/src/analytics/birdeye/birdeye.ts +813 -0
- package/src/analytics/birdeye/constants.ts +74 -0
- package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
- package/src/analytics/birdeye/providers/market.ts +227 -0
- package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
- package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
- package/src/analytics/birdeye/providers/trending.ts +365 -0
- package/src/analytics/birdeye/providers/wallet.ts +14 -0
- package/src/analytics/birdeye/search-category.test.ts +207 -0
- package/src/analytics/birdeye/search-category.ts +506 -0
- package/src/analytics/birdeye/service.ts +992 -0
- package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
- package/src/analytics/birdeye/types/api/common.ts +305 -0
- package/src/analytics/birdeye/types/api/defi.ts +220 -0
- package/src/analytics/birdeye/types/api/pair.ts +200 -0
- package/src/analytics/birdeye/types/api/search.ts +86 -0
- package/src/analytics/birdeye/types/api/token.ts +635 -0
- package/src/analytics/birdeye/types/api/trader.ts +76 -0
- package/src/analytics/birdeye/types/api/wallet.ts +181 -0
- package/src/analytics/birdeye/types/shared.ts +106 -0
- package/src/analytics/birdeye/utils.ts +700 -0
- package/src/analytics/dexscreener/errors.ts +28 -0
- package/src/analytics/dexscreener/index.ts +3 -0
- package/src/analytics/dexscreener/search-category.test.ts +49 -0
- package/src/analytics/dexscreener/search-category.ts +42 -0
- package/src/analytics/dexscreener/service.ts +595 -0
- package/src/analytics/dexscreener/types.ts +128 -0
- package/src/analytics/lpinfo/index.d.ts +7 -0
- package/src/analytics/lpinfo/index.ts +52 -0
- package/src/analytics/lpinfo/kamino/README.md +102 -0
- package/src/analytics/lpinfo/kamino/index.ts +24 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
- package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
- package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
- package/src/analytics/lpinfo/steer/README.md +169 -0
- package/src/analytics/lpinfo/steer/index.ts +23 -0
- package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
- package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
- package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
- package/src/analytics/news/index.ts +52 -0
- package/src/analytics/news/interfaces/types.ts +222 -0
- package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
- package/src/analytics/news/services/newsDataService.ts +332 -0
- package/src/analytics/news/utils/formatters.ts +151 -0
- package/src/analytics/token-info/action.ts +240 -0
- package/src/analytics/token-info/index.ts +3 -0
- package/src/analytics/token-info/params.ts +215 -0
- package/src/analytics/token-info/providers.ts +681 -0
- package/src/analytics/token-info/service.ts +168 -0
- package/src/analytics/token-info/types.ts +74 -0
- package/src/audit/audit-log.ts +45 -0
- package/src/browser-shim/build-shim.ts +123 -0
- package/src/browser-shim/index.ts +5 -0
- package/src/browser-shim/shim.template.js +563 -0
- package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
- package/src/chains/evm/LICENSE +21 -0
- package/src/chains/evm/README.md +106 -0
- package/src/chains/evm/actions/helpers.ts +147 -0
- package/src/chains/evm/actions/swap.ts +839 -0
- package/src/chains/evm/actions/transfer.ts +254 -0
- package/src/chains/evm/biome.json +61 -0
- package/src/chains/evm/bridge-router.ts +660 -0
- package/src/chains/evm/build.ts +89 -0
- package/src/chains/evm/chain-handler.ts +416 -0
- package/src/chains/evm/constants.ts +23 -0
- package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
- package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
- package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
- package/src/chains/evm/dex/aerodrome/index.ts +34 -0
- package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
- package/src/chains/evm/dex/aerodrome/types.ts +318 -0
- package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
- package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
- package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
- package/src/chains/evm/dex/uniswap/index.ts +35 -0
- package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
- package/src/chains/evm/dex/uniswap/types.ts +390 -0
- package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
- package/src/chains/evm/generated/specs/specs.ts +151 -0
- package/src/chains/evm/gov-router.ts +250 -0
- package/src/chains/evm/index.browser.ts +16 -0
- package/src/chains/evm/index.ts +31 -0
- package/src/chains/evm/prompts.ts +193 -0
- package/src/chains/evm/providers/get-balance.ts +123 -0
- package/src/chains/evm/providers/wallet.ts +715 -0
- package/src/chains/evm/routes/sign.ts +333 -0
- package/src/chains/evm/rpc-providers.ts +410 -0
- package/src/chains/evm/service.ts +140 -0
- package/src/chains/evm/templates/index.ts +10 -0
- package/src/chains/evm/types/index.ts +432 -0
- package/src/chains/evm/vitest.config.ts +18 -0
- package/src/chains/registry.ts +668 -0
- package/src/chains/solana/README.md +367 -0
- package/src/chains/wallet-action.ts +533 -0
- package/src/chains/wallet-router.test.ts +296 -0
- package/src/contracts.ts +65 -0
- package/src/core-augmentation.ts +10 -0
- package/src/index.ts +71 -0
- package/src/lib/server-wallet-trade.ts +192 -0
- package/src/lib/wallet-export-guard.ts +330 -0
- package/src/lp/actions/liquidity.ts +827 -0
- package/src/lp/e2e/real-token-tests.ts +428 -0
- package/src/lp/e2e/scenarios.ts +470 -0
- package/src/lp/e2e/test-utils.ts +145 -0
- package/src/lp/lp-manager-entry.ts +303 -0
- package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
- package/src/lp/services/DexInteractionService.ts +226 -0
- package/src/lp/services/LpManagementService.test.ts +148 -0
- package/src/lp/services/LpManagementService.ts +632 -0
- package/src/lp/services/UserLpProfileService.ts +163 -0
- package/src/lp/services/VaultService.ts +153 -0
- package/src/lp/services/YieldOptimizationService.ts +344 -0
- package/src/lp/services/__tests__/MockLpService.ts +146 -0
- package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
- package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
- package/src/lp/types.ts +582 -0
- package/src/lp/utils/solanaClient.ts +143 -0
- package/src/plugin.ts +125 -0
- package/src/policy/policy.ts +19 -0
- package/src/providers/canonical-provider.ts +27 -0
- package/src/providers/unified-wallet-provider.ts +79 -0
- package/src/register-routes.ts +11 -0
- package/src/routes/plugin.ts +47 -0
- package/src/routes/wallet-market-overview-route.ts +869 -0
- package/src/sdk/abi.ts +258 -0
- package/src/sdk/bridge/abis.ts +126 -0
- package/src/sdk/bridge/client.ts +518 -0
- package/src/sdk/bridge/index.ts +56 -0
- package/src/sdk/bridge/solana.ts +604 -0
- package/src/sdk/bridge/types.ts +202 -0
- package/src/sdk/convenience.ts +347 -0
- package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
- package/src/sdk/escrow/types.ts +64 -0
- package/src/sdk/escrow/verifiers.ts +73 -0
- package/src/sdk/identity/erc8004.ts +692 -0
- package/src/sdk/identity/reputation.ts +449 -0
- package/src/sdk/identity/uaid.ts +497 -0
- package/src/sdk/identity/validation.ts +372 -0
- package/src/sdk/index.ts +763 -0
- package/src/sdk/policy/SpendingPolicy.ts +260 -0
- package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
- package/src/sdk/router/PaymentRouter.ts +215 -0
- package/src/sdk/router/index.ts +8 -0
- package/src/sdk/swap/SwapModule.ts +310 -0
- package/src/sdk/swap/abi.ts +117 -0
- package/src/sdk/swap/index.ts +34 -0
- package/src/sdk/swap/types.ts +135 -0
- package/src/sdk/tokens/decimals.ts +140 -0
- package/src/sdk/tokens/registry.ts +911 -0
- package/src/sdk/tokens/solana.ts +419 -0
- package/src/sdk/tokens/transfers.ts +327 -0
- package/src/sdk/types.ts +158 -0
- package/src/sdk/wallet-core.ts +115 -0
- package/src/sdk/x402/budget.ts +168 -0
- package/src/sdk/x402/chains/abstract/index.ts +280 -0
- package/src/sdk/x402/client.ts +320 -0
- package/src/sdk/x402/index.ts +46 -0
- package/src/sdk/x402/middleware.ts +92 -0
- package/src/sdk/x402/multi-asset.ts +144 -0
- package/src/sdk/x402/types.ts +156 -0
- package/src/services/wallet-backend-service.ts +328 -0
- package/src/types/wallet-router.ts +227 -0
- package/src/utils/intent-trajectory.ts +106 -0
- package/src/wallet/backend.ts +62 -0
- package/src/wallet/errors.ts +49 -0
- package/src/wallet/index.ts +27 -0
- package/src/wallet/local-eoa-backend.ts +201 -0
- package/src/wallet/pending.ts +60 -0
- package/src/wallet/select-backend.ts +47 -0
- package/src/wallet/steward-backend.ts +161 -0
- 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
|
+
}
|