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.
Files changed (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,601 @@
1
+ /**
2
+ * App REST Routes
3
+ * ==================
4
+ * All app endpoints consolidated under /apps:
5
+ * - Storage (gated by app:storage)
6
+ * - Token retrieval (admin only, for iframe injection)
7
+ * - API Keys (gated by app:accesskey)
8
+ * - App Approval (gated by strategy:manage)
9
+ */
10
+
11
+ import { Router, Request, Response, NextFunction } from 'express';
12
+ import { createHash } from 'crypto';
13
+ import rateLimit, { ipKeyGenerator } from 'express-rate-limit';
14
+ import { requireWalletAuth } from '../middleware/auth';
15
+ import { requirePermission, isAdmin } from '../lib/permissions';
16
+ import { prisma } from '../lib/db';
17
+ import {
18
+ getRuntime,
19
+ enableStrategy,
20
+ disableStrategy,
21
+ handleAppMessage,
22
+ enqueueAppMessage,
23
+ waitForQueuedAppMessage,
24
+ STRATEGY_ENABLED_STORAGE_KEY,
25
+ } from '../lib/strategy/engine';
26
+ import { loadStrategyManifests } from '../lib/strategy/loader';
27
+ import { createAppToken, revokeAppToken, getAppToken } from '../lib/app-tokens';
28
+ import { validateExternalUrl } from '../lib/network';
29
+ import { onDefaultChanged, parseRateLimit, getDefaultSync } from '../lib/defaults';
30
+ import { logger } from '../lib/logger';
31
+ import { getErrorMessage } from '../lib/error';
32
+
33
+ const router = Router();
34
+
35
+ const bypassRateLimit = process.env.BYPASS_RATE_LIMIT === 'true';
36
+
37
+ function buildLimiterKey(req: Request, prefix: string): string {
38
+ const authHeader = req.headers.authorization;
39
+ if (authHeader?.startsWith('Bearer ')) {
40
+ return prefix + createHash('sha256').update(authHeader.slice(7)).digest('hex').slice(0, 16);
41
+ }
42
+ return prefix + ipKeyGenerator(req.ip || req.socket.remoteAddress || '127.0.0.1');
43
+ }
44
+
45
+ // Hot-reloadable rate limiter for app endpoints
46
+ function createAppHotLimiter(
47
+ defaultKey: string,
48
+ fallback: string,
49
+ prefix: string,
50
+ errorMsg: string,
51
+ ): (req: Request, res: Response, next: NextFunction) => void {
52
+ if (bypassRateLimit) return (_req, _res, next) => next();
53
+
54
+ const { max, windowMs } = parseRateLimit(getDefaultSync(defaultKey, fallback));
55
+ let inner = rateLimit({
56
+ windowMs, max, standardHeaders: true, legacyHeaders: false,
57
+ keyGenerator: (req) => buildLimiterKey(req, prefix),
58
+ handler: (_req, res) => { res.status(429).json({ success: false, error: errorMsg }); },
59
+ });
60
+
61
+ onDefaultChanged(defaultKey, (_key, value) => {
62
+ const updated = parseRateLimit(value);
63
+ inner = rateLimit({
64
+ windowMs: updated.windowMs, max: updated.max, standardHeaders: true, legacyHeaders: false,
65
+ keyGenerator: (req) => buildLimiterKey(req, prefix),
66
+ handler: (_req, res) => { res.status(429).json({ success: false, error: errorMsg }); },
67
+ });
68
+ });
69
+
70
+ return (req, res, next) => inner(req, res, next);
71
+ }
72
+
73
+ // Rate limit for /message endpoint (LLM calls are expensive)
74
+ const messageLimit = createAppHotLimiter('rate.app_message', '10,60000', '', 'Message rate limit exceeded');
75
+
76
+ // Rate limit for /fetch proxy
77
+ const fetchRateLimit = createAppHotLimiter('rate.app_fetch', '60,60000', 'fetch:', 'Fetch rate limit exceeded');
78
+
79
+ // ─── Storage scope enforcement ──────────────────────────────────────
80
+ // Ensures a app:storage token can only access its own storage.
81
+ // Admin tokens and app:storage:all bypass the scope check.
82
+
83
+ function getCallerAppId(agentId: string): string {
84
+ if (agentId.startsWith('strategy:')) return agentId.slice('strategy:'.length);
85
+ if (agentId.startsWith('app:')) return agentId.slice('app:'.length);
86
+ return agentId;
87
+ }
88
+
89
+ function enforceStorageScope(req: Request, res: Response, next: NextFunction): void {
90
+ const auth = req.auth!;
91
+ // Admin tokens and app:storage:all bypass scope
92
+ if (isAdmin(auth) || auth.token.permissions.includes('app:storage:all')) {
93
+ return next();
94
+ }
95
+ // Derive the caller's appId from the token's agentId
96
+ // Strategy tokens use "strategy:<appId>", app tokens use "app:<appId>"
97
+ const callerAppId = getCallerAppId(auth.token.agentId);
98
+ if (req.params.appId !== callerAppId) {
99
+ res.status(403).json({ success: false, error: "Cannot access another app's storage" });
100
+ return;
101
+ }
102
+ next();
103
+ }
104
+
105
+ // ─── Authenticated Storage (app:storage) ───────────────────────
106
+
107
+ /**
108
+ * GET /apps/:appId/storage — List all keys for a app
109
+ */
110
+ router.get('/:appId/storage', requireWalletAuth, requirePermission('app:storage'), enforceStorageScope, async (req: Request, res: Response) => {
111
+ try {
112
+ const { appId } = req.params;
113
+
114
+ const entries = await prisma.appStorage.findMany({
115
+ where: { appId },
116
+ select: { key: true, value: true, updatedAt: true },
117
+ orderBy: { updatedAt: 'desc' },
118
+ });
119
+
120
+ res.json({
121
+ success: true,
122
+ appId,
123
+ entries: entries.map(e => ({
124
+ key: e.key,
125
+ value: JSON.parse(e.value),
126
+ updatedAt: e.updatedAt.toISOString(),
127
+ })),
128
+ });
129
+ } catch (err) {
130
+ const message = getErrorMessage(err);
131
+ res.status(500).json({ success: false, error: message });
132
+ }
133
+ });
134
+
135
+ /**
136
+ * GET /apps/:appId/storage/:key — Get a single value
137
+ */
138
+ router.get('/:appId/storage/:key', requireWalletAuth, requirePermission('app:storage'), enforceStorageScope, async (req: Request, res: Response) => {
139
+ try {
140
+ const { appId, key } = req.params;
141
+
142
+ const entry = await prisma.appStorage.findUnique({
143
+ where: { appId_key: { appId, key } },
144
+ });
145
+
146
+ if (!entry) {
147
+ res.status(404).json({ success: false, error: 'Key not found' });
148
+ return;
149
+ }
150
+
151
+ res.json({
152
+ success: true,
153
+ appId,
154
+ key,
155
+ value: JSON.parse(entry.value),
156
+ updatedAt: entry.updatedAt.toISOString(),
157
+ });
158
+ } catch (err) {
159
+ const message = getErrorMessage(err);
160
+ res.status(500).json({ success: false, error: message });
161
+ }
162
+ });
163
+
164
+ /**
165
+ * PUT /apps/:appId/storage/:key — Set a value (upsert)
166
+ */
167
+ router.put('/:appId/storage/:key', requireWalletAuth, requirePermission('app:storage'), enforceStorageScope, async (req: Request, res: Response) => {
168
+ try {
169
+ const { appId, key } = req.params;
170
+ const { value } = req.body;
171
+
172
+ if (value === undefined) {
173
+ res.status(400).json({ success: false, error: 'value is required' });
174
+ return;
175
+ }
176
+
177
+ const serialized = JSON.stringify(value);
178
+
179
+ const entry = await prisma.appStorage.upsert({
180
+ where: { appId_key: { appId, key } },
181
+ update: { value: serialized },
182
+ create: { appId, key, value: serialized },
183
+ });
184
+
185
+ res.json({
186
+ success: true,
187
+ appId,
188
+ key,
189
+ value: JSON.parse(entry.value),
190
+ updatedAt: entry.updatedAt.toISOString(),
191
+ });
192
+ } catch (err) {
193
+ const message = getErrorMessage(err);
194
+ res.status(500).json({ success: false, error: message });
195
+ }
196
+ });
197
+
198
+ /**
199
+ * DELETE /apps/:appId/storage/:key — Delete a key
200
+ */
201
+ router.delete('/:appId/storage/:key', requireWalletAuth, requirePermission('app:storage'), enforceStorageScope, async (req: Request, res: Response) => {
202
+ try {
203
+ const { appId, key } = req.params;
204
+
205
+ const existing = await prisma.appStorage.findUnique({
206
+ where: { appId_key: { appId, key } },
207
+ });
208
+
209
+ if (!existing) {
210
+ res.status(404).json({ success: false, error: 'Key not found' });
211
+ return;
212
+ }
213
+
214
+ await prisma.appStorage.delete({
215
+ where: { appId_key: { appId, key } },
216
+ });
217
+
218
+ res.json({ success: true, appId, key });
219
+ } catch (err) {
220
+ const message = getErrorMessage(err);
221
+ res.status(500).json({ success: false, error: message });
222
+ }
223
+ });
224
+
225
+ // ─── App Messaging ──────────────────────────────────────────────
226
+
227
+ /**
228
+ * POST /apps/:appId/message — Send a message to the app's AI
229
+ * Body: { message: string }
230
+ * Requires: app:storage permission (same as storage ops), scoped to own app
231
+ */
232
+ router.post('/:appId/message', messageLimit, requireWalletAuth, requirePermission('app:storage'), enforceStorageScope, async (req: Request, res: Response) => {
233
+ const { appId } = req.params;
234
+ const { message, adapter } = req.body;
235
+
236
+ if (!message || typeof message !== 'string') {
237
+ res.status(400).json({ success: false, error: 'message is required' });
238
+ return;
239
+ }
240
+
241
+ try {
242
+ const adapterName = typeof adapter === 'string' ? adapter : 'dashboard';
243
+
244
+ // Fast path for built-in system chat: avoid queue + cron poll latency.
245
+ if (appId === '__system__') {
246
+ const direct = await handleAppMessage(appId, message, undefined, adapterName);
247
+ if (direct.error) {
248
+ res.status(400).json({ success: false, error: direct.error });
249
+ return;
250
+ }
251
+ res.json({ success: true, reply: direct.reply });
252
+ return;
253
+ }
254
+
255
+ // agent-chat widget flow: process directly with the caller's scoped app token.
256
+ const auth = req.auth!;
257
+ const callerAppId = getCallerAppId(auth.token.agentId);
258
+ const isScopedWidgetCaller = !isAdmin(auth)
259
+ && !auth.token.permissions.includes('app:storage:all')
260
+ && callerAppId === appId;
261
+
262
+ if (appId === 'agent-chat' && isScopedWidgetCaller) {
263
+ const authHeader = req.header('authorization') || '';
264
+ const callerToken = authHeader.startsWith('Bearer ')
265
+ ? authHeader.slice('Bearer '.length).trim()
266
+ : '';
267
+
268
+ if (!callerToken) {
269
+ res.status(401).json({ success: false, error: 'Missing bearer token' });
270
+ return;
271
+ }
272
+
273
+ const direct = await handleAppMessage(appId, message, undefined, adapterName, callerToken);
274
+ if (direct.error) {
275
+ res.status(400).json({ success: false, error: direct.error });
276
+ return;
277
+ }
278
+ res.json({ success: true, reply: direct.reply });
279
+ return;
280
+ }
281
+
282
+ const requestId = await enqueueAppMessage(appId, message, adapterName);
283
+ const timeoutMs = getDefaultSync<number>('strategy.message_timeout_ms', 120_000);
284
+ const result = await waitForQueuedAppMessage(requestId, timeoutMs);
285
+
286
+ if (result.status === 'timeout') {
287
+ res.status(504).json({ success: false, error: result.error || 'Timed out waiting for message processing' });
288
+ return;
289
+ }
290
+
291
+ if (result.status === 'error') {
292
+ res.status(400).json({ success: false, error: result.error || 'Message processing failed' });
293
+ return;
294
+ }
295
+
296
+ res.json({ success: true, reply: result.reply });
297
+ } catch (err) {
298
+ const msg = getErrorMessage(err);
299
+ res.status(500).json({ success: false, error: msg });
300
+ }
301
+ });
302
+
303
+ // ─── Fetch Proxy ─────────────────────────────────────────────────────
304
+
305
+ const ALLOWED_FETCH_METHODS = ['GET', 'POST', 'PUT', 'DELETE'];
306
+
307
+ /**
308
+ * POST /apps/:appId/fetch — Proxy an external HTTP request on behalf of a app
309
+ * Body: { url: string, method?: string, headers?: object, body?: string }
310
+ * Requires: app:storage permission (same as storage ops), scoped to own app
311
+ */
312
+ router.post('/:appId/fetch', fetchRateLimit, requireWalletAuth, requirePermission('app:storage'), enforceStorageScope, async (req: Request, res: Response) => {
313
+ const { url, method = 'GET', headers, body } = req.body;
314
+
315
+ // Validate URL
316
+ if (!url || typeof url !== 'string') {
317
+ res.status(400).json({ success: false, error: 'url is required' });
318
+ return;
319
+ }
320
+
321
+ // SSRF protection: validate protocol, resolve DNS, check for private IPs
322
+ try {
323
+ await validateExternalUrl(url);
324
+ } catch (err) {
325
+ const msg = getErrorMessage(err);
326
+ res.status(403).json({ success: false, error: msg });
327
+ return;
328
+ }
329
+
330
+ // Validate method
331
+ const upperMethod = (typeof method === 'string' ? method : 'GET').toUpperCase();
332
+ if (!ALLOWED_FETCH_METHODS.includes(upperMethod)) {
333
+ res.status(400).json({ success: false, error: `Method must be one of: ${ALLOWED_FETCH_METHODS.join(', ')}` });
334
+ return;
335
+ }
336
+
337
+ try {
338
+ const fetchOpts: RequestInit = {
339
+ method: upperMethod,
340
+ redirect: 'error',
341
+ signal: AbortSignal.timeout(10000),
342
+ };
343
+
344
+ if (headers && typeof headers === 'object') {
345
+ fetchOpts.headers = headers;
346
+ }
347
+
348
+ if (body !== undefined && upperMethod !== 'GET') {
349
+ fetchOpts.body = typeof body === 'string' ? body : JSON.stringify(body);
350
+ }
351
+
352
+ const response = await fetch(url, fetchOpts);
353
+
354
+ // Try to parse as JSON, fall back to text
355
+ const contentType = response.headers.get('content-type') || '';
356
+ let data: unknown;
357
+ if (contentType.includes('application/json')) {
358
+ data = await response.json();
359
+ } else {
360
+ data = await response.text();
361
+ }
362
+
363
+ res.json({ success: true, status: response.status, data });
364
+ } catch (err) {
365
+ if (err instanceof DOMException && err.name === 'TimeoutError') {
366
+ res.status(504).json({ success: false, error: 'Request timed out (10s)' });
367
+ return;
368
+ }
369
+ const msg = getErrorMessage(err);
370
+ res.status(502).json({ success: false, error: msg });
371
+ }
372
+ });
373
+
374
+ /**
375
+ * GET /apps/:appId/token — Get the pre-created token for a app
376
+ * Used by ThirdPartyApp to inject token into iframe blob.
377
+ */
378
+ router.get('/:appId/token', requireWalletAuth, (req: Request, res: Response, next: NextFunction) => {
379
+ if (!isAdmin(req.auth!)) {
380
+ res.status(403).json({ success: false, error: 'Admin access required' });
381
+ return;
382
+ }
383
+ next();
384
+ }, (req: Request, res: Response) => {
385
+ const { appId } = req.params;
386
+ const token = getAppToken(appId);
387
+
388
+ if (!token) {
389
+ res.status(404).json({ success: false, error: `No token for app "${appId}"` });
390
+ return;
391
+ }
392
+
393
+ res.json({ success: true, token });
394
+ });
395
+
396
+ // ─── API Key Access (app:accesskey) ────────────────────────────
397
+
398
+ /**
399
+ * GET /apps/:appId/apikey/:keyName — Get API key value from app storage
400
+ */
401
+ router.get('/:appId/apikey/:keyName', requireWalletAuth, requirePermission('app:accesskey'), async (req: Request, res: Response) => {
402
+ try {
403
+ const { appId, keyName } = req.params;
404
+
405
+ const entry = await prisma.appStorage.findUnique({
406
+ where: { appId_key: { appId, key: keyName } },
407
+ });
408
+
409
+ if (!entry) {
410
+ res.status(404).json({ success: false, error: 'API key not found' });
411
+ return;
412
+ }
413
+
414
+ res.json({
415
+ success: true,
416
+ appId,
417
+ keyName,
418
+ value: JSON.parse(entry.value),
419
+ });
420
+ } catch (err) {
421
+ const message = getErrorMessage(err);
422
+ res.status(500).json({ success: false, error: message });
423
+ }
424
+ });
425
+
426
+ // ─── App Hot Reload (admin only) ───────────────────────────────
427
+
428
+ /**
429
+ * POST /apps/:appId/reload — Create token for a newly installed app
430
+ * without requiring a full server restart.
431
+ */
432
+ router.post('/:appId/reload', requireWalletAuth, (req: Request, res: Response, next: NextFunction) => {
433
+ if (!isAdmin(req.auth!)) {
434
+ res.status(403).json({ success: false, error: 'Admin access required' });
435
+ return;
436
+ }
437
+ next();
438
+ }, async (req: Request, res: Response) => {
439
+ const { appId } = req.params;
440
+
441
+ try {
442
+ const token = await createAppToken(appId);
443
+ if (!token) {
444
+ res.status(500).json({ success: false, error: 'Failed to create app token' });
445
+ return;
446
+ }
447
+
448
+ logger.appOperation('reload', appId);
449
+ res.json({ success: true, appId, reloaded: true });
450
+ } catch (err) {
451
+ const message = getErrorMessage(err);
452
+ res.status(500).json({ success: false, error: message });
453
+ }
454
+ });
455
+
456
+ // ─── App Approval (strategy:manage) ────────────────────────────
457
+
458
+ /**
459
+ * POST /apps/:appId/approve — Approve app permissions
460
+ * Reads the app's manifest, creates a HumanAction record, enables strategy if loaded.
461
+ */
462
+ router.post('/:appId/approve', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
463
+ try {
464
+ const { appId } = req.params;
465
+
466
+ // Find the app's manifest
467
+ const manifests = loadStrategyManifests();
468
+ const manifest = manifests.find(m => m.id === appId);
469
+
470
+ if (!manifest) {
471
+ res.status(404).json({ success: false, error: `App "${appId}" not found or has no strategy manifest` });
472
+ return;
473
+ }
474
+
475
+ // Create or update approval record
476
+ // Find existing approval for this app
477
+ const existingApprovals = await prisma.humanAction.findMany({
478
+ where: { type: 'app:approve', status: 'approved' },
479
+ });
480
+ const existing = existingApprovals.find(a => {
481
+ try { return JSON.parse(a.metadata || '{}').appId === appId; } catch { return false; }
482
+ });
483
+
484
+ const metadata = JSON.stringify({
485
+ appId,
486
+ permissions: manifest.permissions,
487
+ limits: manifest.limits || null,
488
+ });
489
+
490
+ const approval = existing
491
+ ? await prisma.humanAction.update({
492
+ where: { id: existing.id },
493
+ data: { metadata, resolvedAt: new Date() },
494
+ })
495
+ : await prisma.humanAction.create({
496
+ data: {
497
+ type: 'app:approve',
498
+ fromTier: 'system',
499
+ chain: 'base',
500
+ status: 'approved',
501
+ resolvedAt: new Date(),
502
+ metadata,
503
+ },
504
+ });
505
+
506
+ // Create/replace app token in central registry
507
+ await createAppToken(appId);
508
+
509
+ // Keep explicit enable override aligned with approval flow.
510
+ await prisma.appStorage.upsert({
511
+ where: { appId_key: { appId, key: STRATEGY_ENABLED_STORAGE_KEY } },
512
+ create: {
513
+ appId,
514
+ key: STRATEGY_ENABLED_STORAGE_KEY,
515
+ value: JSON.stringify(true),
516
+ },
517
+ update: { value: JSON.stringify(true) },
518
+ });
519
+
520
+ // Enable the strategy if it's loaded and not already enabled
521
+ const runtime = getRuntime(appId);
522
+ if (runtime && !runtime.enabled) {
523
+ try {
524
+ await enableStrategy(appId);
525
+ } catch (err) {
526
+ console.error(`[apps] Failed to enable strategy ${appId} after approval:`, err);
527
+ }
528
+ }
529
+
530
+ logger.appOperation('install', appId);
531
+
532
+ res.json({
533
+ success: true,
534
+ appId,
535
+ permissions: manifest.permissions,
536
+ limits: manifest.limits || null,
537
+ approvedAt: approval.resolvedAt?.toISOString() || approval.createdAt.toISOString(),
538
+ });
539
+ } catch (err) {
540
+ const message = getErrorMessage(err);
541
+ res.status(500).json({ success: false, error: message });
542
+ }
543
+ });
544
+
545
+ /**
546
+ * DELETE /apps/:appId/approve — Revoke app approval
547
+ * Disables the strategy and deletes the approval record.
548
+ */
549
+ router.delete('/:appId/approve', requireWalletAuth, requirePermission('strategy:manage'), async (req: Request, res: Response) => {
550
+ try {
551
+ const { appId } = req.params;
552
+
553
+ // Disable the strategy if running
554
+ const runtime = getRuntime(appId);
555
+ if (runtime && runtime.enabled) {
556
+ try {
557
+ await disableStrategy(appId);
558
+ } catch (err) {
559
+ console.error(`[apps] Failed to disable strategy ${appId} on revoke:`, err);
560
+ }
561
+ }
562
+
563
+ // Revoke app token from central registry + add to revokedTokens set
564
+ await revokeAppToken(appId);
565
+
566
+ // Explicitly disable strategy runtime reconciliation for this app.
567
+ await prisma.appStorage.upsert({
568
+ where: { appId_key: { appId, key: STRATEGY_ENABLED_STORAGE_KEY } },
569
+ create: {
570
+ appId,
571
+ key: STRATEGY_ENABLED_STORAGE_KEY,
572
+ value: JSON.stringify(false),
573
+ },
574
+ update: { value: JSON.stringify(false) },
575
+ });
576
+
577
+ // Delete the approval record
578
+ try {
579
+ const approvalRecords = await prisma.humanAction.findMany({
580
+ where: { type: 'app:approve', status: 'approved' },
581
+ });
582
+ const existing = approvalRecords.find(a => {
583
+ try { return JSON.parse(a.metadata || '{}').appId === appId; } catch { return false; }
584
+ });
585
+ if (existing) {
586
+ await prisma.humanAction.delete({ where: { id: existing.id } });
587
+ }
588
+ } catch {
589
+ // Record may not exist — that's fine
590
+ }
591
+
592
+ logger.appOperation('uninstall', appId);
593
+
594
+ res.json({ success: true, appId, revoked: true });
595
+ } catch (err) {
596
+ const message = getErrorMessage(err);
597
+ res.status(500).json({ success: false, error: message });
598
+ }
599
+ });
600
+
601
+ export default router;