auramaxx 1.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Definitions
|
|
3
|
+
* ====================
|
|
4
|
+
* Provider-agnostic tool definitions + HTTP handler for executing wallet API calls.
|
|
5
|
+
* Single source of truth — both the MCP server and SDK tool-use loop read from here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
9
|
+
import { publicEncrypt, constants, generateKeyPairSync } from 'crypto';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { getErrorMessage } from '../lib/error';
|
|
12
|
+
|
|
13
|
+
/** Provider-agnostic tool definition */
|
|
14
|
+
export interface ToolDef {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object';
|
|
19
|
+
properties: Record<string, unknown>;
|
|
20
|
+
required?: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** All available tools */
|
|
25
|
+
export const TOOLS: ToolDef[] = [
|
|
26
|
+
{
|
|
27
|
+
name: 'create_vault',
|
|
28
|
+
description:
|
|
29
|
+
'Create a new wallet vault with a password. Only one primary vault can exist — returns error if one already exists. Returns the vault address, seed phrase (mnemonic), and admin token. Store the password securely (e.g. in env as AURA_VAULT_PASSWORD) for future unlock via POST /unlock. The MCP server auto-activates the returned token for the current session.',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
password: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Password to encrypt the vault (minimum 8 characters)',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['password'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'wallet_api',
|
|
43
|
+
description:
|
|
44
|
+
'Call the AuraWallet API. Common endpoints: GET /wallets, GET /token/search?q=PEPE&chain=base (find contract by ticker/name), POST /wallet/create, POST /send, POST /swap, POST /fund, GET /token/:tokenAddress/balance/:walletAddress (check any address\'s token balance). If you have no token yet, use socket bootstrap (preferred) or set AURA_TOKEN for CI/ops. Read the docs://api resource for the full endpoint reference.',
|
|
45
|
+
parameters: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
method: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
enum: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
51
|
+
description: 'HTTP method',
|
|
52
|
+
},
|
|
53
|
+
endpoint: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'API path, e.g. /wallets',
|
|
56
|
+
},
|
|
57
|
+
body: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
description: 'POST request body (optional)',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ['method', 'endpoint'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'request_human_action',
|
|
67
|
+
description:
|
|
68
|
+
'Request human approval for a privileged wallet action. Use this when wallet_api returns 403 (insufficient permissions). The human sees the summary and approves or rejects. On approval the action is auto-executed with a scoped temporary token.',
|
|
69
|
+
parameters: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
summary: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Human-readable description of the action (shown in approval card)',
|
|
75
|
+
},
|
|
76
|
+
permissions: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
description: 'Permission strings needed, e.g. ["swap"], ["send:hot"], ["fund"]',
|
|
79
|
+
},
|
|
80
|
+
action: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
description: 'Pre-computed API call: { endpoint: "/swap", method: "POST", body: {...} }',
|
|
83
|
+
},
|
|
84
|
+
limits: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
description: 'Spending caps per permission in native currency, e.g. { swap: 0.01 }',
|
|
87
|
+
},
|
|
88
|
+
walletAccess: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
description: 'Wallet addresses the temporary token needs access to, e.g. ["0x123...", "0x456..."]',
|
|
91
|
+
},
|
|
92
|
+
ttl: {
|
|
93
|
+
type: 'number',
|
|
94
|
+
description: 'Seconds the temporary token lives (default 60)',
|
|
95
|
+
},
|
|
96
|
+
pubkey: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Agent RSA public key (PEM or base64). If omitted, MCP generates an ephemeral key for this request.',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: ['summary', 'permissions', 'action'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert JSON Schema properties to Zod schema shape.
|
|
108
|
+
* Used by the MCP server to bridge provider-agnostic tool defs with the MCP SDK's Zod requirement.
|
|
109
|
+
*/
|
|
110
|
+
export function jsonSchemaToZod(
|
|
111
|
+
props: Record<string, unknown>,
|
|
112
|
+
requiredFields: string[],
|
|
113
|
+
): Record<string, z.ZodTypeAny> {
|
|
114
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
115
|
+
const required = new Set(requiredFields);
|
|
116
|
+
|
|
117
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
118
|
+
const s = schema as Record<string, unknown>;
|
|
119
|
+
let zodType: z.ZodTypeAny;
|
|
120
|
+
|
|
121
|
+
if (s.type === 'string') {
|
|
122
|
+
zodType = s.enum
|
|
123
|
+
? z.enum(s.enum as [string, ...string[]])
|
|
124
|
+
: z.string();
|
|
125
|
+
} else if (s.type === 'object') {
|
|
126
|
+
zodType = z.record(z.unknown());
|
|
127
|
+
} else if (s.type === 'array') {
|
|
128
|
+
zodType = z.array(z.unknown());
|
|
129
|
+
} else if (s.type === 'number') {
|
|
130
|
+
zodType = z.number();
|
|
131
|
+
} else {
|
|
132
|
+
zodType = z.unknown();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (s.description) {
|
|
136
|
+
zodType = zodType.describe(s.description as string);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
shape[key] = required.has(key) ? zodType : zodType.optional();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return shape;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Base URL for the wallet server (configurable for testing) */
|
|
146
|
+
const WALLET_BASE_URL = process.env.WALLET_SERVER_URL || 'http://127.0.0.1:4242';
|
|
147
|
+
|
|
148
|
+
/** Max response size to prevent context bloat */
|
|
149
|
+
const MAX_RESPONSE_SIZE = 4096;
|
|
150
|
+
|
|
151
|
+
/** Timeout per tool call */
|
|
152
|
+
const TOOL_TIMEOUT_MS = 10_000;
|
|
153
|
+
|
|
154
|
+
function generateAgentPubkeyPem(): string {
|
|
155
|
+
const pair = generateKeyPairSync('rsa', {
|
|
156
|
+
modulusLength: 2048,
|
|
157
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
158
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
159
|
+
});
|
|
160
|
+
return pair.publicKey;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Permission → allowed endpoint prefixes.
|
|
165
|
+
* Prevents a app from disguising e.g. a /send as a /swap request.
|
|
166
|
+
*/
|
|
167
|
+
const PERMISSION_ENDPOINT_MAP: Record<string, string[]> = {
|
|
168
|
+
'swap': ['/swap'],
|
|
169
|
+
'send:hot': ['/send'],
|
|
170
|
+
'send:temp': ['/send'],
|
|
171
|
+
'fund': ['/fund'],
|
|
172
|
+
'launch': ['/launch'],
|
|
173
|
+
'wallet:create:hot': ['/wallet/create'],
|
|
174
|
+
'wallet:create:temp': ['/wallet/create'],
|
|
175
|
+
'wallet:list': ['/wallets', '/wallet/', '/wallets/transactions'],
|
|
176
|
+
'wallet:rename': ['/wallet/rename'],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reverse mapping: endpoint prefix → required permissions.
|
|
181
|
+
* Used to catch wrong/unknown permission strings for known endpoints.
|
|
182
|
+
*/
|
|
183
|
+
const ENDPOINT_REQUIRED_PERMISSIONS: Record<string, string[]> = {
|
|
184
|
+
'/swap': ['swap'],
|
|
185
|
+
'/send': ['send:hot', 'send:temp'],
|
|
186
|
+
'/fund': ['fund'],
|
|
187
|
+
'/launch': ['launch'],
|
|
188
|
+
'/wallet/create': ['wallet:create:hot', 'wallet:create:temp'],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Validate that requested permissions match the action endpoint.
|
|
193
|
+
* Returns an error message if invalid, null if ok.
|
|
194
|
+
*
|
|
195
|
+
* Two-pass validation:
|
|
196
|
+
* 1. Forward check: mapped permissions must match the endpoint prefix
|
|
197
|
+
* 2. Reverse check: known endpoints must have at least one correct permission
|
|
198
|
+
*/
|
|
199
|
+
export function validatePermissionEndpoint(
|
|
200
|
+
permissions: string[],
|
|
201
|
+
endpoint: string,
|
|
202
|
+
): string | null {
|
|
203
|
+
for (const perm of permissions) {
|
|
204
|
+
const allowed = PERMISSION_ENDPOINT_MAP[perm];
|
|
205
|
+
if (!allowed) continue; // Unknown permission — check reverse below
|
|
206
|
+
const match = allowed.some((prefix) => endpoint.startsWith(prefix));
|
|
207
|
+
if (match) return null; // At least one permission matches the endpoint
|
|
208
|
+
}
|
|
209
|
+
// If we have mapped permissions but none matched, reject
|
|
210
|
+
const mappedPerms = permissions.filter((p) => PERMISSION_ENDPOINT_MAP[p]);
|
|
211
|
+
if (mappedPerms.length > 0) {
|
|
212
|
+
// Find expected permissions for this endpoint
|
|
213
|
+
for (const [prefix, required] of Object.entries(ENDPOINT_REQUIRED_PERMISSIONS)) {
|
|
214
|
+
if (endpoint.startsWith(prefix)) {
|
|
215
|
+
return `Permission(s) [${mappedPerms.join(', ')}] do not match endpoint ${endpoint}. Expected one of: [${required.join(', ')}]`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return `Permission(s) [${mappedPerms.join(', ')}] do not match endpoint ${endpoint}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Reverse check: if the endpoint has known requirements, verify at least one is requested
|
|
222
|
+
for (const [prefix, required] of Object.entries(ENDPOINT_REQUIRED_PERMISSIONS)) {
|
|
223
|
+
if (endpoint.startsWith(prefix)) {
|
|
224
|
+
const hasRequired = required.some((r) => permissions.includes(r));
|
|
225
|
+
if (!hasRequired) {
|
|
226
|
+
return `Endpoint ${endpoint} requires one of [${required.join(', ')}] but got [${permissions.join(', ')}]`;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null; // No mapped permissions and unknown endpoint — let server enforce
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Format for Anthropic SDK */
|
|
236
|
+
export function toAnthropicTools(): Anthropic.Tool[] {
|
|
237
|
+
return TOOLS.map((t) => ({
|
|
238
|
+
name: t.name,
|
|
239
|
+
description: t.description,
|
|
240
|
+
input_schema: {
|
|
241
|
+
type: t.parameters.type as 'object',
|
|
242
|
+
properties: t.parameters.properties,
|
|
243
|
+
required: t.parameters.required,
|
|
244
|
+
},
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Format for OpenAI SDK */
|
|
249
|
+
export function toOpenAITools(): Array<{
|
|
250
|
+
type: 'function';
|
|
251
|
+
function: { name: string; description: string; parameters: ToolDef['parameters'] };
|
|
252
|
+
}> {
|
|
253
|
+
return TOOLS.map((t) => ({
|
|
254
|
+
type: 'function' as const,
|
|
255
|
+
function: {
|
|
256
|
+
name: t.name,
|
|
257
|
+
description: t.description,
|
|
258
|
+
parameters: t.parameters,
|
|
259
|
+
},
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Execute a tool call — makes HTTP request to wallet server.
|
|
265
|
+
* Validates endpoint, enforces timeout, truncates response.
|
|
266
|
+
*/
|
|
267
|
+
export async function executeTool(
|
|
268
|
+
toolName: string,
|
|
269
|
+
input: Record<string, unknown>,
|
|
270
|
+
token?: string,
|
|
271
|
+
): Promise<string> {
|
|
272
|
+
if (toolName === 'create_vault') {
|
|
273
|
+
return executeCreateVault(input);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (toolName === 'request_human_action') {
|
|
277
|
+
return executeRequestHumanAction(input, token);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (toolName !== 'wallet_api') {
|
|
281
|
+
return JSON.stringify({ error: `Unknown tool: ${toolName}` });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return executeWalletApi(input, token);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Handle create_vault tool — creates a new vault with RSA-encrypted password.
|
|
289
|
+
* Fetches the server's public key, encrypts the password, and calls POST /setup.
|
|
290
|
+
*/
|
|
291
|
+
async function executeCreateVault(
|
|
292
|
+
input: Record<string, unknown>,
|
|
293
|
+
): Promise<string> {
|
|
294
|
+
const { password } = input as { password?: string };
|
|
295
|
+
|
|
296
|
+
if (!password || typeof password !== 'string') {
|
|
297
|
+
return JSON.stringify({ error: 'password is required' });
|
|
298
|
+
}
|
|
299
|
+
if (password.length < 8) {
|
|
300
|
+
return JSON.stringify({ error: 'Password must be at least 8 characters' });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Step 1: Fetch server's RSA public key
|
|
304
|
+
let publicKeyPem: string;
|
|
305
|
+
try {
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
|
|
308
|
+
const res = await fetch(`${WALLET_BASE_URL}/auth/connect`, { signal: controller.signal });
|
|
309
|
+
clearTimeout(timeout);
|
|
310
|
+
const data = await res.json() as { publicKey: string };
|
|
311
|
+
publicKeyPem = data.publicKey;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const msg = getErrorMessage(err);
|
|
314
|
+
if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED')) {
|
|
315
|
+
return JSON.stringify({ error: `Wallet server not reachable at ${WALLET_BASE_URL}. Start it first: npx aurawallet start` });
|
|
316
|
+
}
|
|
317
|
+
return JSON.stringify({ error: `Could not fetch server public key: ${msg}` });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Step 2: Encrypt password with RSA-OAEP
|
|
321
|
+
let encrypted: string;
|
|
322
|
+
const pubkey = generateAgentPubkeyPem();
|
|
323
|
+
try {
|
|
324
|
+
const buffer = Buffer.from(password, 'utf8');
|
|
325
|
+
const enc = publicEncrypt(
|
|
326
|
+
{ key: publicKeyPem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
327
|
+
buffer,
|
|
328
|
+
);
|
|
329
|
+
encrypted = enc.toString('base64');
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const msg = getErrorMessage(err);
|
|
332
|
+
return JSON.stringify({ error: `Password encryption failed: ${msg}` });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Step 3: Create vault via POST /setup
|
|
336
|
+
try {
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
|
|
339
|
+
const res = await fetch(`${WALLET_BASE_URL}/setup`, {
|
|
340
|
+
method: 'POST',
|
|
341
|
+
headers: { 'Content-Type': 'application/json' },
|
|
342
|
+
body: JSON.stringify({ encrypted, pubkey }),
|
|
343
|
+
signal: controller.signal,
|
|
344
|
+
});
|
|
345
|
+
clearTimeout(timeout);
|
|
346
|
+
|
|
347
|
+
const text = await res.text();
|
|
348
|
+
if (text.length > MAX_RESPONSE_SIZE) {
|
|
349
|
+
return text.slice(0, MAX_RESPONSE_SIZE) + '\n...[truncated]';
|
|
350
|
+
}
|
|
351
|
+
return text;
|
|
352
|
+
} catch (err) {
|
|
353
|
+
const msg = getErrorMessage(err);
|
|
354
|
+
return JSON.stringify({ error: `Vault creation failed: ${msg}` });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Handle request_human_action tool — creates a pending action request
|
|
360
|
+
* with a pre-computed action that auto-executes on approval.
|
|
361
|
+
*/
|
|
362
|
+
async function executeRequestHumanAction(
|
|
363
|
+
input: Record<string, unknown>,
|
|
364
|
+
token?: string,
|
|
365
|
+
): Promise<string> {
|
|
366
|
+
const { summary, permissions, action, limits, walletAccess, ttl, pubkey } = input as {
|
|
367
|
+
summary?: string;
|
|
368
|
+
permissions?: string[];
|
|
369
|
+
action?: { endpoint?: string; method?: string; body?: Record<string, unknown> };
|
|
370
|
+
limits?: Record<string, number>;
|
|
371
|
+
walletAccess?: string[];
|
|
372
|
+
ttl?: number;
|
|
373
|
+
pubkey?: string;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Validate inputs
|
|
377
|
+
if (!summary || typeof summary !== 'string' || summary.trim().length === 0) {
|
|
378
|
+
return JSON.stringify({ error: 'summary is required and must be a non-empty string' });
|
|
379
|
+
}
|
|
380
|
+
const MAX_SUMMARY_LENGTH = 500;
|
|
381
|
+
if (summary.length > MAX_SUMMARY_LENGTH) {
|
|
382
|
+
return JSON.stringify({ error: `summary must be ${MAX_SUMMARY_LENGTH} characters or fewer` });
|
|
383
|
+
}
|
|
384
|
+
if (!permissions || !Array.isArray(permissions) || permissions.length === 0) {
|
|
385
|
+
return JSON.stringify({ error: 'permissions must be a non-empty array' });
|
|
386
|
+
}
|
|
387
|
+
if (!action || typeof action !== 'object' || !action.endpoint || !action.method) {
|
|
388
|
+
return JSON.stringify({ error: 'action must have endpoint and method' });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Security: block privilege escalation (defense-in-depth — server also checks)
|
|
392
|
+
const blocked = permissions.filter((p: string) => p === 'admin:*' || p === 'action:create');
|
|
393
|
+
if (blocked.length > 0) {
|
|
394
|
+
return JSON.stringify({ error: `Cannot request privileged permissions: ${blocked.join(', ')}` });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Security: validate permission↔endpoint mapping
|
|
398
|
+
const endpointError = validatePermissionEndpoint(permissions, action.endpoint);
|
|
399
|
+
if (endpointError) {
|
|
400
|
+
return JSON.stringify({ error: endpointError });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!token) {
|
|
404
|
+
return JSON.stringify({ error: 'No auth token available — cannot create action request. To get a token, use wallet_api POST /auth with { agentId, profile } then poll GET /auth/<requestId>?secret=<secret> after human approval.' });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const actionPubkey = typeof pubkey === 'string' && pubkey.trim().length > 0
|
|
408
|
+
? pubkey
|
|
409
|
+
: generateAgentPubkeyPem();
|
|
410
|
+
|
|
411
|
+
// POST to /actions with the pre-computed action in metadata
|
|
412
|
+
const url = `${WALLET_BASE_URL}/actions`;
|
|
413
|
+
try {
|
|
414
|
+
const controller = new AbortController();
|
|
415
|
+
const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
|
|
416
|
+
|
|
417
|
+
const res = await fetch(url, {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: {
|
|
420
|
+
'Content-Type': 'application/json',
|
|
421
|
+
'Authorization': `Bearer ${token}`,
|
|
422
|
+
},
|
|
423
|
+
signal: controller.signal,
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
summary,
|
|
426
|
+
permissions,
|
|
427
|
+
limits: limits || {},
|
|
428
|
+
walletAccess: walletAccess || undefined,
|
|
429
|
+
ttl: ttl || 60,
|
|
430
|
+
pubkey: actionPubkey,
|
|
431
|
+
metadata: {
|
|
432
|
+
action: {
|
|
433
|
+
endpoint: action.endpoint,
|
|
434
|
+
method: action.method,
|
|
435
|
+
body: action.body,
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
});
|
|
440
|
+
clearTimeout(timeout);
|
|
441
|
+
|
|
442
|
+
const text = await res.text();
|
|
443
|
+
if (text.length > MAX_RESPONSE_SIZE) {
|
|
444
|
+
return text.slice(0, MAX_RESPONSE_SIZE) + '\n...[truncated]';
|
|
445
|
+
}
|
|
446
|
+
return text;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const msg = getErrorMessage(err);
|
|
449
|
+
return JSON.stringify({ error: `Action request failed: ${msg}` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Execute a wallet_api tool call */
|
|
454
|
+
async function executeWalletApi(
|
|
455
|
+
input: Record<string, unknown>,
|
|
456
|
+
token?: string,
|
|
457
|
+
): Promise<string> {
|
|
458
|
+
const { method, endpoint, body } = input as {
|
|
459
|
+
method: string;
|
|
460
|
+
endpoint: string;
|
|
461
|
+
body?: Record<string, unknown>;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Validate endpoint
|
|
465
|
+
if (!endpoint || typeof endpoint !== 'string' || !endpoint.startsWith('/')) {
|
|
466
|
+
return JSON.stringify({ error: 'endpoint must start with /' });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Block internal-only endpoints (defense-in-depth)
|
|
470
|
+
const BLOCKED_ENDPOINTS = ['/auth/internal', '/apps/internal', '/strategies/internal'];
|
|
471
|
+
if (BLOCKED_ENDPOINTS.some(prefix => endpoint.startsWith(prefix))) {
|
|
472
|
+
return JSON.stringify({ error: 'This endpoint is not accessible via MCP' });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Validate method
|
|
476
|
+
const upperMethod = (method || 'GET').toUpperCase();
|
|
477
|
+
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(upperMethod)) {
|
|
478
|
+
return JSON.stringify({ error: 'method must be GET, POST, PUT, or DELETE' });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const url = `${WALLET_BASE_URL}${endpoint}`;
|
|
482
|
+
const headers: Record<string, string> = {
|
|
483
|
+
'Content-Type': 'application/json',
|
|
484
|
+
};
|
|
485
|
+
if (token) {
|
|
486
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
|
|
492
|
+
|
|
493
|
+
const fetchOpts: RequestInit = {
|
|
494
|
+
method: upperMethod,
|
|
495
|
+
headers,
|
|
496
|
+
signal: controller.signal,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
if ((upperMethod === 'POST' || upperMethod === 'PUT') && body) {
|
|
500
|
+
fetchOpts.body = JSON.stringify(body);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const res = await fetch(url, fetchOpts);
|
|
504
|
+
clearTimeout(timeout);
|
|
505
|
+
|
|
506
|
+
const text = await res.text();
|
|
507
|
+
|
|
508
|
+
// Truncate to prevent context bloat, except encrypted credential reads
|
|
509
|
+
// where truncation breaks client-side decryption.
|
|
510
|
+
const bypassTruncation = /^\/credentials\/[^/]+\/read(?:\?.*)?$/.test(endpoint);
|
|
511
|
+
if (!bypassTruncation && text.length > MAX_RESPONSE_SIZE) {
|
|
512
|
+
return text.slice(0, MAX_RESPONSE_SIZE) + '\n...[truncated]';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return text;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
const msg = getErrorMessage(err);
|
|
518
|
+
if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED')) {
|
|
519
|
+
return JSON.stringify({ error: `Wallet server not reachable at ${WALLET_BASE_URL}. Is it running? Start it with: npx aurawallet start` });
|
|
520
|
+
}
|
|
521
|
+
return JSON.stringify({ error: `API call failed: ${msg}` });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import {
|
|
3
|
+
validateToken,
|
|
4
|
+
getTokenHash,
|
|
5
|
+
AgentTokenPayload,
|
|
6
|
+
} from '../lib/auth';
|
|
7
|
+
import { isRevoked } from '../lib/sessions';
|
|
8
|
+
import { logger } from '../lib/logger';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Auth info attached to requests
|
|
12
|
+
*/
|
|
13
|
+
export interface AuthInfo {
|
|
14
|
+
token: AgentTokenPayload;
|
|
15
|
+
tokenHash: string;
|
|
16
|
+
raw: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Extend Express Request to include auth info
|
|
20
|
+
declare global {
|
|
21
|
+
namespace Express {
|
|
22
|
+
interface Request {
|
|
23
|
+
auth?: AuthInfo;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Middleware that requires a valid Bearer token for all requests.
|
|
30
|
+
* Admin tokens are regular tokens with admin:* permission.
|
|
31
|
+
* Attaches auth info to req.auth on success.
|
|
32
|
+
*/
|
|
33
|
+
export function requireWalletAuth(req: Request, res: Response, next: NextFunction): void {
|
|
34
|
+
const authHeader = req.headers.authorization;
|
|
35
|
+
|
|
36
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
37
|
+
logger.authFailed('Missing authorization header', req.path);
|
|
38
|
+
res.status(401).json({ error: 'Authorization header required' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rawToken = authHeader.slice(7);
|
|
43
|
+
const token = validateToken(rawToken);
|
|
44
|
+
|
|
45
|
+
if (!token) {
|
|
46
|
+
logger.authFailed('Invalid or expired token', req.path);
|
|
47
|
+
res.status(401).json({ error: 'Invalid or expired token' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tokenHash = getTokenHash(rawToken);
|
|
52
|
+
|
|
53
|
+
if (isRevoked(tokenHash)) {
|
|
54
|
+
logger.authFailed('Token revoked', req.path, { tokenHash });
|
|
55
|
+
res.status(401).json({ error: 'Token has been revoked' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Attach auth info to request
|
|
60
|
+
req.auth = {
|
|
61
|
+
token,
|
|
62
|
+
tokenHash,
|
|
63
|
+
raw: rawToken,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
next();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Middleware that requires admin permissions.
|
|
71
|
+
* Must be used after requireWalletAuth (needs req.auth).
|
|
72
|
+
*/
|
|
73
|
+
export function requireAdmin(req: Request, res: Response, next: NextFunction): void {
|
|
74
|
+
if (!req.auth) {
|
|
75
|
+
res.status(401).json({ error: 'Authorization required' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for admin permission in token
|
|
80
|
+
const perms: string[] = req.auth.token.permissions || [];
|
|
81
|
+
const hasAdmin = perms.some(p => p === 'admin:*' || p === '*');
|
|
82
|
+
if (!hasAdmin) {
|
|
83
|
+
res.status(403).json({ error: 'Admin access required' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optional auth middleware - extracts token if present but doesn't require it.
|
|
92
|
+
* Useful for routes that behave differently for authenticated vs unauthenticated users.
|
|
93
|
+
*/
|
|
94
|
+
export function optionalWalletAuth(req: Request, res: Response, next: NextFunction): void {
|
|
95
|
+
const authHeader = req.headers.authorization;
|
|
96
|
+
|
|
97
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
98
|
+
// No auth - continue without setting req.auth
|
|
99
|
+
next();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rawToken = authHeader.slice(7);
|
|
104
|
+
const token = validateToken(rawToken);
|
|
105
|
+
|
|
106
|
+
if (token) {
|
|
107
|
+
const tokenHash = getTokenHash(rawToken);
|
|
108
|
+
|
|
109
|
+
if (!isRevoked(tokenHash)) {
|
|
110
|
+
req.auth = {
|
|
111
|
+
token,
|
|
112
|
+
tokenHash,
|
|
113
|
+
raw: rawToken,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
next();
|
|
119
|
+
}
|