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,703 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
|
3
|
+
import { prisma } from '../lib/db';
|
|
4
|
+
import { requireWalletAuth } from '../middleware/auth';
|
|
5
|
+
import { requirePermission } from '../lib/permissions';
|
|
6
|
+
import { ApprovalRouter, loadAdaptersFromDb } from '../lib/adapters';
|
|
7
|
+
import { SERVER_PORT } from '../lib/config';
|
|
8
|
+
import { validateExternalUrl } from '../lib/network';
|
|
9
|
+
import { logger } from '../lib/logger';
|
|
10
|
+
import { getErrorMessage } from '../lib/error';
|
|
11
|
+
|
|
12
|
+
/** Reference to the live approval router (set via setApprovalRouter) */
|
|
13
|
+
let approvalRouter: ApprovalRouter | null = null;
|
|
14
|
+
|
|
15
|
+
/** In-memory nonce store for Telegram chat ID auto-detection */
|
|
16
|
+
interface SetupNonce {
|
|
17
|
+
botToken: string;
|
|
18
|
+
botUsername: string;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
/** getUpdates offset — skip all updates before this ID */
|
|
21
|
+
offset?: number;
|
|
22
|
+
}
|
|
23
|
+
const telegramSetupNonces = new Map<string, SetupNonce>();
|
|
24
|
+
|
|
25
|
+
/** Exported for testing */
|
|
26
|
+
export { telegramSetupNonces };
|
|
27
|
+
|
|
28
|
+
/** Called from server/index.ts to share the approval router reference */
|
|
29
|
+
export function setApprovalRouter(router: ApprovalRouter | null): void {
|
|
30
|
+
approvalRouter = router;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Called from server/index.ts to read current router */
|
|
34
|
+
export function getApprovalRouter(): ApprovalRouter | null {
|
|
35
|
+
return approvalRouter;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const router = Router();
|
|
39
|
+
|
|
40
|
+
// GET /adapters — List configured adapters
|
|
41
|
+
router.get('/', requireWalletAuth, requirePermission('adapter:manage'), async (_req: Request, res: Response) => {
|
|
42
|
+
try {
|
|
43
|
+
const appConfig = await prisma.appConfig.findUnique({
|
|
44
|
+
where: { id: 'global' },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let config: { enabled: boolean; chat?: { defaultApp?: string }; adapters: Array<{ type: string; enabled: boolean; config: Record<string, unknown>; chat?: { enabled?: boolean } }> } = {
|
|
48
|
+
enabled: false,
|
|
49
|
+
adapters: [],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (appConfig?.adapterConfig) {
|
|
53
|
+
try {
|
|
54
|
+
config = JSON.parse(appConfig.adapterConfig);
|
|
55
|
+
} catch {
|
|
56
|
+
// Invalid JSON, return default
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check which adapter types have secrets stored
|
|
61
|
+
const secretKeys = await prisma.apiKey.findMany({
|
|
62
|
+
where: { service: { startsWith: 'adapter:' }, isActive: true },
|
|
63
|
+
select: { service: true, name: true },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const secretsByType: Record<string, string[]> = {};
|
|
67
|
+
for (const key of secretKeys) {
|
|
68
|
+
const adapterType = key.service.replace('adapter:', '');
|
|
69
|
+
if (!secretsByType[adapterType]) secretsByType[adapterType] = [];
|
|
70
|
+
secretsByType[adapterType].push(key.name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Annotate adapters with secret status
|
|
74
|
+
const adapters = (config.adapters || []).map((a) => ({
|
|
75
|
+
type: a.type,
|
|
76
|
+
enabled: a.enabled,
|
|
77
|
+
config: a.config,
|
|
78
|
+
chat: a.chat,
|
|
79
|
+
hasSecrets: (secretsByType[a.type] || []).length > 0,
|
|
80
|
+
secretKeys: secretsByType[a.type] || [],
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
res.json({
|
|
84
|
+
success: true,
|
|
85
|
+
enabled: config.enabled,
|
|
86
|
+
chat: config.chat,
|
|
87
|
+
adapters,
|
|
88
|
+
running: approvalRouter !== null,
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = getErrorMessage(error);
|
|
92
|
+
res.status(500).json({ error: message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// POST /adapters — Save adapter config
|
|
97
|
+
router.post('/', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
|
|
98
|
+
try {
|
|
99
|
+
const { type, enabled, config, chat: chatConfig } = req.body;
|
|
100
|
+
|
|
101
|
+
if (!type || typeof type !== 'string') {
|
|
102
|
+
res.status(400).json({ error: 'type is required' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof enabled !== 'boolean') {
|
|
107
|
+
res.status(400).json({ error: 'enabled must be a boolean' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Read current config
|
|
112
|
+
const appConfig = await prisma.appConfig.findUnique({
|
|
113
|
+
where: { id: 'global' },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let current: { enabled: boolean; chat?: { defaultApp?: string }; adapters: Array<{ type: string; enabled: boolean; config: Record<string, unknown>; chat?: { enabled?: boolean } }> } = {
|
|
117
|
+
enabled: true,
|
|
118
|
+
adapters: [],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (appConfig?.adapterConfig) {
|
|
122
|
+
try {
|
|
123
|
+
current = JSON.parse(appConfig.adapterConfig);
|
|
124
|
+
} catch {
|
|
125
|
+
// Reset on invalid JSON
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Upsert the adapter entry
|
|
130
|
+
const idx = current.adapters.findIndex((a) => a.type === type);
|
|
131
|
+
const entry: { type: string; enabled: boolean; config: Record<string, unknown>; chat?: { enabled?: boolean } } = { type, enabled, config: config || {} };
|
|
132
|
+
if (chatConfig && typeof chatConfig === 'object') {
|
|
133
|
+
entry.chat = chatConfig;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (idx >= 0) {
|
|
137
|
+
current.adapters[idx] = entry;
|
|
138
|
+
} else {
|
|
139
|
+
current.adapters.push(entry);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If any adapter is being enabled, enable the system
|
|
143
|
+
if (enabled) {
|
|
144
|
+
current.enabled = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await prisma.appConfig.upsert({
|
|
148
|
+
where: { id: 'global' },
|
|
149
|
+
update: { adapterConfig: JSON.stringify(current) },
|
|
150
|
+
create: { id: 'global', adapterConfig: JSON.stringify(current) },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const action = idx >= 0 ? 'updated' : 'created';
|
|
154
|
+
logger.adapterChanged(action, type);
|
|
155
|
+
|
|
156
|
+
res.json({ success: true, adapter: entry });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const message = getErrorMessage(error);
|
|
159
|
+
res.status(500).json({ error: message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// DELETE /adapters/:type — Remove adapter config
|
|
164
|
+
router.delete('/:type', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
|
|
165
|
+
try {
|
|
166
|
+
const { type } = req.params;
|
|
167
|
+
|
|
168
|
+
const appConfig = await prisma.appConfig.findUnique({
|
|
169
|
+
where: { id: 'global' },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!appConfig?.adapterConfig) {
|
|
173
|
+
res.status(404).json({ error: 'No adapter config found' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let current: { enabled: boolean; adapters: Array<{ type: string; enabled: boolean; config: Record<string, unknown> }> };
|
|
178
|
+
try {
|
|
179
|
+
current = JSON.parse(appConfig.adapterConfig);
|
|
180
|
+
} catch {
|
|
181
|
+
res.status(500).json({ error: 'Invalid adapter config in database' });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const idx = current.adapters.findIndex((a) => a.type === type);
|
|
186
|
+
if (idx < 0) {
|
|
187
|
+
res.status(404).json({ error: `Adapter type '${type}' not found` });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
current.adapters.splice(idx, 1);
|
|
192
|
+
|
|
193
|
+
// If no adapters remain, disable the system
|
|
194
|
+
if (current.adapters.length === 0) {
|
|
195
|
+
current.enabled = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await prisma.appConfig.update({
|
|
199
|
+
where: { id: 'global' },
|
|
200
|
+
data: { adapterConfig: JSON.stringify(current) },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
logger.adapterChanged('deleted', type);
|
|
204
|
+
|
|
205
|
+
res.json({ success: true, message: `Adapter '${type}' removed` });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const message = getErrorMessage(error);
|
|
208
|
+
res.status(500).json({ error: message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// POST /adapters/test — Send a test message through a configured adapter
|
|
213
|
+
router.post('/test', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
|
|
214
|
+
try {
|
|
215
|
+
const { type } = req.body;
|
|
216
|
+
|
|
217
|
+
if (!type || typeof type !== 'string') {
|
|
218
|
+
res.status(400).json({ error: 'type is required' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 5s timeout for external calls
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
switch (type) {
|
|
228
|
+
case 'telegram': {
|
|
229
|
+
// Read bot token from DB
|
|
230
|
+
const tokenKey = await prisma.apiKey.findFirst({
|
|
231
|
+
where: { service: 'adapter:telegram', name: 'botToken', isActive: true },
|
|
232
|
+
});
|
|
233
|
+
if (!tokenKey) {
|
|
234
|
+
console.warn('[adapters] telegram test: bot token not found in ApiKey table');
|
|
235
|
+
res.status(400).json({ error: 'Telegram bot token not configured' });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Read chat ID from adapter config
|
|
240
|
+
const appConfig = await prisma.appConfig.findUnique({ where: { id: 'global' } });
|
|
241
|
+
let chatId: string | undefined;
|
|
242
|
+
if (appConfig?.adapterConfig) {
|
|
243
|
+
try {
|
|
244
|
+
const config = JSON.parse(appConfig.adapterConfig);
|
|
245
|
+
const telegramAdapter = config.adapters?.find((a: { type: string }) => a.type === 'telegram');
|
|
246
|
+
chatId = telegramAdapter?.config?.chatId ? String(telegramAdapter.config.chatId) : undefined;
|
|
247
|
+
} catch { /* ignore parse errors */ }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!chatId) {
|
|
251
|
+
console.warn('[adapters] telegram test: chatId not found in adapter config');
|
|
252
|
+
res.status(400).json({ error: 'Telegram chat ID not configured. Reconfigure the adapter with a chat ID.' });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const resp = await fetch(`https://api.telegram.org/bot${tokenKey.key}/sendMessage`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
chat_id: chatId,
|
|
261
|
+
text: '<b>AuraWallet</b>\n\nAdapter chat is now active. You can send messages here and the AI will respond.',
|
|
262
|
+
parse_mode: 'HTML',
|
|
263
|
+
}),
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const data = await resp.json() as { ok?: boolean; description?: string };
|
|
268
|
+
if (data.ok) {
|
|
269
|
+
res.json({ success: true });
|
|
270
|
+
} else {
|
|
271
|
+
console.warn('[adapters] telegram test: API call failed:', data.description);
|
|
272
|
+
res.json({ success: false, error: data.description || 'Failed to send test message' });
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'webhook': {
|
|
278
|
+
// Read webhook URL from adapter config
|
|
279
|
+
const appConfig2 = await prisma.appConfig.findUnique({ where: { id: 'global' } });
|
|
280
|
+
let webhookUrl: string | undefined;
|
|
281
|
+
if (appConfig2?.adapterConfig) {
|
|
282
|
+
try {
|
|
283
|
+
const config = JSON.parse(appConfig2.adapterConfig);
|
|
284
|
+
const webhookAdapter = config.adapters?.find((a: { type: string }) => a.type === 'webhook');
|
|
285
|
+
webhookUrl = webhookAdapter?.config?.url;
|
|
286
|
+
} catch { /* ignore parse errors */ }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!webhookUrl) {
|
|
290
|
+
res.status(404).json({ error: 'Webhook URL not configured' });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// SSRF protection: validate webhook URL before sending test
|
|
295
|
+
try {
|
|
296
|
+
await validateExternalUrl(webhookUrl);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
const msg = getErrorMessage(err);
|
|
299
|
+
res.status(403).json({ error: msg });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const resp = await fetch(webhookUrl, {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
type: 'test',
|
|
308
|
+
data: {},
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
}),
|
|
311
|
+
signal: controller.signal,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (resp.ok) {
|
|
315
|
+
res.json({ success: true });
|
|
316
|
+
} else {
|
|
317
|
+
res.json({ success: false, error: `Webhook returned ${resp.status}` });
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
default:
|
|
323
|
+
res.status(400).json({ error: `Unknown adapter type: ${type}` });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
328
|
+
res.json({ success: false, error: 'Test timed out' });
|
|
329
|
+
} else {
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
clearTimeout(timeout);
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = getErrorMessage(error);
|
|
337
|
+
res.status(500).json({ error: message });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// POST /adapters/chat — Update top-level chat config (defaultApp)
|
|
342
|
+
router.post('/chat', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
|
|
343
|
+
try {
|
|
344
|
+
const { defaultApp } = req.body;
|
|
345
|
+
|
|
346
|
+
const appConfig = await prisma.appConfig.findUnique({
|
|
347
|
+
where: { id: 'global' },
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
let current: { enabled: boolean; chat?: { defaultApp?: string }; adapters: unknown[] } = {
|
|
351
|
+
enabled: true,
|
|
352
|
+
adapters: [],
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
if (appConfig?.adapterConfig) {
|
|
356
|
+
try {
|
|
357
|
+
current = JSON.parse(appConfig.adapterConfig);
|
|
358
|
+
} catch {
|
|
359
|
+
// Reset on invalid JSON
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
current.chat = {
|
|
364
|
+
...current.chat,
|
|
365
|
+
defaultApp: defaultApp || undefined,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
await prisma.appConfig.upsert({
|
|
369
|
+
where: { id: 'global' },
|
|
370
|
+
update: { adapterConfig: JSON.stringify(current) },
|
|
371
|
+
create: { id: 'global', adapterConfig: JSON.stringify(current) },
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Clear the app cache in the router
|
|
375
|
+
if (approvalRouter) {
|
|
376
|
+
approvalRouter.clearAppCache();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
res.json({ success: true, chat: current.chat });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const message = getErrorMessage(error);
|
|
382
|
+
res.status(500).json({ error: message });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// POST /adapters/:type/message — Inbound chat from external adapter (HMAC-authenticated)
|
|
387
|
+
router.post('/:type/message', async (req: Request, res: Response) => {
|
|
388
|
+
try {
|
|
389
|
+
const { type } = req.params;
|
|
390
|
+
const { text, senderId } = req.body;
|
|
391
|
+
|
|
392
|
+
if (!text || typeof text !== 'string') {
|
|
393
|
+
res.status(400).json({ error: 'text is required' });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!senderId || typeof senderId !== 'string') {
|
|
398
|
+
res.status(400).json({ error: 'senderId is required' });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// HMAC authentication: look up the adapter's secret
|
|
403
|
+
const secretKey = await prisma.apiKey.findFirst({
|
|
404
|
+
where: { service: `adapter:${type}`, name: 'secret', isActive: true },
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (secretKey) {
|
|
408
|
+
// Validate HMAC signature
|
|
409
|
+
const signature = req.headers['x-signature-256'] as string | undefined;
|
|
410
|
+
if (!signature) {
|
|
411
|
+
res.status(401).json({ error: 'Missing X-Signature-256 header' });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const rawBody = JSON.stringify(req.body);
|
|
416
|
+
const expected = `sha256=${createHmac('sha256', secretKey.key).update(rawBody).digest('hex')}`;
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const sigBuf = Buffer.from(signature);
|
|
420
|
+
const expectedBuf = Buffer.from(expected);
|
|
421
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
422
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!approvalRouter) {
|
|
432
|
+
res.status(503).json({ error: 'Adapter router not running' });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const appId = await approvalRouter.resolveApp(req.body.targetApp);
|
|
437
|
+
if (!appId) {
|
|
438
|
+
res.status(400).json({ error: 'No target app configured. Set chat.defaultApp in adapter config.' });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const result = await approvalRouter.sendMessage(appId, text, undefined, type);
|
|
443
|
+
res.json({
|
|
444
|
+
success: !result.error,
|
|
445
|
+
reply: result.reply,
|
|
446
|
+
error: result.error,
|
|
447
|
+
});
|
|
448
|
+
} catch (error) {
|
|
449
|
+
const message = getErrorMessage(error);
|
|
450
|
+
res.status(500).json({ error: message });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// POST /adapters/telegram/setup-link — Generate deep link for auto-detecting chat ID
|
|
455
|
+
router.post('/telegram/setup-link', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
|
|
456
|
+
try {
|
|
457
|
+
// Accept bot token from body (pre-save flow) or read from DB
|
|
458
|
+
let botToken = req.body.botToken as string | undefined;
|
|
459
|
+
if (!botToken) {
|
|
460
|
+
const tokenKey = await prisma.apiKey.findFirst({
|
|
461
|
+
where: { service: 'adapter:telegram', name: 'botToken', isActive: true },
|
|
462
|
+
});
|
|
463
|
+
if (!tokenKey) {
|
|
464
|
+
res.status(400).json({ error: 'Bot token not provided and not saved. Pass botToken in body or save it first.' });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
botToken = tokenKey.key;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Validate bot token via getMe
|
|
471
|
+
const controller = new AbortController();
|
|
472
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
473
|
+
try {
|
|
474
|
+
const meResp = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
|
|
475
|
+
signal: controller.signal,
|
|
476
|
+
});
|
|
477
|
+
const meData = await meResp.json() as { ok?: boolean; result?: { username?: string }; description?: string };
|
|
478
|
+
if (!meData.ok) {
|
|
479
|
+
res.status(400).json({ error: meData.description || 'Invalid bot token' });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const botUsername = meData.result?.username || '';
|
|
484
|
+
|
|
485
|
+
// Generate nonce
|
|
486
|
+
const nonce = randomBytes(12).toString('base64url');
|
|
487
|
+
|
|
488
|
+
// Lazy cleanup of expired nonces
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
for (const [key, val] of telegramSetupNonces) {
|
|
491
|
+
if (val.expiresAt < now) telegramSetupNonces.delete(key);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Store nonce with 120s TTL
|
|
495
|
+
telegramSetupNonces.set(nonce, {
|
|
496
|
+
botToken,
|
|
497
|
+
botUsername,
|
|
498
|
+
expiresAt: now + 120_000,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Stop the running approval router so its Telegram polling doesn't
|
|
502
|
+
// consume the /start message before detect-chat can see it.
|
|
503
|
+
// It will be restarted after detection completes (via /adapters/restart).
|
|
504
|
+
if (approvalRouter) {
|
|
505
|
+
await approvalRouter.stop();
|
|
506
|
+
approvalRouter = null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Delete webhook to ensure getUpdates polling works
|
|
510
|
+
await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`, {
|
|
511
|
+
signal: controller.signal,
|
|
512
|
+
}).catch(() => { /* non-fatal */ });
|
|
513
|
+
|
|
514
|
+
// Flush stale updates so detect-chat only sees new messages.
|
|
515
|
+
// Call getUpdates with offset=-1 to get the last update, then
|
|
516
|
+
// confirm it so subsequent polls start fresh.
|
|
517
|
+
let nextOffset = 0;
|
|
518
|
+
try {
|
|
519
|
+
const flushResp = await fetch(
|
|
520
|
+
`https://api.telegram.org/bot${botToken}/getUpdates?offset=-1&timeout=0`,
|
|
521
|
+
{ signal: controller.signal },
|
|
522
|
+
);
|
|
523
|
+
const flushData = await flushResp.json() as { ok?: boolean; result?: Array<{ update_id: number }> };
|
|
524
|
+
if (flushData.ok && flushData.result?.length) {
|
|
525
|
+
const lastId = flushData.result[flushData.result.length - 1].update_id;
|
|
526
|
+
// Confirm the last update so it's removed from the queue
|
|
527
|
+
await fetch(
|
|
528
|
+
`https://api.telegram.org/bot${botToken}/getUpdates?offset=${lastId + 1}&timeout=0`,
|
|
529
|
+
{ signal: controller.signal },
|
|
530
|
+
);
|
|
531
|
+
nextOffset = lastId + 1;
|
|
532
|
+
}
|
|
533
|
+
} catch { /* non-fatal — detect-chat will still work, just may see stale updates */ }
|
|
534
|
+
|
|
535
|
+
// Store offset in nonce so detect-chat can skip old updates
|
|
536
|
+
const nonceEntry = telegramSetupNonces.get(nonce);
|
|
537
|
+
if (nonceEntry) nonceEntry.offset = nextOffset;
|
|
538
|
+
|
|
539
|
+
res.json({
|
|
540
|
+
success: true,
|
|
541
|
+
link: `https://t.me/${botUsername}?start=${nonce}`,
|
|
542
|
+
setupToken: nonce,
|
|
543
|
+
botUsername,
|
|
544
|
+
});
|
|
545
|
+
} finally {
|
|
546
|
+
clearTimeout(timeout);
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const message = getErrorMessage(error);
|
|
550
|
+
res.status(500).json({ error: message });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// POST /adapters/telegram/detect-chat — Poll for /start message to auto-detect chat ID
|
|
555
|
+
router.post('/telegram/detect-chat', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
|
|
556
|
+
try {
|
|
557
|
+
const { setupToken } = req.body;
|
|
558
|
+
if (!setupToken || typeof setupToken !== 'string') {
|
|
559
|
+
res.status(400).json({ error: 'setupToken is required' });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const nonceEntry = telegramSetupNonces.get(setupToken);
|
|
564
|
+
if (!nonceEntry) {
|
|
565
|
+
res.status(400).json({ error: 'Invalid or expired setup token' });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (nonceEntry.expiresAt < Date.now()) {
|
|
570
|
+
telegramSetupNonces.delete(setupToken);
|
|
571
|
+
res.status(400).json({ error: 'Setup token expired' });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const { botToken } = nonceEntry;
|
|
576
|
+
|
|
577
|
+
// Long-poll getUpdates (25s timeout)
|
|
578
|
+
const controller = new AbortController();
|
|
579
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
580
|
+
try {
|
|
581
|
+
const offsetParam = nonceEntry.offset ? `&offset=${nonceEntry.offset}` : '';
|
|
582
|
+
const updatesResp = await fetch(
|
|
583
|
+
`https://api.telegram.org/bot${botToken}/getUpdates?timeout=25&allowed_updates=${encodeURIComponent(JSON.stringify(['message']))}${offsetParam}`,
|
|
584
|
+
{ signal: controller.signal },
|
|
585
|
+
);
|
|
586
|
+
const updatesData = await updatesResp.json() as {
|
|
587
|
+
ok?: boolean;
|
|
588
|
+
result?: Array<{
|
|
589
|
+
update_id: number;
|
|
590
|
+
message?: {
|
|
591
|
+
text?: string;
|
|
592
|
+
chat?: { id: number; first_name?: string; username?: string };
|
|
593
|
+
};
|
|
594
|
+
}>;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
if (!updatesData.ok || !updatesData.result) {
|
|
598
|
+
res.json({ chatId: null, timeout: true });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Look for /start message
|
|
603
|
+
let detectedUpdate: (typeof updatesData.result)[number] | null = null;
|
|
604
|
+
let verified = false;
|
|
605
|
+
|
|
606
|
+
for (const update of updatesData.result) {
|
|
607
|
+
const text = update.message?.text || '';
|
|
608
|
+
if (text === `/start ${setupToken}`) {
|
|
609
|
+
detectedUpdate = update;
|
|
610
|
+
verified = true;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
if (text === '/start' || text.startsWith('/start ')) {
|
|
614
|
+
detectedUpdate = update;
|
|
615
|
+
verified = false;
|
|
616
|
+
// Keep looking for exact nonce match
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!detectedUpdate || !detectedUpdate.message?.chat) {
|
|
621
|
+
// Advance offset past any updates we just saw so the next
|
|
622
|
+
// poll attempt doesn't re-process them
|
|
623
|
+
if (updatesData.result.length > 0) {
|
|
624
|
+
const maxId = updatesData.result[updatesData.result.length - 1].update_id;
|
|
625
|
+
nonceEntry.offset = maxId + 1;
|
|
626
|
+
await fetch(
|
|
627
|
+
`https://api.telegram.org/bot${botToken}/getUpdates?offset=${maxId + 1}&timeout=0`,
|
|
628
|
+
{ signal: controller.signal },
|
|
629
|
+
).catch(() => { /* non-fatal */ });
|
|
630
|
+
}
|
|
631
|
+
res.json({ chatId: null, timeout: true });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const chat = detectedUpdate.message.chat;
|
|
636
|
+
const chatId = String(chat.id);
|
|
637
|
+
|
|
638
|
+
// Confirm the update by calling getUpdates with offset past it
|
|
639
|
+
await fetch(
|
|
640
|
+
`https://api.telegram.org/bot${botToken}/getUpdates?offset=${detectedUpdate.update_id + 1}&timeout=0`,
|
|
641
|
+
{ signal: controller.signal },
|
|
642
|
+
).catch(() => { /* non-fatal */ });
|
|
643
|
+
|
|
644
|
+
// Clean up nonce
|
|
645
|
+
telegramSetupNonces.delete(setupToken);
|
|
646
|
+
|
|
647
|
+
res.json({
|
|
648
|
+
chatId,
|
|
649
|
+
firstName: chat.first_name || null,
|
|
650
|
+
username: chat.username || null,
|
|
651
|
+
verified,
|
|
652
|
+
});
|
|
653
|
+
} finally {
|
|
654
|
+
clearTimeout(timeout);
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
658
|
+
res.json({ chatId: null, timeout: true });
|
|
659
|
+
} else {
|
|
660
|
+
const message = getErrorMessage(error);
|
|
661
|
+
res.status(500).json({ error: message });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// POST /adapters/restart — Restart approval router with current DB config
|
|
667
|
+
router.post('/restart', requireWalletAuth, requirePermission('adapter:manage'), async (_req: Request, res: Response) => {
|
|
668
|
+
try {
|
|
669
|
+
// Stop existing router
|
|
670
|
+
if (approvalRouter) {
|
|
671
|
+
await approvalRouter.stop();
|
|
672
|
+
approvalRouter = null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Load adapters from DB
|
|
676
|
+
const adapters = await loadAdaptersFromDb();
|
|
677
|
+
|
|
678
|
+
if (adapters.length === 0) {
|
|
679
|
+
res.json({ success: true, message: 'No adapters configured, router stopped', running: false });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const newRouter = new ApprovalRouter(`http://127.0.0.1:${SERVER_PORT}`);
|
|
684
|
+
for (const adapter of adapters) {
|
|
685
|
+
newRouter.registerAdapter(adapter);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
await newRouter.start();
|
|
689
|
+
approvalRouter = newRouter;
|
|
690
|
+
|
|
691
|
+
res.json({
|
|
692
|
+
success: true,
|
|
693
|
+
message: `Approval router started with ${adapters.length} adapter(s)`,
|
|
694
|
+
running: true,
|
|
695
|
+
adapterCount: adapters.length,
|
|
696
|
+
});
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const message = getErrorMessage(error);
|
|
699
|
+
res.status(500).json({ error: message });
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
export default router;
|