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,894 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy REST Routes
|
|
3
|
+
* ====================
|
|
4
|
+
* Endpoints for managing strategy lifecycle, config, and intent approvals.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { Router, Request, Response } from 'express';
|
|
9
|
+
import { requireWalletAuth } from '../middleware/auth';
|
|
10
|
+
import { requirePermission, isAdmin } from '../lib/permissions';
|
|
11
|
+
import { prisma } from '../lib/db';
|
|
12
|
+
import { events } from '../lib/events';
|
|
13
|
+
import { logger } from '../lib/logger';
|
|
14
|
+
import {
|
|
15
|
+
getStrategies,
|
|
16
|
+
enableStrategy,
|
|
17
|
+
disableStrategy,
|
|
18
|
+
reloadStrategies,
|
|
19
|
+
getRuntime,
|
|
20
|
+
isEngineStarted,
|
|
21
|
+
} from '../lib/strategy/engine';
|
|
22
|
+
import { getState } from '../lib/strategy/state';
|
|
23
|
+
import type { StrategyStatus, StrategyManifest } from '../lib/strategy/types';
|
|
24
|
+
import { getDefaultSync } from '../lib/defaults';
|
|
25
|
+
import { buildTemplateStrategy, isSupportedTemplate, listStrategyTemplates } from '../lib/strategy/templates';
|
|
26
|
+
import {
|
|
27
|
+
prepareThirdPartyStrategyFromManifest,
|
|
28
|
+
prepareThirdPartyStrategyFromSource,
|
|
29
|
+
} from '../lib/strategy/installer';
|
|
30
|
+
import {
|
|
31
|
+
listPersistedStrategies,
|
|
32
|
+
getPersistedStrategy,
|
|
33
|
+
createPersistedStrategy,
|
|
34
|
+
updatePersistedStrategyConfig,
|
|
35
|
+
updatePersistedStrategyEnabled,
|
|
36
|
+
} from '../lib/strategy/repository';
|
|
37
|
+
import { getErrorMessage } from '../lib/error';
|
|
38
|
+
import { createAppTokens, getAppToken } from '../lib/app-tokens';
|
|
39
|
+
|
|
40
|
+
const router = Router();
|
|
41
|
+
|
|
42
|
+
// Guard: prevent strategy mutations before cron starts the engine
|
|
43
|
+
router.use((req, res, next) => {
|
|
44
|
+
if (req.method === 'GET') return next(); // reads always OK (serve from DB)
|
|
45
|
+
if (req.path.startsWith('/internal/')) return next(); // internal endpoints for cron itself
|
|
46
|
+
if (req.path === '/' && req.method === 'POST') return next(); // creation writes to DB
|
|
47
|
+
if (req.path === '/install' && req.method === 'POST') return next(); // install writes to DB
|
|
48
|
+
if (req.path === '/reload') return next(); // reload has its own guard
|
|
49
|
+
|
|
50
|
+
const cronEnabled = getDefaultSync<boolean>('strategy.cron_enabled', true);
|
|
51
|
+
if (cronEnabled && !isEngineStarted()) {
|
|
52
|
+
res.status(503).json({
|
|
53
|
+
success: false,
|
|
54
|
+
error: 'Strategy engine not ready — waiting for cron to initialize',
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const STRATEGY_RUNNER_SYNC_KEY = 'strategy_runner';
|
|
62
|
+
|
|
63
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
64
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseInstallApprovalMetadata(
|
|
68
|
+
metadata: string | null | undefined,
|
|
69
|
+
): { strategyId?: string; permissions?: string[]; limits?: Record<string, unknown> } {
|
|
70
|
+
if (!metadata) return {};
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(metadata) as { strategyId?: string; permissions?: string[]; limits?: Record<string, unknown> };
|
|
73
|
+
return parsed || {};
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function findThirdPartyInstallApproval(strategyId: string): Promise<{ id: string; status: string } | null> {
|
|
80
|
+
const approvals = await prisma.humanAction.findMany({
|
|
81
|
+
where: { type: 'strategy:install:approve' },
|
|
82
|
+
select: { id: true, status: true, metadata: true },
|
|
83
|
+
orderBy: { createdAt: 'desc' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const row of approvals) {
|
|
87
|
+
const parsed = parseInstallApprovalMetadata(row.metadata);
|
|
88
|
+
if (parsed.strategyId === strategyId) {
|
|
89
|
+
return { id: row.id, status: row.status };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function buildStrategyStatuses(): Promise<StrategyStatus[]> {
|
|
97
|
+
const runtimeStatusById = new Map(getStrategies().map((status) => [status.id, status]));
|
|
98
|
+
const persisted = await listPersistedStrategies();
|
|
99
|
+
|
|
100
|
+
const persistedStatuses: StrategyStatus[] = persisted.map((strategy) => {
|
|
101
|
+
const runtime = runtimeStatusById.get(strategy.id);
|
|
102
|
+
return {
|
|
103
|
+
id: strategy.id,
|
|
104
|
+
name: strategy.name,
|
|
105
|
+
icon: strategy.manifest.icon,
|
|
106
|
+
ticker: runtime?.ticker ?? strategy.manifest.ticker,
|
|
107
|
+
enabled: runtime?.enabled ?? strategy.enabled,
|
|
108
|
+
running: runtime?.running ?? false,
|
|
109
|
+
lastTick: runtime?.lastTick ?? strategy.lastTickAt?.getTime(),
|
|
110
|
+
lastError: runtime?.lastError ?? strategy.lastError ?? undefined,
|
|
111
|
+
errorCount: runtime?.errorCount ?? strategy.errorCount,
|
|
112
|
+
pausedUntil: runtime?.pausedUntil,
|
|
113
|
+
} satisfies StrategyStatus;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return persistedStatuses;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* GET /strategies — List all strategies with status
|
|
121
|
+
*/
|
|
122
|
+
router.get('/', requireWalletAuth, requirePermission('strategy:read'), async (_req: Request, res: Response) => {
|
|
123
|
+
try {
|
|
124
|
+
const strategies = await buildStrategyStatuses();
|
|
125
|
+
res.json({ success: true, strategies });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const message = getErrorMessage(err);
|
|
128
|
+
res.status(500).json({ success: false, error: message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* GET /strategies/templates — List available strategy templates
|
|
134
|
+
*/
|
|
135
|
+
router.get('/templates', requireWalletAuth, requirePermission('strategy:read'), async (_req: Request, res: Response) => {
|
|
136
|
+
try {
|
|
137
|
+
const templates = listStrategyTemplates();
|
|
138
|
+
res.json({ success: true, templates });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const message = getErrorMessage(err);
|
|
141
|
+
res.status(500).json({ success: false, error: message });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* POST /strategies — Create a DB-backed strategy from a template
|
|
147
|
+
*/
|
|
148
|
+
router.post('/', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
149
|
+
try {
|
|
150
|
+
if (!isPlainObject(req.body)) {
|
|
151
|
+
res.status(400).json({ success: false, error: 'Request body must be a JSON object' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { template, name, mode = 'headless', config = {}, enabled = false } = req.body as {
|
|
156
|
+
template?: string;
|
|
157
|
+
name?: string;
|
|
158
|
+
mode?: string;
|
|
159
|
+
config?: unknown;
|
|
160
|
+
enabled?: boolean;
|
|
161
|
+
permissions?: unknown;
|
|
162
|
+
limits?: unknown;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!template || typeof template !== 'string') {
|
|
166
|
+
res.status(400).json({ success: false, error: 'template is required' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!isSupportedTemplate(template)) {
|
|
170
|
+
res.status(400).json({ success: false, error: `Unsupported template "${template}"` });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
174
|
+
res.status(400).json({ success: false, error: 'name is required' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (mode !== 'headless' && mode !== 'app-linked') {
|
|
178
|
+
res.status(400).json({ success: false, error: 'mode must be "headless" or "app-linked"' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isPlainObject(config)) {
|
|
182
|
+
res.status(400).json({ success: false, error: 'config must be an object' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (typeof enabled !== 'boolean') {
|
|
186
|
+
res.status(400).json({ success: false, error: 'enabled must be a boolean' });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if ('permissions' in req.body || 'limits' in req.body) {
|
|
190
|
+
res.status(400).json({
|
|
191
|
+
success: false,
|
|
192
|
+
error: 'permissions/limits cannot be overridden for template strategies',
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const strategyId = randomUUID();
|
|
198
|
+
let built;
|
|
199
|
+
try {
|
|
200
|
+
built = buildTemplateStrategy({
|
|
201
|
+
templateId: template,
|
|
202
|
+
strategyId,
|
|
203
|
+
strategyName: name.trim(),
|
|
204
|
+
mode,
|
|
205
|
+
rawConfig: config,
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const message = getErrorMessage(err);
|
|
209
|
+
res.status(400).json({ success: false, error: message });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const created = await createPersistedStrategy({
|
|
214
|
+
id: strategyId,
|
|
215
|
+
name: name.trim(),
|
|
216
|
+
templateId: template,
|
|
217
|
+
mode,
|
|
218
|
+
manifest: built.manifest,
|
|
219
|
+
config: built.config,
|
|
220
|
+
state: {},
|
|
221
|
+
schedule: built.schedule,
|
|
222
|
+
permissions: built.permissions,
|
|
223
|
+
limits: built.limits ?? null,
|
|
224
|
+
enabled: Boolean(enabled),
|
|
225
|
+
status: enabled ? 'enabled' : 'draft',
|
|
226
|
+
createdBy: req.auth?.token?.agentId?.startsWith('admin') ? 'human' : `agent:${req.auth?.token?.agentId || 'unknown'}`,
|
|
227
|
+
provenance: { source: 'template', templateId: template },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const runtime = getRuntime(created.id);
|
|
231
|
+
if (runtime && enabled && !runtime.enabled) {
|
|
232
|
+
await enableStrategy(created.id).catch((err) => console.warn(`[strategy:${created.id}] auto-enable after creation failed:`, getErrorMessage(err)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
res.status(201).json({
|
|
236
|
+
success: true,
|
|
237
|
+
strategy: {
|
|
238
|
+
id: created.id,
|
|
239
|
+
name: created.name,
|
|
240
|
+
templateId: created.templateId,
|
|
241
|
+
mode: created.mode,
|
|
242
|
+
enabled: created.enabled,
|
|
243
|
+
status: created.status,
|
|
244
|
+
config: created.config,
|
|
245
|
+
schedule: created.schedule,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const message = getErrorMessage(err);
|
|
250
|
+
res.status(500).json({ success: false, error: message });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* POST /strategies/install — Install/register a third-party strategy source or manifest
|
|
256
|
+
*/
|
|
257
|
+
router.post('/install', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
258
|
+
try {
|
|
259
|
+
if (!isPlainObject(req.body)) {
|
|
260
|
+
res.status(400).json({ success: false, error: 'Request body must be a JSON object' });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const {
|
|
265
|
+
source,
|
|
266
|
+
manifest,
|
|
267
|
+
sourceLabel,
|
|
268
|
+
name,
|
|
269
|
+
mode = 'headless',
|
|
270
|
+
config = {},
|
|
271
|
+
enabled = false,
|
|
272
|
+
approve = false,
|
|
273
|
+
} = req.body as {
|
|
274
|
+
source?: unknown;
|
|
275
|
+
manifest?: unknown;
|
|
276
|
+
sourceLabel?: unknown;
|
|
277
|
+
name?: unknown;
|
|
278
|
+
mode?: unknown;
|
|
279
|
+
config?: unknown;
|
|
280
|
+
enabled?: unknown;
|
|
281
|
+
approve?: unknown;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if ((typeof source === 'string') === Boolean(manifest)) {
|
|
285
|
+
res.status(400).json({
|
|
286
|
+
success: false,
|
|
287
|
+
error: 'Provide exactly one of "source" or "manifest"',
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (name !== undefined && (typeof name !== 'string' || name.trim().length === 0)) {
|
|
292
|
+
res.status(400).json({ success: false, error: 'name must be a non-empty string when provided' });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (mode !== 'headless' && mode !== 'app-linked') {
|
|
296
|
+
res.status(400).json({ success: false, error: 'mode must be "headless" or "app-linked"' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!isPlainObject(config)) {
|
|
300
|
+
res.status(400).json({ success: false, error: 'config must be an object' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (typeof enabled !== 'boolean') {
|
|
304
|
+
res.status(400).json({ success: false, error: 'enabled must be a boolean' });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (typeof approve !== 'boolean') {
|
|
308
|
+
res.status(400).json({ success: false, error: 'approve must be a boolean' });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (sourceLabel !== undefined && typeof sourceLabel !== 'string') {
|
|
312
|
+
res.status(400).json({ success: false, error: 'sourceLabel must be a string when provided' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const strategyId = randomUUID();
|
|
317
|
+
const prepared = typeof source === 'string'
|
|
318
|
+
? await prepareThirdPartyStrategyFromSource({
|
|
319
|
+
source,
|
|
320
|
+
strategyId,
|
|
321
|
+
strategyName: typeof name === 'string' ? name : undefined,
|
|
322
|
+
})
|
|
323
|
+
: prepareThirdPartyStrategyFromManifest({
|
|
324
|
+
manifest: manifest as StrategyManifest,
|
|
325
|
+
strategyId,
|
|
326
|
+
sourceLabel: typeof sourceLabel === 'string' ? sourceLabel : undefined,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const strategyName = typeof name === 'string' ? name.trim() : prepared.manifest.name || strategyId;
|
|
330
|
+
prepared.manifest.id = strategyId;
|
|
331
|
+
prepared.manifest.name = strategyName;
|
|
332
|
+
|
|
333
|
+
const permissions = Array.isArray(prepared.manifest.permissions)
|
|
334
|
+
? prepared.manifest.permissions
|
|
335
|
+
: [];
|
|
336
|
+
const limits = prepared.manifest.limits || null;
|
|
337
|
+
const requiresPermissionApproval = permissions.length > 0 || Boolean(limits);
|
|
338
|
+
|
|
339
|
+
let approvalRequired = false;
|
|
340
|
+
let approvalId: string | undefined;
|
|
341
|
+
|
|
342
|
+
if (requiresPermissionApproval) {
|
|
343
|
+
if (approve) {
|
|
344
|
+
if (!req.auth || !isAdmin(req.auth)) {
|
|
345
|
+
res.status(403).json({
|
|
346
|
+
success: false,
|
|
347
|
+
error: 'Explicit install approval requires admin authentication',
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const approval = await prisma.humanAction.create({
|
|
352
|
+
data: {
|
|
353
|
+
type: 'strategy:install:approve',
|
|
354
|
+
fromTier: 'system',
|
|
355
|
+
chain: 'base',
|
|
356
|
+
status: 'approved',
|
|
357
|
+
resolvedAt: new Date(),
|
|
358
|
+
metadata: JSON.stringify({
|
|
359
|
+
strategyId,
|
|
360
|
+
permissions,
|
|
361
|
+
limits,
|
|
362
|
+
provenance: prepared.provenance,
|
|
363
|
+
}),
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
approvalId = approval.id;
|
|
367
|
+
} else {
|
|
368
|
+
const approval = await prisma.humanAction.create({
|
|
369
|
+
data: {
|
|
370
|
+
type: 'strategy:install:approve',
|
|
371
|
+
fromTier: 'system',
|
|
372
|
+
chain: 'base',
|
|
373
|
+
status: 'pending',
|
|
374
|
+
metadata: JSON.stringify({
|
|
375
|
+
strategyId,
|
|
376
|
+
permissions,
|
|
377
|
+
limits,
|
|
378
|
+
provenance: prepared.provenance,
|
|
379
|
+
}),
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
approvalId = approval.id;
|
|
383
|
+
approvalRequired = true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const shouldEnable = enabled && !approvalRequired;
|
|
388
|
+
const created = await createPersistedStrategy({
|
|
389
|
+
id: strategyId,
|
|
390
|
+
name: strategyName,
|
|
391
|
+
mode,
|
|
392
|
+
manifest: prepared.manifest,
|
|
393
|
+
config: config as Record<string, unknown>,
|
|
394
|
+
state: {},
|
|
395
|
+
schedule: { kind: prepared.manifest.jobs?.length ? 'jobs' : prepared.manifest.ticker ? 'ticker' : 'manual' },
|
|
396
|
+
permissions,
|
|
397
|
+
limits,
|
|
398
|
+
enabled: shouldEnable,
|
|
399
|
+
status: approvalRequired ? 'awaiting_approval' : shouldEnable ? 'enabled' : 'draft',
|
|
400
|
+
createdBy: req.auth?.token?.agentId?.startsWith('admin') ? 'human' : `agent:${req.auth?.token?.agentId || 'unknown'}`,
|
|
401
|
+
provenance: {
|
|
402
|
+
source: 'third_party',
|
|
403
|
+
...prepared.provenance,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const runtime = getRuntime(created.id);
|
|
408
|
+
if (runtime && shouldEnable && !runtime.enabled) {
|
|
409
|
+
await enableStrategy(created.id).catch((err) => console.warn(`[strategy:${created.id}] auto-enable after install failed:`, getErrorMessage(err)));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
res.status(201).json({
|
|
413
|
+
success: true,
|
|
414
|
+
strategy: {
|
|
415
|
+
id: created.id,
|
|
416
|
+
name: created.name,
|
|
417
|
+
mode: created.mode,
|
|
418
|
+
enabled: created.enabled,
|
|
419
|
+
status: created.status,
|
|
420
|
+
permissions: created.permissions,
|
|
421
|
+
limits: created.limits,
|
|
422
|
+
provenance: created.provenance,
|
|
423
|
+
},
|
|
424
|
+
approvalRequired,
|
|
425
|
+
approvalId: approvalId || null,
|
|
426
|
+
});
|
|
427
|
+
} catch (err) {
|
|
428
|
+
const message = getErrorMessage(err);
|
|
429
|
+
res.status(400).json({ success: false, error: message });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* GET /strategies/health — Cron-owned strategy runtime health
|
|
435
|
+
*/
|
|
436
|
+
router.get('/health', requireWalletAuth, requirePermission('strategy:read'), async (_req: Request, res: Response) => {
|
|
437
|
+
try {
|
|
438
|
+
const cronEnabled = getDefaultSync<boolean>('strategy.cron_enabled', true);
|
|
439
|
+
const staleAfterMs = getDefaultSync<number>('strategy.health_stale_ms', 30_000);
|
|
440
|
+
|
|
441
|
+
const syncState = await prisma.syncState.findUnique({
|
|
442
|
+
where: { chain: STRATEGY_RUNNER_SYNC_KEY },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
const lastSyncAtMs = syncState?.lastSyncAt?.getTime() ?? null;
|
|
447
|
+
const isStale = cronEnabled
|
|
448
|
+
? !lastSyncAtMs || now - lastSyncAtMs > staleAfterMs
|
|
449
|
+
: false;
|
|
450
|
+
const isErrored = syncState?.lastSyncStatus === 'error';
|
|
451
|
+
const healthy = cronEnabled ? !isStale && !isErrored : true;
|
|
452
|
+
|
|
453
|
+
res.status(healthy ? 200 : 503).json({
|
|
454
|
+
success: healthy,
|
|
455
|
+
strategyRuntime: {
|
|
456
|
+
owner: 'cron',
|
|
457
|
+
cronEnabled,
|
|
458
|
+
apiEngineStarted: isEngineStarted(),
|
|
459
|
+
healthy,
|
|
460
|
+
staleAfterMs,
|
|
461
|
+
isStale,
|
|
462
|
+
lastSyncAt: syncState?.lastSyncAt?.toISOString() ?? null,
|
|
463
|
+
lastStatus: syncState?.lastSyncStatus ?? 'unknown',
|
|
464
|
+
lastError: syncState?.lastError ?? null,
|
|
465
|
+
syncCount: syncState?.syncCount ?? 0,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
} catch (err) {
|
|
469
|
+
const message = getErrorMessage(err);
|
|
470
|
+
res.status(500).json({ success: false, error: message });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
async function setStrategyEnabledState(id: string, nextEnabled: boolean): Promise<{
|
|
475
|
+
success: boolean;
|
|
476
|
+
status?: number;
|
|
477
|
+
error?: string;
|
|
478
|
+
enabled?: boolean;
|
|
479
|
+
owner?: 'api-runtime' | 'cron-runtime';
|
|
480
|
+
}> {
|
|
481
|
+
const persisted = await getPersistedStrategy(id);
|
|
482
|
+
if (!persisted) {
|
|
483
|
+
return { success: false, status: 404, error: `Strategy "${id}" not found` };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (nextEnabled) {
|
|
487
|
+
const isThirdParty = persisted.provenance?.source === 'third_party';
|
|
488
|
+
const needsInstallApproval = isThirdParty && (persisted.permissions.length > 0 || Boolean(persisted.limits));
|
|
489
|
+
if (needsInstallApproval) {
|
|
490
|
+
const approval = await findThirdPartyInstallApproval(id);
|
|
491
|
+
if (!approval || approval.status !== 'approved') {
|
|
492
|
+
return {
|
|
493
|
+
success: false,
|
|
494
|
+
status: 409,
|
|
495
|
+
error: `Strategy "${id}" requires explicit install approval before enabling`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const updated = await updatePersistedStrategyEnabled(id, nextEnabled);
|
|
502
|
+
if (!updated) {
|
|
503
|
+
return { success: false, status: 500, error: `Failed to update strategy "${id}"` };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const runtime = getRuntime(id);
|
|
507
|
+
if (runtime) {
|
|
508
|
+
if (nextEnabled && !runtime.enabled) {
|
|
509
|
+
await enableStrategy(id);
|
|
510
|
+
} else if (!nextEnabled && runtime.enabled) {
|
|
511
|
+
await disableStrategy(id);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
logger.strategyToggled(id, nextEnabled);
|
|
516
|
+
return {
|
|
517
|
+
success: true,
|
|
518
|
+
enabled: nextEnabled,
|
|
519
|
+
owner: runtime ? 'api-runtime' : 'cron-runtime',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* POST /strategies/:id/toggle — Enable/disable a strategy
|
|
525
|
+
*/
|
|
526
|
+
router.post('/:id/toggle', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
527
|
+
try {
|
|
528
|
+
const { id } = req.params;
|
|
529
|
+
const statuses = await buildStrategyStatuses();
|
|
530
|
+
const current = statuses.find((status) => status.id === id);
|
|
531
|
+
if (!current) {
|
|
532
|
+
res.status(404).json({ success: false, error: `Strategy "${id}" not found` });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const currentEnabled = current.enabled;
|
|
536
|
+
const nextEnabled = !currentEnabled;
|
|
537
|
+
|
|
538
|
+
const result = await setStrategyEnabledState(id, nextEnabled);
|
|
539
|
+
if (!result.success) {
|
|
540
|
+
res.status(result.status || 500).json({ success: false, error: result.error || 'Unknown error' });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
res.json({ success: true, enabled: result.enabled, owner: result.owner });
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const message = getErrorMessage(err);
|
|
547
|
+
res.status(500).json({ success: false, error: message });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* POST /strategies/:id/enable — Explicit enable endpoint
|
|
553
|
+
*/
|
|
554
|
+
router.post('/:id/enable', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
555
|
+
try {
|
|
556
|
+
const { id } = req.params;
|
|
557
|
+
const result = await setStrategyEnabledState(id, true);
|
|
558
|
+
if (!result.success) {
|
|
559
|
+
res.status(result.status || 500).json({ success: false, error: result.error || 'Unknown error' });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
res.json({ success: true, enabled: true, owner: result.owner });
|
|
563
|
+
} catch (err) {
|
|
564
|
+
const message = getErrorMessage(err);
|
|
565
|
+
res.status(500).json({ success: false, error: message });
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* POST /strategies/:id/disable — Explicit disable endpoint
|
|
571
|
+
*/
|
|
572
|
+
router.post('/:id/disable', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
573
|
+
try {
|
|
574
|
+
const { id } = req.params;
|
|
575
|
+
const result = await setStrategyEnabledState(id, false);
|
|
576
|
+
if (!result.success) {
|
|
577
|
+
res.status(result.status || 500).json({ success: false, error: result.error || 'Unknown error' });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
res.json({ success: true, enabled: false, owner: result.owner });
|
|
581
|
+
} catch (err) {
|
|
582
|
+
const message = getErrorMessage(err);
|
|
583
|
+
res.status(500).json({ success: false, error: message });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* GET /strategies/:id/config — Get effective config (manifest + overrides)
|
|
589
|
+
*/
|
|
590
|
+
router.get('/:id/config', requireWalletAuth, requirePermission('strategy:read'), async (req: Request, res: Response) => {
|
|
591
|
+
try {
|
|
592
|
+
const { id } = req.params;
|
|
593
|
+
const persisted = await getPersistedStrategy(id);
|
|
594
|
+
if (!persisted) {
|
|
595
|
+
res.status(404).json({ success: false, error: `Strategy "${id}" not found` });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const manifestConfig = isPlainObject(persisted.manifest.config)
|
|
600
|
+
? persisted.manifest.config as Record<string, unknown>
|
|
601
|
+
: {};
|
|
602
|
+
const overrides = persisted.config;
|
|
603
|
+
const effective = { ...manifestConfig, ...overrides };
|
|
604
|
+
|
|
605
|
+
res.json({
|
|
606
|
+
success: true,
|
|
607
|
+
config: effective,
|
|
608
|
+
manifest: manifestConfig,
|
|
609
|
+
overrides,
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
const message = getErrorMessage(err);
|
|
613
|
+
res.status(500).json({ success: false, error: message });
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* PUT /strategies/:id/config — Update config overrides
|
|
619
|
+
*/
|
|
620
|
+
router.put('/:id/config', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
621
|
+
try {
|
|
622
|
+
const { id } = req.params;
|
|
623
|
+
const overrides = req.body;
|
|
624
|
+
if (!overrides || typeof overrides !== 'object') {
|
|
625
|
+
res.status(400).json({ success: false, error: 'Request body must be a JSON object' });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const persisted = await getPersistedStrategy(id);
|
|
630
|
+
if (!persisted) {
|
|
631
|
+
res.status(404).json({ success: false, error: `Strategy "${id}" not found` });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const updated = await updatePersistedStrategyConfig(id, overrides as Record<string, unknown>);
|
|
636
|
+
if (!updated) {
|
|
637
|
+
res.status(500).json({ success: false, error: `Failed to update config for strategy "${id}"` });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const manifestConfig = isPlainObject(updated.manifest.config)
|
|
641
|
+
? updated.manifest.config as Record<string, unknown>
|
|
642
|
+
: {};
|
|
643
|
+
const effective = { ...manifestConfig, ...updated.config };
|
|
644
|
+
|
|
645
|
+
res.json({ success: true, config: effective });
|
|
646
|
+
} catch (err) {
|
|
647
|
+
const message = getErrorMessage(err);
|
|
648
|
+
res.status(500).json({ success: false, error: message });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* POST /strategies/:id/approve — Approve/reject pending intents
|
|
654
|
+
*/
|
|
655
|
+
router.post('/:id/approve', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
|
|
656
|
+
try {
|
|
657
|
+
const { id } = req.params;
|
|
658
|
+
const { approvalId, approved } = req.body;
|
|
659
|
+
|
|
660
|
+
if (!approvalId) {
|
|
661
|
+
res.status(400).json({ success: false, error: 'Missing approvalId' });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const approval = await prisma.humanAction.findUnique({
|
|
666
|
+
where: { id: approvalId },
|
|
667
|
+
});
|
|
668
|
+
const supportedTypes = new Set(['strategy:approve', 'strategy:install:approve']);
|
|
669
|
+
if (!approval || !supportedTypes.has(approval.type) || approval.status !== 'pending') {
|
|
670
|
+
res.status(404).json({ success: false, error: 'Approval not found or already resolved' });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let strategyId: string | undefined;
|
|
675
|
+
try {
|
|
676
|
+
const meta = JSON.parse(approval.metadata || '{}') as { strategyId?: string };
|
|
677
|
+
strategyId = meta.strategyId;
|
|
678
|
+
} catch {
|
|
679
|
+
strategyId = undefined;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (strategyId && strategyId !== id) {
|
|
683
|
+
res.status(400).json({ success: false, error: 'Approval does not belong to this strategy' });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
await prisma.humanAction.update({
|
|
688
|
+
where: { id: approvalId },
|
|
689
|
+
data: {
|
|
690
|
+
status: approved === false ? 'rejected' : 'approved',
|
|
691
|
+
resolvedAt: new Date(),
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (approval.type === 'strategy:install:approve' && strategyId) {
|
|
696
|
+
await prisma.strategy.updateMany({
|
|
697
|
+
where: { id: strategyId },
|
|
698
|
+
data: {
|
|
699
|
+
status: approved === false ? 'disabled' : 'draft',
|
|
700
|
+
enabled: false,
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
events.actionResolved({
|
|
706
|
+
id: approvalId,
|
|
707
|
+
type: approval.type,
|
|
708
|
+
approved: approved !== false,
|
|
709
|
+
resolvedBy: 'dashboard',
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
res.json({
|
|
713
|
+
success: true,
|
|
714
|
+
approved: approved !== false,
|
|
715
|
+
type: approval.type,
|
|
716
|
+
});
|
|
717
|
+
} catch (err) {
|
|
718
|
+
const message = getErrorMessage(err);
|
|
719
|
+
res.status(500).json({ success: false, error: message });
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* GET /strategies/:id/state — Get strategy state (for debugging)
|
|
725
|
+
*/
|
|
726
|
+
router.get('/:id/state', requireWalletAuth, requirePermission('strategy:read'), async (req: Request, res: Response) => {
|
|
727
|
+
try {
|
|
728
|
+
const { id } = req.params;
|
|
729
|
+
const persisted = await getPersistedStrategy(id);
|
|
730
|
+
if (!persisted) {
|
|
731
|
+
res.status(404).json({ success: false, error: `Strategy "${id}" not found` });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const runtime = getRuntime(id);
|
|
736
|
+
const state = runtime ? getState(id) : persisted.state;
|
|
737
|
+
|
|
738
|
+
const pendingRows = await prisma.humanAction.findMany({
|
|
739
|
+
where: {
|
|
740
|
+
status: 'pending',
|
|
741
|
+
OR: [
|
|
742
|
+
{ type: 'strategy:approve' },
|
|
743
|
+
{ type: 'strategy:install:approve' },
|
|
744
|
+
{ type: 'action' },
|
|
745
|
+
],
|
|
746
|
+
},
|
|
747
|
+
orderBy: { createdAt: 'asc' },
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const pendingApprovalsList = pendingRows
|
|
751
|
+
.map((row) => {
|
|
752
|
+
let metadata: { strategyId?: string; intents?: unknown[] } = {};
|
|
753
|
+
try {
|
|
754
|
+
metadata = JSON.parse(row.metadata || '{}');
|
|
755
|
+
} catch {
|
|
756
|
+
metadata = {};
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
id: row.id,
|
|
760
|
+
strategyId: metadata.strategyId,
|
|
761
|
+
intents: Array.isArray(metadata.intents) ? metadata.intents : [],
|
|
762
|
+
createdAt: row.createdAt.getTime(),
|
|
763
|
+
};
|
|
764
|
+
})
|
|
765
|
+
.filter((row) => row.strategyId === id);
|
|
766
|
+
|
|
767
|
+
res.json({
|
|
768
|
+
success: true,
|
|
769
|
+
state,
|
|
770
|
+
pendingApprovals: pendingApprovalsList,
|
|
771
|
+
});
|
|
772
|
+
} catch (err) {
|
|
773
|
+
const message = getErrorMessage(err);
|
|
774
|
+
res.status(500).json({ success: false, error: message });
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* GET /history — Strategy action history (from Event table)
|
|
780
|
+
*/
|
|
781
|
+
router.get('/history', requireWalletAuth, requirePermission('strategy:read'), async (req: Request, res: Response) => {
|
|
782
|
+
try {
|
|
783
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 50, 250);
|
|
784
|
+
const offset = parseInt(req.query.offset as string) || 0;
|
|
785
|
+
const strategyId = req.query.strategyId as string | undefined;
|
|
786
|
+
|
|
787
|
+
const where: Record<string, unknown> = {
|
|
788
|
+
type: { startsWith: 'strategy:' },
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// Filter by strategyId in data if provided
|
|
792
|
+
const events = await prisma.event.findMany({
|
|
793
|
+
where,
|
|
794
|
+
orderBy: { timestamp: 'desc' },
|
|
795
|
+
take: limit,
|
|
796
|
+
skip: offset,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Filter by strategyId in JSON data if needed
|
|
800
|
+
let filtered = events;
|
|
801
|
+
if (strategyId) {
|
|
802
|
+
filtered = events.filter(e => {
|
|
803
|
+
try {
|
|
804
|
+
const data = JSON.parse(e.data as string);
|
|
805
|
+
return data.strategyId === strategyId;
|
|
806
|
+
} catch {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
res.json({
|
|
813
|
+
success: true,
|
|
814
|
+
history: filtered.map(e => ({
|
|
815
|
+
id: e.id,
|
|
816
|
+
type: e.type,
|
|
817
|
+
data: JSON.parse(e.data as string),
|
|
818
|
+
timestamp: e.timestamp,
|
|
819
|
+
})),
|
|
820
|
+
count: filtered.length,
|
|
821
|
+
});
|
|
822
|
+
} catch (err) {
|
|
823
|
+
const message = getErrorMessage(err);
|
|
824
|
+
res.status(500).json({ success: false, error: message });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* POST /strategies/reload — Hot-reload strategies from disk
|
|
830
|
+
*/
|
|
831
|
+
router.post('/reload', requireWalletAuth, requirePermission('strategy:manage'), async (_req: Request, res: Response) => {
|
|
832
|
+
try {
|
|
833
|
+
const persisted = await listPersistedStrategies();
|
|
834
|
+
if (!isEngineStarted()) {
|
|
835
|
+
res.json({
|
|
836
|
+
success: true,
|
|
837
|
+
added: [],
|
|
838
|
+
removed: [],
|
|
839
|
+
total: persisted.length,
|
|
840
|
+
handledBy: 'cron:strategy-runner',
|
|
841
|
+
});
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const result = await reloadStrategies();
|
|
846
|
+
res.json({ success: true, ...result, total: persisted.length });
|
|
847
|
+
} catch (err) {
|
|
848
|
+
const message = getErrorMessage(err);
|
|
849
|
+
res.status(500).json({ success: false, error: message });
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// ─── Internal routes (localhost only, no auth) ─────────────────────
|
|
854
|
+
|
|
855
|
+
function isLocalhost(req: Request): boolean {
|
|
856
|
+
const ip = req.ip || req.socket.remoteAddress || '';
|
|
857
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function hasValidCronSecret(req: Request): boolean {
|
|
861
|
+
const required = process.env.STRATEGY_CRON_SHARED_SECRET;
|
|
862
|
+
if (!required) return false;
|
|
863
|
+
return req.header('x-strategy-cron-secret') === required;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* POST /strategies/internal/provision-tokens
|
|
868
|
+
* Re-provision app tokens after SIGNING_KEY rotation.
|
|
869
|
+
* Localhost-only, no auth token required.
|
|
870
|
+
*/
|
|
871
|
+
router.post('/internal/provision-tokens', async (req: Request, res: Response) => {
|
|
872
|
+
if (!isLocalhost(req)) {
|
|
873
|
+
res.status(403).json({ success: false, error: 'Internal only' });
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (!hasValidCronSecret(req)) {
|
|
877
|
+
res.status(403).json({ success: false, error: 'Invalid cron secret' });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
await createAppTokens();
|
|
882
|
+
const tokenMap: Record<string, string> = {};
|
|
883
|
+
const persisted = await listPersistedStrategies();
|
|
884
|
+
for (const s of persisted) {
|
|
885
|
+
const t = getAppToken(s.id);
|
|
886
|
+
if (t) tokenMap[s.id] = t;
|
|
887
|
+
}
|
|
888
|
+
res.json({ success: true, tokens: tokenMap });
|
|
889
|
+
} catch (err) {
|
|
890
|
+
res.status(500).json({ success: false, error: getErrorMessage(err) });
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
export default router;
|