@scriptmasterlabs/mcp-x402 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +35 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/keepalive.yml +31 -0
- package/.well-known/agentcard.json +34 -0
- package/CONTRIBUTING.md +76 -0
- package/Dockerfile +19 -0
- package/LICENSE +21 -0
- package/README.md +304 -0
- package/agents.json +67 -0
- package/dist/lib/chains/base.d.ts +10 -0
- package/dist/lib/chains/base.d.ts.map +1 -0
- package/dist/lib/chains/base.js +73 -0
- package/dist/lib/chains/base.js.map +1 -0
- package/dist/lib/chains/solana.d.ts +10 -0
- package/dist/lib/chains/solana.d.ts.map +1 -0
- package/dist/lib/chains/solana.js +49 -0
- package/dist/lib/chains/solana.js.map +1 -0
- package/dist/lib/chains/xrpl.d.ts +10 -0
- package/dist/lib/chains/xrpl.d.ts.map +1 -0
- package/dist/lib/chains/xrpl.js +55 -0
- package/dist/lib/chains/xrpl.js.map +1 -0
- package/dist/lib/credit/bureau.d.ts +10 -0
- package/dist/lib/credit/bureau.d.ts.map +1 -0
- package/dist/lib/credit/bureau.js +58 -0
- package/dist/lib/credit/bureau.js.map +1 -0
- package/dist/lib/sml-api/agentcard.d.ts +17 -0
- package/dist/lib/sml-api/agentcard.d.ts.map +1 -0
- package/dist/lib/sml-api/agentcard.js +30 -0
- package/dist/lib/sml-api/agentcard.js.map +1 -0
- package/dist/lib/sml-api/backtest.d.ts +22 -0
- package/dist/lib/sml-api/backtest.d.ts.map +1 -0
- package/dist/lib/sml-api/backtest.js +28 -0
- package/dist/lib/sml-api/backtest.js.map +1 -0
- package/dist/lib/sml-api/brokers.d.ts +40 -0
- package/dist/lib/sml-api/brokers.d.ts.map +1 -0
- package/dist/lib/sml-api/brokers.js +128 -0
- package/dist/lib/sml-api/brokers.js.map +1 -0
- package/dist/lib/sml-api/copytrader.d.ts +11 -0
- package/dist/lib/sml-api/copytrader.d.ts.map +1 -0
- package/dist/lib/sml-api/copytrader.js +30 -0
- package/dist/lib/sml-api/copytrader.js.map +1 -0
- package/dist/lib/sml-api/crawl.d.ts +20 -0
- package/dist/lib/sml-api/crawl.d.ts.map +1 -0
- package/dist/lib/sml-api/crawl.js +32 -0
- package/dist/lib/sml-api/crawl.js.map +1 -0
- package/dist/lib/sml-api/echo.d.ts +10 -0
- package/dist/lib/sml-api/echo.d.ts.map +1 -0
- package/dist/lib/sml-api/echo.js +23 -0
- package/dist/lib/sml-api/echo.js.map +1 -0
- package/dist/lib/sml-api/forge.d.ts +11 -0
- package/dist/lib/sml-api/forge.d.ts.map +1 -0
- package/dist/lib/sml-api/forge.js +29 -0
- package/dist/lib/sml-api/forge.js.map +1 -0
- package/dist/lib/sml-api/ftd.d.ts +18 -0
- package/dist/lib/sml-api/ftd.d.ts.map +1 -0
- package/dist/lib/sml-api/ftd.js +43 -0
- package/dist/lib/sml-api/ftd.js.map +1 -0
- package/dist/lib/sml-api/ghost.d.ts +13 -0
- package/dist/lib/sml-api/ghost.d.ts.map +1 -0
- package/dist/lib/sml-api/ghost.js +29 -0
- package/dist/lib/sml-api/ghost.js.map +1 -0
- package/dist/lib/sml-api/launchpad.d.ts +20 -0
- package/dist/lib/sml-api/launchpad.d.ts.map +1 -0
- package/dist/lib/sml-api/launchpad.js +31 -0
- package/dist/lib/sml-api/launchpad.js.map +1 -0
- package/dist/lib/sml-api/leviathan.d.ts +22 -0
- package/dist/lib/sml-api/leviathan.d.ts.map +1 -0
- package/dist/lib/sml-api/leviathan.js +33 -0
- package/dist/lib/sml-api/leviathan.js.map +1 -0
- package/dist/lib/sml-api/nexus.d.ts +18 -0
- package/dist/lib/sml-api/nexus.d.ts.map +1 -0
- package/dist/lib/sml-api/nexus.js +40 -0
- package/dist/lib/sml-api/nexus.js.map +1 -0
- package/dist/lib/sml-api/proof402.d.ts +6 -0
- package/dist/lib/sml-api/proof402.d.ts.map +1 -0
- package/dist/lib/sml-api/proof402.js +30 -0
- package/dist/lib/sml-api/proof402.js.map +1 -0
- package/dist/lib/sml-api/rails.d.ts +12 -0
- package/dist/lib/sml-api/rails.d.ts.map +1 -0
- package/dist/lib/sml-api/rails.js +29 -0
- package/dist/lib/sml-api/rails.js.map +1 -0
- package/dist/lib/sml-api/shadow.d.ts +15 -0
- package/dist/lib/sml-api/shadow.d.ts.map +1 -0
- package/dist/lib/sml-api/shadow.js +27 -0
- package/dist/lib/sml-api/shadow.js.map +1 -0
- package/dist/lib/sml-api/squeezeos.d.ts +21 -0
- package/dist/lib/sml-api/squeezeos.d.ts.map +1 -0
- package/dist/lib/sml-api/squeezeos.js +97 -0
- package/dist/lib/sml-api/squeezeos.js.map +1 -0
- package/dist/lib/sml-api/xdeo.d.ts +13 -0
- package/dist/lib/sml-api/xdeo.d.ts.map +1 -0
- package/dist/lib/sml-api/xdeo.js +34 -0
- package/dist/lib/sml-api/xdeo.js.map +1 -0
- package/dist/lib/sml-api/xmit.d.ts +13 -0
- package/dist/lib/sml-api/xmit.d.ts.map +1 -0
- package/dist/lib/sml-api/xmit.js +34 -0
- package/dist/lib/sml-api/xmit.js.map +1 -0
- package/dist/server/health.d.ts +16 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +39 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +193 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/payments/ap2.d.ts +17 -0
- package/dist/server/payments/ap2.d.ts.map +1 -0
- package/dist/server/payments/ap2.js +75 -0
- package/dist/server/payments/ap2.js.map +1 -0
- package/dist/server/payments/receipt.d.ts +28 -0
- package/dist/server/payments/receipt.d.ts.map +1 -0
- package/dist/server/payments/receipt.js +60 -0
- package/dist/server/payments/receipt.js.map +1 -0
- package/dist/server/payments/router.d.ts +23 -0
- package/dist/server/payments/router.d.ts.map +1 -0
- package/dist/server/payments/router.js +69 -0
- package/dist/server/payments/router.js.map +1 -0
- package/dist/server/payments/wallet.d.ts +18 -0
- package/dist/server/payments/wallet.d.ts.map +1 -0
- package/dist/server/payments/wallet.js +107 -0
- package/dist/server/payments/wallet.js.map +1 -0
- package/dist/server/payments/x402.d.ts +29 -0
- package/dist/server/payments/x402.d.ts.map +1 -0
- package/dist/server/payments/x402.js +122 -0
- package/dist/server/payments/x402.js.map +1 -0
- package/dist/server/registry/catalog.d.ts +12 -0
- package/dist/server/registry/catalog.d.ts.map +1 -0
- package/dist/server/registry/catalog.js +55 -0
- package/dist/server/registry/catalog.js.map +1 -0
- package/dist/server/registry/discovery.d.ts +16 -0
- package/dist/server/registry/discovery.d.ts.map +1 -0
- package/dist/server/registry/discovery.js +33 -0
- package/dist/server/registry/discovery.js.map +1 -0
- package/dist/server/registry/pricing.d.ts +10 -0
- package/dist/server/registry/pricing.d.ts.map +1 -0
- package/dist/server/registry/pricing.js +66 -0
- package/dist/server/registry/pricing.js.map +1 -0
- package/dist/server/security/acl.d.ts +28 -0
- package/dist/server/security/acl.d.ts.map +1 -0
- package/dist/server/security/acl.js +36 -0
- package/dist/server/security/acl.js.map +1 -0
- package/dist/server/security/audit.d.ts +15 -0
- package/dist/server/security/audit.d.ts.map +1 -0
- package/dist/server/security/audit.js +77 -0
- package/dist/server/security/audit.js.map +1 -0
- package/dist/server/security/rate-limit.d.ts +12 -0
- package/dist/server/security/rate-limit.d.ts.map +1 -0
- package/dist/server/security/rate-limit.js +72 -0
- package/dist/server/security/rate-limit.js.map +1 -0
- package/dist/server/security/sandbox.d.ts +7 -0
- package/dist/server/security/sandbox.d.ts.map +1 -0
- package/dist/server/security/sandbox.js +42 -0
- package/dist/server/security/sandbox.js.map +1 -0
- package/dist/server/tools/agentcard.d.ts +3 -0
- package/dist/server/tools/agentcard.d.ts.map +1 -0
- package/dist/server/tools/agentcard.js +118 -0
- package/dist/server/tools/agentcard.js.map +1 -0
- package/dist/server/tools/backtest.d.ts +3 -0
- package/dist/server/tools/backtest.d.ts.map +1 -0
- package/dist/server/tools/backtest.js +112 -0
- package/dist/server/tools/backtest.js.map +1 -0
- package/dist/server/tools/brokers.d.ts +3 -0
- package/dist/server/tools/brokers.d.ts.map +1 -0
- package/dist/server/tools/brokers.js +223 -0
- package/dist/server/tools/brokers.js.map +1 -0
- package/dist/server/tools/copytrader.d.ts +3 -0
- package/dist/server/tools/copytrader.d.ts.map +1 -0
- package/dist/server/tools/copytrader.js +90 -0
- package/dist/server/tools/copytrader.js.map +1 -0
- package/dist/server/tools/crawl.d.ts +3 -0
- package/dist/server/tools/crawl.d.ts.map +1 -0
- package/dist/server/tools/crawl.js +60 -0
- package/dist/server/tools/crawl.js.map +1 -0
- package/dist/server/tools/discovery.d.ts +3 -0
- package/dist/server/tools/discovery.d.ts.map +1 -0
- package/dist/server/tools/discovery.js +188 -0
- package/dist/server/tools/discovery.js.map +1 -0
- package/dist/server/tools/echo.d.ts +3 -0
- package/dist/server/tools/echo.d.ts.map +1 -0
- package/dist/server/tools/echo.js +48 -0
- package/dist/server/tools/echo.js.map +1 -0
- package/dist/server/tools/forge.d.ts +3 -0
- package/dist/server/tools/forge.d.ts.map +1 -0
- package/dist/server/tools/forge.js +77 -0
- package/dist/server/tools/forge.js.map +1 -0
- package/dist/server/tools/ftd.d.ts +3 -0
- package/dist/server/tools/ftd.d.ts.map +1 -0
- package/dist/server/tools/ftd.js +70 -0
- package/dist/server/tools/ftd.js.map +1 -0
- package/dist/server/tools/ghost.d.ts +3 -0
- package/dist/server/tools/ghost.d.ts.map +1 -0
- package/dist/server/tools/ghost.js +83 -0
- package/dist/server/tools/ghost.js.map +1 -0
- package/dist/server/tools/index.d.ts +3 -0
- package/dist/server/tools/index.d.ts.map +1 -0
- package/dist/server/tools/index.js +44 -0
- package/dist/server/tools/index.js.map +1 -0
- package/dist/server/tools/launchpad.d.ts +3 -0
- package/dist/server/tools/launchpad.d.ts.map +1 -0
- package/dist/server/tools/launchpad.js +151 -0
- package/dist/server/tools/launchpad.js.map +1 -0
- package/dist/server/tools/leviathan.d.ts +3 -0
- package/dist/server/tools/leviathan.d.ts.map +1 -0
- package/dist/server/tools/leviathan.js +73 -0
- package/dist/server/tools/leviathan.js.map +1 -0
- package/dist/server/tools/nexus.d.ts +3 -0
- package/dist/server/tools/nexus.d.ts.map +1 -0
- package/dist/server/tools/nexus.js +65 -0
- package/dist/server/tools/nexus.js.map +1 -0
- package/dist/server/tools/proof402.d.ts +3 -0
- package/dist/server/tools/proof402.d.ts.map +1 -0
- package/dist/server/tools/proof402.js +74 -0
- package/dist/server/tools/proof402.js.map +1 -0
- package/dist/server/tools/rails.d.ts +3 -0
- package/dist/server/tools/rails.d.ts.map +1 -0
- package/dist/server/tools/rails.js +82 -0
- package/dist/server/tools/rails.js.map +1 -0
- package/dist/server/tools/shadow.d.ts +3 -0
- package/dist/server/tools/shadow.d.ts.map +1 -0
- package/dist/server/tools/shadow.js +114 -0
- package/dist/server/tools/shadow.js.map +1 -0
- package/dist/server/tools/squeezeos.d.ts +3 -0
- package/dist/server/tools/squeezeos.d.ts.map +1 -0
- package/dist/server/tools/squeezeos.js +231 -0
- package/dist/server/tools/squeezeos.js.map +1 -0
- package/dist/server/tools/xdeo.d.ts +3 -0
- package/dist/server/tools/xdeo.d.ts.map +1 -0
- package/dist/server/tools/xdeo.js +58 -0
- package/dist/server/tools/xdeo.js.map +1 -0
- package/dist/server/tools/xmit.d.ts +3 -0
- package/dist/server/tools/xmit.d.ts.map +1 -0
- package/dist/server/tools/xmit.js +59 -0
- package/dist/server/tools/xmit.js.map +1 -0
- package/docker-compose.yml +50 -0
- package/llms.txt +70 -0
- package/package.json +77 -0
- package/render.yaml +39 -0
- package/sdk/mcp-x402-sdk/package.json +18 -0
- package/sdk/mcp-x402-sdk/src/index.ts +118 -0
- package/sdk/mcp-x402-sdk/tsconfig.json +14 -0
- package/server.json +60 -0
- package/services/backtest_service.py +176 -0
- package/src/lib/chains/base.ts +77 -0
- package/src/lib/chains/solana.ts +59 -0
- package/src/lib/chains/xrpl.ts +63 -0
- package/src/lib/credit/bureau.ts +65 -0
- package/src/lib/sml-api/agentcard.ts +40 -0
- package/src/lib/sml-api/backtest.ts +47 -0
- package/src/lib/sml-api/brokers.ts +160 -0
- package/src/lib/sml-api/copytrader.ts +33 -0
- package/src/lib/sml-api/crawl.ts +44 -0
- package/src/lib/sml-api/echo.ts +28 -0
- package/src/lib/sml-api/forge.ts +33 -0
- package/src/lib/sml-api/ftd.ts +53 -0
- package/src/lib/sml-api/ghost.ts +35 -0
- package/src/lib/sml-api/launchpad.ts +43 -0
- package/src/lib/sml-api/leviathan.ts +49 -0
- package/src/lib/sml-api/nexus.ts +50 -0
- package/src/lib/sml-api/proof402.ts +27 -0
- package/src/lib/sml-api/rails.ts +34 -0
- package/src/lib/sml-api/shadow.ts +35 -0
- package/src/lib/sml-api/squeezeos.ts +95 -0
- package/src/lib/sml-api/xdeo.ts +40 -0
- package/src/lib/sml-api/xmit.ts +40 -0
- package/src/server/health.ts +52 -0
- package/src/server/index.ts +206 -0
- package/src/server/payments/ap2.ts +99 -0
- package/src/server/payments/receipt.ts +85 -0
- package/src/server/payments/router.ts +110 -0
- package/src/server/payments/wallet.ts +123 -0
- package/src/server/payments/x402.ts +162 -0
- package/src/server/registry/catalog.ts +61 -0
- package/src/server/registry/discovery.ts +39 -0
- package/src/server/registry/pricing.ts +76 -0
- package/src/server/security/acl.ts +42 -0
- package/src/server/security/audit.ts +94 -0
- package/src/server/security/rate-limit.ts +84 -0
- package/src/server/security/sandbox.ts +40 -0
- package/src/server/tools/agentcard.ts +134 -0
- package/src/server/tools/backtest.ts +119 -0
- package/src/server/tools/brokers.ts +250 -0
- package/src/server/tools/copytrader.ts +104 -0
- package/src/server/tools/crawl.ts +70 -0
- package/src/server/tools/discovery.ts +202 -0
- package/src/server/tools/echo.ts +58 -0
- package/src/server/tools/forge.ts +87 -0
- package/src/server/tools/ftd.ts +88 -0
- package/src/server/tools/ghost.ts +93 -0
- package/src/server/tools/index.ts +42 -0
- package/src/server/tools/launchpad.ts +173 -0
- package/src/server/tools/leviathan.ts +81 -0
- package/src/server/tools/nexus.ts +76 -0
- package/src/server/tools/proof402.ts +87 -0
- package/src/server/tools/rails.ts +92 -0
- package/src/server/tools/shadow.ts +128 -0
- package/src/server/tools/squeezeos.ts +312 -0
- package/src/server/tools/xdeo.ts +67 -0
- package/src/server/tools/xmit.ts +68 -0
- package/tests/integration/e2e.test.ts +51 -0
- package/tests/unit/payments.test.ts +49 -0
- package/tests/unit/security.test.ts +92 -0
- package/tests/unit/tools.test.ts +42 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
|
|
3
|
+
const startTime = Date.now();
|
|
4
|
+
|
|
5
|
+
export interface HealthStatus {
|
|
6
|
+
status: 'ok' | 'degraded';
|
|
7
|
+
version: string;
|
|
8
|
+
transport: string;
|
|
9
|
+
uptime_seconds: number;
|
|
10
|
+
uptime_human: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
checks: {
|
|
13
|
+
process: 'ok';
|
|
14
|
+
memory_mb: number;
|
|
15
|
+
memory_ok: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatUptime(ms: number): string {
|
|
20
|
+
const s = Math.floor(ms / 1000);
|
|
21
|
+
const m = Math.floor(s / 60);
|
|
22
|
+
const h = Math.floor(m / 60);
|
|
23
|
+
const d = Math.floor(h / 24);
|
|
24
|
+
if (d > 0) return `${d}d ${h % 24}h ${m % 60}m`;
|
|
25
|
+
if (h > 0) return `${h}h ${m % 60}m ${s % 60}s`;
|
|
26
|
+
if (m > 0) return `${m}m ${s % 60}s`;
|
|
27
|
+
return `${s}s`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function healthHandler(_req: Request, res: Response): void {
|
|
31
|
+
const uptimeMs = Date.now() - startTime;
|
|
32
|
+
const memMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
33
|
+
const memOk = memMb < 450; // warn if approaching 512MB container limit
|
|
34
|
+
|
|
35
|
+
const body: HealthStatus = {
|
|
36
|
+
status: memOk ? 'ok' : 'degraded',
|
|
37
|
+
version: process.env['npm_package_version'] ?? '1.0.0',
|
|
38
|
+
transport: process.env['MCP_TRANSPORT'] ?? 'stdio',
|
|
39
|
+
uptime_seconds: Math.floor(uptimeMs / 1000),
|
|
40
|
+
uptime_human: formatUptime(uptimeMs),
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
checks: {
|
|
43
|
+
process: 'ok',
|
|
44
|
+
memory_mb: memMb,
|
|
45
|
+
memory_ok: memOk,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Return 200 even if degraded — let the orchestrator decide.
|
|
50
|
+
// Only return 5xx if the process itself is fundamentally broken.
|
|
51
|
+
res.status(200).json(body);
|
|
52
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import cors from 'cors';
|
|
9
|
+
import { registerTools } from './tools/index.js';
|
|
10
|
+
import { AuditLogger } from './security/audit.js';
|
|
11
|
+
import { RateLimiter } from './security/rate-limit.js';
|
|
12
|
+
import { healthHandler } from './health.js';
|
|
13
|
+
|
|
14
|
+
const VERSION = '1.0.0';
|
|
15
|
+
|
|
16
|
+
async function createServer(): Promise<McpServer> {
|
|
17
|
+
const server = new McpServer(
|
|
18
|
+
{ name: 'mcp-x402', version: VERSION },
|
|
19
|
+
{ capabilities: { tools: {} } },
|
|
20
|
+
);
|
|
21
|
+
await registerTools(server);
|
|
22
|
+
return server;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function runStdio(): Promise<void> {
|
|
26
|
+
const server = await createServer();
|
|
27
|
+
const transport = new StdioServerTransport();
|
|
28
|
+
await server.connect(transport);
|
|
29
|
+
AuditLogger.getInstance().info('server_start', { transport: 'stdio', version: VERSION });
|
|
30
|
+
|
|
31
|
+
const shutdown = async () => {
|
|
32
|
+
AuditLogger.getInstance().info('server_stop', { transport: 'stdio' });
|
|
33
|
+
await server.close();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
};
|
|
36
|
+
process.on('SIGINT', shutdown);
|
|
37
|
+
process.on('SIGTERM', shutdown);
|
|
38
|
+
|
|
39
|
+
// Keep stdio process alive
|
|
40
|
+
process.stdin.on('end', () => {
|
|
41
|
+
AuditLogger.getInstance().warn('stdio_stdin_end', {});
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runSSE(): Promise<void> {
|
|
47
|
+
const app = express();
|
|
48
|
+
const port = parseInt(process.env['MCP_SSE_PORT'] ?? '3402', 10);
|
|
49
|
+
|
|
50
|
+
app.use(cors({ origin: process.env['CORS_ORIGIN'] ?? '*' }));
|
|
51
|
+
app.use(express.json({ limit: '1mb' }));
|
|
52
|
+
|
|
53
|
+
// Health endpoint
|
|
54
|
+
app.get('/health', healthHandler);
|
|
55
|
+
|
|
56
|
+
app.get('/agents.json', (_req, res) => {
|
|
57
|
+
res.sendFile('agents.json', { root: process.cwd() });
|
|
58
|
+
});
|
|
59
|
+
app.get('/llms.txt', (_req, res) => {
|
|
60
|
+
res.sendFile('llms.txt', { root: process.cwd() });
|
|
61
|
+
});
|
|
62
|
+
app.get('/.well-known/agentcard.json', (_req, res) => {
|
|
63
|
+
res.sendFile('.well-known/agentcard.json', { root: process.cwd() });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// FIX: Root handler — was 404, now returns service discovery
|
|
67
|
+
app.get('/', (_req, res) => {
|
|
68
|
+
res.json({
|
|
69
|
+
name: 'mcp-x402',
|
|
70
|
+
version: VERSION,
|
|
71
|
+
description: 'The x402 Amazon — 43+ tools, pay-per-call via XRPL. scriptmasterlabs.com',
|
|
72
|
+
status: 'online',
|
|
73
|
+
transport: 'streamable-http + sse',
|
|
74
|
+
endpoints: {
|
|
75
|
+
mcp_streamable: 'POST /mcp',
|
|
76
|
+
sse_connect: 'GET /sse',
|
|
77
|
+
sse_messages: 'POST /messages',
|
|
78
|
+
health: 'GET /health',
|
|
79
|
+
agentCard: 'GET /.well-known/agentcard.json',
|
|
80
|
+
llms: 'GET /llms.txt',
|
|
81
|
+
},
|
|
82
|
+
links: {
|
|
83
|
+
github: 'https://github.com/Timwal78/SML_Portfolio/tree/main/mcp-x402',
|
|
84
|
+
homepage: 'https://scriptmasterlabs.com',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Streamable HTTP transport — claude.ai web connectors
|
|
90
|
+
const streamableTransports = new Map<string, StreamableHTTPServerTransport>();
|
|
91
|
+
|
|
92
|
+
app.post('/mcp', async (req, res) => {
|
|
93
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
94
|
+
let transport = sessionId ? streamableTransports.get(sessionId) : undefined;
|
|
95
|
+
|
|
96
|
+
if (!transport) {
|
|
97
|
+
const newSessionId = randomUUID();
|
|
98
|
+
transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId });
|
|
99
|
+
streamableTransports.set(newSessionId, transport);
|
|
100
|
+
transport.onclose = () => streamableTransports.delete(newSessionId);
|
|
101
|
+
const server = await createServer();
|
|
102
|
+
await server.connect(transport);
|
|
103
|
+
AuditLogger.getInstance().info('mcp_connect', { sessionId: newSessionId });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await transport.handleRequest(req, res, req.body);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// FIX: GET /mcp with no session was 404, now returns service info
|
|
110
|
+
app.get('/mcp', async (req, res) => {
|
|
111
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
112
|
+
const transport = sessionId ? streamableTransports.get(sessionId) : undefined;
|
|
113
|
+
if (!transport) {
|
|
114
|
+
res.json({
|
|
115
|
+
name: 'mcp-x402',
|
|
116
|
+
version: VERSION,
|
|
117
|
+
protocol: 'MCP/streamable-http',
|
|
118
|
+
status: 'ready',
|
|
119
|
+
tools: '43+ tools available',
|
|
120
|
+
how_to_connect: 'POST /mcp with a JSON-RPC initialize request',
|
|
121
|
+
sse_alternative: 'GET /sse for legacy SSE transport',
|
|
122
|
+
health: '/health',
|
|
123
|
+
homepage: 'https://scriptmasterlabs.com',
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await transport.handleRequest(req, res);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
app.delete('/mcp', async (req, res) => {
|
|
131
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
132
|
+
const transport = sessionId ? streamableTransports.get(sessionId) : undefined;
|
|
133
|
+
if (!transport) { res.status(404).json({ error: 'session_not_found' }); return; }
|
|
134
|
+
await transport.handleRequest(req, res);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const transports = new Map<string, SSEServerTransport>();
|
|
138
|
+
const rateLimiter = RateLimiter.getInstance();
|
|
139
|
+
|
|
140
|
+
app.get('/sse', async (req, res) => {
|
|
141
|
+
const clientIp = req.ip ?? 'unknown';
|
|
142
|
+
if (!rateLimiter.checkIp(clientIp)) {
|
|
143
|
+
res.status(429).json({ error: 'rate_limit_exceeded', retry_after: 60 });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const transport = new SSEServerTransport('/messages', res);
|
|
147
|
+
const sessionId = transport.sessionId;
|
|
148
|
+
transports.set(sessionId, transport);
|
|
149
|
+
const server = await createServer();
|
|
150
|
+
await server.connect(transport);
|
|
151
|
+
AuditLogger.getInstance().info('sse_connect', { sessionId, clientIp });
|
|
152
|
+
res.on('close', async () => {
|
|
153
|
+
transports.delete(sessionId);
|
|
154
|
+
AuditLogger.getInstance().info('sse_disconnect', { sessionId });
|
|
155
|
+
await server.close();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.post('/messages', async (req, res) => {
|
|
160
|
+
const sessionId = req.query['sessionId'] as string | undefined;
|
|
161
|
+
if (!sessionId) { res.status(400).json({ error: 'missing_session_id' }); return; }
|
|
162
|
+
const transport = transports.get(sessionId);
|
|
163
|
+
if (!transport) { res.status(404).json({ error: 'session_not_found' }); return; }
|
|
164
|
+
await transport.handlePostMessage(req, res);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const httpServer = await new Promise<ReturnType<typeof app.listen>>(
|
|
168
|
+
(resolve) => {
|
|
169
|
+
const s = app.listen(port, () => resolve(s));
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
AuditLogger.getInstance().info('server_start', { transport: 'sse', port, version: VERSION });
|
|
174
|
+
console.error(`[mcp-x402] listening on :${port} — health: http://localhost:${port}/health`);
|
|
175
|
+
|
|
176
|
+
const shutdown = async () => {
|
|
177
|
+
AuditLogger.getInstance().info('server_stop', { transport: 'sse' });
|
|
178
|
+
for (const [id] of transports) {
|
|
179
|
+
AuditLogger.getInstance().info('sse_force_close', { sessionId: id });
|
|
180
|
+
}
|
|
181
|
+
httpServer.close(() => process.exit(0));
|
|
182
|
+
setTimeout(() => process.exit(1), 10_000).unref();
|
|
183
|
+
};
|
|
184
|
+
process.on('SIGINT', shutdown);
|
|
185
|
+
process.on('SIGTERM', shutdown);
|
|
186
|
+
|
|
187
|
+
process.on('uncaughtException', (err) => {
|
|
188
|
+
AuditLogger.getInstance().error('uncaught_exception', { error: String(err), stack: err.stack ?? '' });
|
|
189
|
+
});
|
|
190
|
+
process.on('unhandledRejection', (reason) => {
|
|
191
|
+
AuditLogger.getInstance().error('unhandledRejection', { reason: String(reason) });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const transport = process.env['MCP_TRANSPORT'] ?? 'stdio';
|
|
196
|
+
if (transport === 'sse') {
|
|
197
|
+
runSSE().catch((err) => {
|
|
198
|
+
console.error('[mcp-x402] fatal:', err);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
runStdio().catch((err) => {
|
|
203
|
+
console.error('[mcp-x402] fatal:', err);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AuditLogger } from '../security/audit.js';
|
|
2
|
+
|
|
3
|
+
export interface MandateParams {
|
|
4
|
+
maxAmount: string;
|
|
5
|
+
currency: string;
|
|
6
|
+
toolName: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MandateCache {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// In-memory mandate cache — 5 minute TTL per wallet+tool combination
|
|
15
|
+
const mandateCache = new Map<string, MandateCache>();
|
|
16
|
+
|
|
17
|
+
export class AP2Client {
|
|
18
|
+
private static instance: AP2Client;
|
|
19
|
+
private readonly baseUrl: string;
|
|
20
|
+
|
|
21
|
+
private constructor() {
|
|
22
|
+
this.baseUrl = process.env['SML_API_BASE'] ?? 'https://api.scriptmasterlabs.com';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static getInstance(): AP2Client {
|
|
26
|
+
if (!AP2Client.instance) {
|
|
27
|
+
AP2Client.instance = new AP2Client();
|
|
28
|
+
}
|
|
29
|
+
return AP2Client.instance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async verifyMandate(wallet: string, params: MandateParams): Promise<boolean> {
|
|
33
|
+
const cacheKey = `${wallet}:${params.toolName}:${params.currency}`;
|
|
34
|
+
const cached = mandateCache.get(cacheKey);
|
|
35
|
+
|
|
36
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
37
|
+
return cached.valid;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const audit = AuditLogger.getInstance();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
45
|
+
|
|
46
|
+
const res = await fetch(`${this.baseUrl}/ap2/v1/mandate/verify`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
wallet,
|
|
51
|
+
max_amount: params.maxAmount,
|
|
52
|
+
currency: params.currency,
|
|
53
|
+
tool: params.toolName,
|
|
54
|
+
}),
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
audit.warn('ap2_mandate_http_error', { status: res.status, wallet });
|
|
62
|
+
mandateCache.set(cacheKey, { valid: false, expiresAt: Date.now() + 60_000 });
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const body = (await res.json()) as { valid: boolean; expires_in?: number };
|
|
67
|
+
const ttl = (body.expires_in ?? 300) * 1000;
|
|
68
|
+
|
|
69
|
+
mandateCache.set(cacheKey, { valid: body.valid, expiresAt: Date.now() + ttl });
|
|
70
|
+
return body.valid;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
audit.error('ap2_mandate_error', { error: String(err), wallet });
|
|
73
|
+
// Fail closed — if AP2 is unreachable, deny payment
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async createMandate(
|
|
79
|
+
wallet: string,
|
|
80
|
+
params: { dailyCap: string; currency: string },
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
const res = await fetch(`${this.baseUrl}/ap2/v1/mandate/create`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
wallet,
|
|
87
|
+
daily_cap: params.dailyCap,
|
|
88
|
+
currency: params.currency,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(`AP2 mandate creation failed: HTTP ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const body = (await res.json()) as { mandate_id: string };
|
|
97
|
+
return body.mandate_id;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { AuditLogger } from '../security/audit.js';
|
|
3
|
+
|
|
4
|
+
export interface ReceiptInput {
|
|
5
|
+
txHash: string;
|
|
6
|
+
chain: string;
|
|
7
|
+
amount: string;
|
|
8
|
+
currency: string;
|
|
9
|
+
tool: string;
|
|
10
|
+
wallet: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Receipt {
|
|
14
|
+
id: string;
|
|
15
|
+
txHash: string;
|
|
16
|
+
chain: string;
|
|
17
|
+
amount: string;
|
|
18
|
+
currency: string;
|
|
19
|
+
tool: string;
|
|
20
|
+
wallet: string;
|
|
21
|
+
issuedAt: number;
|
|
22
|
+
hash: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ReceiptStore {
|
|
26
|
+
private static instance: ReceiptStore;
|
|
27
|
+
private readonly baseUrl: string;
|
|
28
|
+
|
|
29
|
+
private constructor() {
|
|
30
|
+
this.baseUrl = process.env['SML_API_BASE'] ?? 'https://api.scriptmasterlabs.com';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static getInstance(): ReceiptStore {
|
|
34
|
+
if (!ReceiptStore.instance) {
|
|
35
|
+
ReceiptStore.instance = new ReceiptStore();
|
|
36
|
+
}
|
|
37
|
+
return ReceiptStore.instance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async create(input: ReceiptInput): Promise<Receipt> {
|
|
41
|
+
const id = randomUUID();
|
|
42
|
+
const issuedAt = Date.now();
|
|
43
|
+
|
|
44
|
+
// Content hash for tamper detection
|
|
45
|
+
const hash = createHash('sha256')
|
|
46
|
+
.update(`${id}:${input.txHash}:${input.chain}:${input.amount}:${input.currency}:${input.tool}:${input.wallet}:${issuedAt}`)
|
|
47
|
+
.digest('hex');
|
|
48
|
+
|
|
49
|
+
const receipt: Receipt = { id, ...input, issuedAt, hash };
|
|
50
|
+
|
|
51
|
+
// Attempt to register with 402Proof server (N7)
|
|
52
|
+
try {
|
|
53
|
+
await this.registerWithProofServer(receipt);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Log but don't fail — local receipt is still valid
|
|
56
|
+
AuditLogger.getInstance().warn('proof_server_register_fail', { error: String(err), receiptId: id });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
AuditLogger.getInstance().info('receipt_issued', { receiptId: id, tool: input.tool });
|
|
60
|
+
return receipt;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async registerWithProofServer(receipt: Receipt): Promise<void> {
|
|
64
|
+
const proofUrl = process.env['PROOF402_URL'] ?? 'https://four02proof.onrender.com';
|
|
65
|
+
const res = await fetch(`${proofUrl}/v1/receipt`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
receipt_id: receipt.id,
|
|
70
|
+
tx_hash: receipt.txHash,
|
|
71
|
+
chain: receipt.chain,
|
|
72
|
+
amount: receipt.amount,
|
|
73
|
+
currency: receipt.currency,
|
|
74
|
+
tool: receipt.tool,
|
|
75
|
+
wallet: receipt.wallet,
|
|
76
|
+
issued_at: receipt.issuedAt,
|
|
77
|
+
hash: receipt.hash,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(`402Proof server returned HTTP ${res.status}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AuditLogger } from '../security/audit.js';
|
|
2
|
+
import { BaseChain } from '../../lib/chains/base.js';
|
|
3
|
+
import { XRPLChain } from '../../lib/chains/xrpl.js';
|
|
4
|
+
import { SolanaChain } from '../../lib/chains/solana.js';
|
|
5
|
+
|
|
6
|
+
export interface RouteParams {
|
|
7
|
+
amount: string;
|
|
8
|
+
currency: 'USDC' | 'RLUSD';
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RouteResult {
|
|
15
|
+
txHash: string;
|
|
16
|
+
chain: string;
|
|
17
|
+
latencyMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Chain preference order: cheapest/fastest first (N13)
|
|
21
|
+
const CHAIN_PREFERENCE = ['base', 'xrpl', 'solana'] as const;
|
|
22
|
+
|
|
23
|
+
export class ChainRouter {
|
|
24
|
+
private static instance: ChainRouter;
|
|
25
|
+
|
|
26
|
+
private constructor(
|
|
27
|
+
private readonly base = BaseChain.getInstance(),
|
|
28
|
+
private readonly xrpl = XRPLChain.getInstance(),
|
|
29
|
+
private readonly solana = SolanaChain.getInstance(),
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
static getInstance(): ChainRouter {
|
|
33
|
+
if (!ChainRouter.instance) {
|
|
34
|
+
ChainRouter.instance = new ChainRouter();
|
|
35
|
+
}
|
|
36
|
+
return ChainRouter.instance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async route(params: RouteParams): Promise<RouteResult> {
|
|
40
|
+
const audit = AuditLogger.getInstance();
|
|
41
|
+
const timeout = params.timeoutMs ?? 3000;
|
|
42
|
+
|
|
43
|
+
// For RLUSD, prefer XRPL
|
|
44
|
+
const ordered =
|
|
45
|
+
params.currency === 'RLUSD'
|
|
46
|
+
? (['xrpl', 'base', 'solana'] as const)
|
|
47
|
+
: CHAIN_PREFERENCE;
|
|
48
|
+
|
|
49
|
+
const errors: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (const chain of ordered) {
|
|
52
|
+
try {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
let txHash: string;
|
|
55
|
+
|
|
56
|
+
switch (chain) {
|
|
57
|
+
case 'base':
|
|
58
|
+
txHash = await this.withTimeout(
|
|
59
|
+
this.base.sendPayment(params),
|
|
60
|
+
timeout,
|
|
61
|
+
'base',
|
|
62
|
+
);
|
|
63
|
+
break;
|
|
64
|
+
case 'xrpl':
|
|
65
|
+
txHash = await this.withTimeout(
|
|
66
|
+
this.xrpl.sendPayment(params),
|
|
67
|
+
timeout,
|
|
68
|
+
'xrpl',
|
|
69
|
+
);
|
|
70
|
+
break;
|
|
71
|
+
case 'solana':
|
|
72
|
+
txHash = await this.withTimeout(
|
|
73
|
+
this.solana.sendPayment(params),
|
|
74
|
+
timeout,
|
|
75
|
+
'solana',
|
|
76
|
+
);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const latencyMs = Date.now() - start;
|
|
81
|
+
audit.info('chain_route_success', { chain, latencyMs, tx: txHash });
|
|
82
|
+
|
|
83
|
+
return { txHash, chain, latencyMs };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = String(err);
|
|
86
|
+
errors.push(`${chain}: ${msg}`);
|
|
87
|
+
audit.warn('chain_route_fail', { chain, error: msg });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`All chains failed.\n${errors.join('\n')}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private withTimeout<T>(
|
|
95
|
+
promise: Promise<T>,
|
|
96
|
+
ms: number,
|
|
97
|
+
chain: string,
|
|
98
|
+
): Promise<T> {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const t = setTimeout(
|
|
101
|
+
() => reject(new Error(`${chain} timeout after ${ms}ms`)),
|
|
102
|
+
ms,
|
|
103
|
+
);
|
|
104
|
+
promise.then(
|
|
105
|
+
(v) => { clearTimeout(t); resolve(v); },
|
|
106
|
+
(e) => { clearTimeout(t); reject(e); },
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { AuditLogger } from '../security/audit.js';
|
|
2
|
+
|
|
3
|
+
const KEYCHAIN_SERVICE = 'mcp-x402';
|
|
4
|
+
const KEYCHAIN_ACCOUNT = 'master-seed';
|
|
5
|
+
|
|
6
|
+
export interface WalletInfo {
|
|
7
|
+
address: string;
|
|
8
|
+
chain: 'base' | 'xrpl' | 'solana';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class WalletManager {
|
|
12
|
+
private static instance: WalletManager;
|
|
13
|
+
private cachedAddress: string | null = null;
|
|
14
|
+
private cachedSeed: string | null = null;
|
|
15
|
+
|
|
16
|
+
private constructor() {}
|
|
17
|
+
|
|
18
|
+
static getInstance(): WalletManager {
|
|
19
|
+
if (!WalletManager.instance) {
|
|
20
|
+
WalletManager.instance = new WalletManager();
|
|
21
|
+
}
|
|
22
|
+
return WalletManager.instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try OS keychain; returns null if keytar is unavailable (Docker/cloud) or no entry exists.
|
|
26
|
+
private async keytarGet(): Promise<string | null> {
|
|
27
|
+
try {
|
|
28
|
+
const kt = await import('keytar');
|
|
29
|
+
return await kt.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async keytarSet(value: string): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const kt = await import('keytar');
|
|
38
|
+
await kt.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, value);
|
|
39
|
+
} catch {
|
|
40
|
+
// Silently skip — keytar unavailable in this environment
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Seed resolution priority:
|
|
45
|
+
// 1. OS keychain (local desktop — most secure)
|
|
46
|
+
// 2. WALLET_SEED env var (Render secret — cloud/Docker deployment)
|
|
47
|
+
// 3. CI_WALLET_SEED env var (CI only, never production)
|
|
48
|
+
// 4. Generate fresh and try to persist in keychain
|
|
49
|
+
async getSeed(): Promise<string> {
|
|
50
|
+
if (this.cachedSeed) return this.cachedSeed;
|
|
51
|
+
|
|
52
|
+
const audit = AuditLogger.getInstance();
|
|
53
|
+
|
|
54
|
+
let seed = await this.keytarGet();
|
|
55
|
+
|
|
56
|
+
if (!seed) {
|
|
57
|
+
const envSeed = process.env['WALLET_SEED'];
|
|
58
|
+
if (envSeed) {
|
|
59
|
+
seed = envSeed;
|
|
60
|
+
audit.warn('wallet_env_seed', { note: 'Using WALLET_SEED env var (cloud deployment).' });
|
|
61
|
+
} else if (process.env['CI_WALLET_SEED'] && process.env['NODE_ENV'] !== 'production') {
|
|
62
|
+
seed = process.env['CI_WALLET_SEED'];
|
|
63
|
+
audit.warn('wallet_ci_fallback', { note: 'Using CI_WALLET_SEED. NEVER do this in production.' });
|
|
64
|
+
} else {
|
|
65
|
+
const { generateMnemonic } = await import('bip39');
|
|
66
|
+
seed = generateMnemonic(256);
|
|
67
|
+
await this.keytarSet(seed);
|
|
68
|
+
audit.info('wallet_created', { note: 'New BIP-39 seed generated.' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.cachedSeed = seed;
|
|
73
|
+
return seed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getOrCreateWallet(): Promise<WalletInfo> {
|
|
77
|
+
if (this.cachedAddress) {
|
|
78
|
+
return { address: this.cachedAddress, chain: 'base' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const audit = AuditLogger.getInstance();
|
|
82
|
+
const seed = await this.getSeed();
|
|
83
|
+
const address = await this.deriveAddress(seed);
|
|
84
|
+
this.cachedAddress = address;
|
|
85
|
+
audit.info('wallet_loaded', { address });
|
|
86
|
+
return { address, chain: 'base' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async deriveAddress(mnemonic: string): Promise<string> {
|
|
90
|
+
// BIP-44 deterministic derivation: m/44'/60'/0'/0/0 (Base = EVM)
|
|
91
|
+
const { mnemonicToSeedSync } = await import('bip39');
|
|
92
|
+
const { default: HDKey } = await import('hdkey');
|
|
93
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
94
|
+
|
|
95
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
96
|
+
const hdkey = HDKey.fromMasterSeed(seed);
|
|
97
|
+
const child = hdkey.derive("m/44'/60'/0'/0/0");
|
|
98
|
+
|
|
99
|
+
if (!child.privateKey) {
|
|
100
|
+
throw new Error('Failed to derive private key from HD path');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const account = privateKeyToAccount(`0x${child.privateKey.toString('hex')}`);
|
|
104
|
+
return account.address;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async signPayload(payload: string): Promise<string> {
|
|
108
|
+
const mnemonic = await this.getSeed();
|
|
109
|
+
|
|
110
|
+
const { mnemonicToSeedSync } = await import('bip39');
|
|
111
|
+
const { default: HDKey } = await import('hdkey');
|
|
112
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
113
|
+
|
|
114
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
115
|
+
const hdkey = HDKey.fromMasterSeed(seed);
|
|
116
|
+
const child = hdkey.derive("m/44'/60'/0'/0/0");
|
|
117
|
+
|
|
118
|
+
if (!child.privateKey) throw new Error('Key derivation failed');
|
|
119
|
+
|
|
120
|
+
const account = privateKeyToAccount(`0x${child.privateKey.toString('hex')}`);
|
|
121
|
+
return account.signMessage({ message: payload });
|
|
122
|
+
}
|
|
123
|
+
}
|