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,340 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { requireWalletAuth } from '../middleware/auth';
3
+ import { requireAdmin } from '../lib/permissions';
4
+ import { readdir, stat, copyFile, unlink, rename } from 'fs/promises';
5
+ import { join, dirname } from 'path';
6
+ import { existsSync, mkdirSync } from 'fs';
7
+ import { DATA_PATHS, getDbPath } from '../lib/config';
8
+ import { logger } from '../lib/logger';
9
+ import Database from 'better-sqlite3';
10
+
11
+ const router = Router();
12
+
13
+ export function getBackupsDir(): string {
14
+ return join(dirname(getDbPath()), 'backups');
15
+ }
16
+
17
+ export function ensureBackupsDir(): void {
18
+ const dir = getBackupsDir();
19
+ if (!existsSync(dir)) {
20
+ mkdirSync(dir, { recursive: true });
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Run WAL checkpoint to flush all data to the main DB file.
26
+ */
27
+ function walCheckpoint(dbPath: string): void {
28
+ const db = new Database(dbPath, { readonly: false });
29
+ try {
30
+ db.pragma('wal_checkpoint(TRUNCATE)');
31
+ } finally {
32
+ db.close();
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Verify SQLite database integrity. Returns true if valid.
38
+ */
39
+ export function verifyIntegrity(dbPath: string): boolean {
40
+ const db = new Database(dbPath, { readonly: true });
41
+ try {
42
+ const result = db.pragma('integrity_check') as { integrity_check: string }[];
43
+ return result.length === 1 && result[0].integrity_check === 'ok';
44
+ } finally {
45
+ db.close();
46
+ }
47
+ }
48
+
49
+ interface BackupInfo {
50
+ filename: string;
51
+ timestamp: string;
52
+ size: number;
53
+ date: string;
54
+ }
55
+
56
+ /**
57
+ * GET /backup - List all backups
58
+ * Requires: admin permission
59
+ */
60
+ router.get('/', requireWalletAuth, requireAdmin, async (_req: Request, res: Response) => {
61
+ try {
62
+ const backupsDir = getBackupsDir();
63
+ ensureBackupsDir();
64
+ const files = await readdir(backupsDir);
65
+ const backups: BackupInfo[] = [];
66
+
67
+ for (const file of files) {
68
+ if (file.startsWith('aurawallet.db.') && file.endsWith('.bak')) {
69
+ const filePath = join(backupsDir, file);
70
+ const fileStat = await stat(filePath);
71
+
72
+ // Extract timestamp from filename: aurawallet.db.YYYYMMDD_HHMMSS.bak
73
+ const match = file.match(/aurawallet\.db\.(\d{8}_\d{6})\.bak/);
74
+ const timestamp = match ? match[1] : '';
75
+
76
+ backups.push({
77
+ filename: file,
78
+ timestamp,
79
+ size: fileStat.size,
80
+ date: fileStat.mtime.toISOString(),
81
+ });
82
+ }
83
+ }
84
+
85
+ // Sort by date descending (newest first)
86
+ backups.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
87
+
88
+ res.json({
89
+ success: true,
90
+ backups,
91
+ });
92
+ } catch (error) {
93
+ console.error('[Backup] Failed to list backups:', error);
94
+ res.status(500).json({ success: false, error: 'Failed to list backups' });
95
+ }
96
+ });
97
+
98
+ /**
99
+ * POST /backup - Create a new backup
100
+ * Requires: admin permission
101
+ */
102
+ router.post('/', requireWalletAuth, requireAdmin, async (_req: Request, res: Response) => {
103
+ try {
104
+ const dbFile = getDbPath();
105
+ const backupsDir = getBackupsDir();
106
+ ensureBackupsDir();
107
+
108
+ // Check if database exists
109
+ if (!existsSync(dbFile)) {
110
+ res.status(404).json({ success: false, error: 'Database file not found' });
111
+ return;
112
+ }
113
+
114
+ // Generate timestamp
115
+ const now = new Date();
116
+ const timestamp = now.toISOString()
117
+ .replace(/[-:]/g, '')
118
+ .replace('T', '_')
119
+ .slice(0, 15);
120
+
121
+ const backupFilename = `aurawallet.db.${timestamp}.bak`;
122
+ const backupPath = join(backupsDir, backupFilename);
123
+ let credentialsCopied = 0;
124
+
125
+ // WAL checkpoint — flush all data to main DB file
126
+ walCheckpoint(dbFile);
127
+
128
+ // Atomic write: copy to temp file, then rename
129
+ const tempPath = backupPath + '.tmp';
130
+ await copyFile(dbFile, tempPath);
131
+ await rename(tempPath, backupPath);
132
+
133
+ // Verify backup integrity
134
+ if (!verifyIntegrity(backupPath)) {
135
+ await unlink(backupPath);
136
+ res.status(500).json({ success: false, error: 'Backup failed integrity check' });
137
+ return;
138
+ }
139
+
140
+ // Copy credential files alongside the DB backup.
141
+ if (existsSync(DATA_PATHS.credentials)) {
142
+ const credentialFiles = (await readdir(DATA_PATHS.credentials))
143
+ .filter(file => file.startsWith('cred-') && file.endsWith('.json'));
144
+
145
+ for (const file of credentialFiles) {
146
+ await copyFile(
147
+ join(DATA_PATHS.credentials, file),
148
+ join(backupsDir, `credentials.${timestamp}.${file}`),
149
+ );
150
+ }
151
+ credentialsCopied = credentialFiles.length;
152
+ }
153
+
154
+ // Clean up old backups (keep last 10)
155
+ const files = await readdir(backupsDir);
156
+ const backupFiles = files
157
+ .filter(f => f.startsWith('aurawallet.db.') && f.endsWith('.bak'))
158
+ .sort()
159
+ .reverse();
160
+
161
+ // Delete older backups beyond the 10th
162
+ for (let i = 10; i < backupFiles.length; i++) {
163
+ const oldBackup = backupFiles[i];
164
+ await unlink(join(backupsDir, oldBackup));
165
+
166
+ const match = oldBackup.match(/aurawallet\.db\.(\d{8}_\d{6})\.bak/);
167
+ if (!match) continue;
168
+
169
+ const oldTimestamp = match[1];
170
+ const oldCredentialFiles = files.filter(f => f.startsWith(`credentials.${oldTimestamp}.cred-`) && f.endsWith('.json'));
171
+ for (const oldCredentialFile of oldCredentialFiles) {
172
+ await unlink(join(backupsDir, oldCredentialFile));
173
+ }
174
+ }
175
+
176
+ const fileStat = await stat(backupPath);
177
+
178
+ logger.backup(backupFilename);
179
+
180
+ res.json({
181
+ success: true,
182
+ backup: {
183
+ filename: backupFilename,
184
+ timestamp,
185
+ size: fileStat.size,
186
+ date: fileStat.mtime.toISOString(),
187
+ credentialsCopied,
188
+ },
189
+ });
190
+ } catch (error) {
191
+ console.error('[Backup] Failed to create backup:', error);
192
+ res.status(500).json({ success: false, error: 'Failed to create backup' });
193
+ }
194
+ });
195
+
196
+ /**
197
+ * PUT /backup - Restore from a backup
198
+ * Requires: admin permission
199
+ *
200
+ * Hardened to match CLI restore: atomic write, pre-restore backup,
201
+ * integrity verification, schema migrations, and WAL cleanup.
202
+ */
203
+ router.put('/', requireWalletAuth, requireAdmin, async (req: Request, res: Response) => {
204
+ try {
205
+ const { filename } = req.body;
206
+
207
+ if (!filename) {
208
+ res.status(400).json({ success: false, error: 'Filename is required' });
209
+ return;
210
+ }
211
+
212
+ // Validate filename format to prevent path traversal
213
+ if (!filename.match(/^aurawallet\.db\.\d{8}_\d{6}\.bak$/)) {
214
+ res.status(400).json({ success: false, error: 'Invalid backup filename' });
215
+ return;
216
+ }
217
+
218
+ const backupsDir = getBackupsDir();
219
+ const backupPath = join(backupsDir, filename);
220
+
221
+ if (!existsSync(backupPath)) {
222
+ res.status(404).json({ success: false, error: 'Backup file not found' });
223
+ return;
224
+ }
225
+
226
+ // Verify backup integrity before restoring
227
+ if (!verifyIntegrity(backupPath)) {
228
+ res.status(400).json({ success: false, error: 'Backup failed integrity check' });
229
+ return;
230
+ }
231
+
232
+ const dbPath = getDbPath();
233
+
234
+ // Create pre-restore safety backup
235
+ let preRestoreName = '';
236
+ if (existsSync(dbPath)) {
237
+ const now = new Date();
238
+ const ts = now.toISOString().replace(/[-:]/g, '').replace('T', '_').slice(0, 15);
239
+ preRestoreName = `pre-restore.${ts}.bak`;
240
+ await copyFile(dbPath, join(backupsDir, preRestoreName));
241
+ }
242
+
243
+ // Atomic write: copy to temp, then rename
244
+ const tempPath = dbPath + '.restore-tmp';
245
+ await copyFile(backupPath, tempPath);
246
+ await rename(tempPath, dbPath);
247
+
248
+ // Run schema migrations
249
+ try {
250
+ const { execSync } = await import('child_process');
251
+ execSync('npx prisma migrate deploy', {
252
+ cwd: join(dirname(dbPath), '..'),
253
+ env: { ...process.env, DATABASE_URL: `file:${dbPath}` },
254
+ encoding: 'utf-8',
255
+ stdio: ['pipe', 'pipe', 'pipe'],
256
+ });
257
+ } catch (migrationError: any) {
258
+ const errMsg = migrationError.stderr || migrationError.message || 'Unknown migration error';
259
+ logger.error(`[Backup] Migration failed after restore: ${errMsg}`);
260
+ // Restore the pre-restore backup since migration failed
261
+ if (preRestoreName && existsSync(join(backupsDir, preRestoreName))) {
262
+ const revertTemp = dbPath + '.revert-tmp';
263
+ await copyFile(join(backupsDir, preRestoreName), revertTemp);
264
+ await rename(revertTemp, dbPath);
265
+ }
266
+ res.status(500).json({
267
+ success: false,
268
+ error: `Restore aborted: schema migration failed. Pre-restore backup restored. Details: ${errMsg}`,
269
+ });
270
+ return;
271
+ }
272
+
273
+ // Verify restored DB integrity
274
+ if (!verifyIntegrity(dbPath)) {
275
+ logger.error('[Backup] Restored DB failed integrity check');
276
+ // Revert to pre-restore backup
277
+ if (preRestoreName && existsSync(join(backupsDir, preRestoreName))) {
278
+ const revertTemp = dbPath + '.revert-tmp';
279
+ await copyFile(join(backupsDir, preRestoreName), revertTemp);
280
+ await rename(revertTemp, dbPath);
281
+ }
282
+ res.status(500).json({
283
+ success: false,
284
+ error: 'Restored database failed integrity check. Pre-restore backup restored.',
285
+ });
286
+ return;
287
+ }
288
+
289
+ // Restore credential files from matching timestamp, if present.
290
+ const timestampMatch = filename.match(/aurawallet\.db\.(\d{8}_\d{6})\.bak$/);
291
+ let credentialsRestored = 0;
292
+ if (timestampMatch) {
293
+ const timestamp = timestampMatch[1];
294
+ const allBackupFiles = await readdir(backupsDir);
295
+ const credentialBackups = allBackupFiles
296
+ .filter(file => file.startsWith(`credentials.${timestamp}.cred-`) && file.endsWith('.json'));
297
+
298
+ if (credentialBackups.length > 0) {
299
+ if (!existsSync(DATA_PATHS.credentials)) {
300
+ mkdirSync(DATA_PATHS.credentials, { recursive: true });
301
+ }
302
+
303
+ const existingCredentialFiles = await readdir(DATA_PATHS.credentials);
304
+ for (const file of existingCredentialFiles) {
305
+ if (file.startsWith('cred-') && file.endsWith('.json')) {
306
+ await unlink(join(DATA_PATHS.credentials, file));
307
+ }
308
+ }
309
+
310
+ for (const backupFile of credentialBackups) {
311
+ const destName = backupFile.replace(`credentials.${timestamp}.`, '');
312
+ await copyFile(join(backupsDir, backupFile), join(DATA_PATHS.credentials, destName));
313
+ }
314
+ credentialsRestored = credentialBackups.length;
315
+ }
316
+ }
317
+
318
+ // Clean up old pre-restore backups (keep last 3)
319
+ const allFiles = await readdir(backupsDir);
320
+ const preRestoreFiles = allFiles
321
+ .filter(f => f.startsWith('pre-restore.') && f.endsWith('.bak'))
322
+ .sort()
323
+ .reverse();
324
+ for (let i = 3; i < preRestoreFiles.length; i++) {
325
+ await unlink(join(backupsDir, preRestoreFiles[i]));
326
+ }
327
+
328
+ res.json({
329
+ success: true,
330
+ message: 'Database restored successfully',
331
+ preRestoreBackup: preRestoreName,
332
+ credentialsRestored,
333
+ });
334
+ } catch (error) {
335
+ console.error('[Backup] Failed to restore backup:', error);
336
+ res.status(500).json({ success: false, error: 'Failed to restore backup' });
337
+ }
338
+ });
339
+
340
+ export default router;
@@ -0,0 +1,270 @@
1
+ /**
2
+ * POST /batch — Generic batch endpoint with dependency chaining.
3
+ *
4
+ * Dispatches sub-requests internally against the Express app.
5
+ * Auth is enforced per-sub-request via inherited headers.
6
+ */
7
+ import { Router, Request, Response } from 'express';
8
+ import http from 'http';
9
+ import { Readable } from 'stream';
10
+ import { Socket } from 'net';
11
+ import {
12
+ validateBatchRequest,
13
+ resolveTemplates,
14
+ resolveBodyTemplates,
15
+ type BatchSubRequest,
16
+ type BatchResponse,
17
+ } from '../lib/batch';
18
+
19
+ const router = Router();
20
+
21
+ const SUB_REQUEST_TIMEOUT_MS = 30_000;
22
+
23
+ /**
24
+ * Dispatch a sub-request internally through the Express app.
25
+ * Creates mock req/res objects and calls app.handle().
26
+ */
27
+ function dispatchInternal(
28
+ app: any,
29
+ method: string,
30
+ fullPath: string,
31
+ body: Record<string, unknown> | undefined,
32
+ parentReq: Request,
33
+ ): Promise<BatchResponse> {
34
+ return new Promise((resolve) => {
35
+ // Parse path and query string
36
+ const [pathname, queryString] = fullPath.split('?');
37
+ const query: Record<string, string> = {};
38
+ if (queryString) {
39
+ for (const pair of queryString.split('&')) {
40
+ const [key, ...rest] = pair.split('=');
41
+ query[decodeURIComponent(key)] = decodeURIComponent(rest.join('='));
42
+ }
43
+ }
44
+
45
+ // Build JSON body as a readable stream
46
+ const bodyStr = body ? JSON.stringify(body) : '';
47
+ const bodyBuf = Buffer.from(bodyStr);
48
+
49
+ // Create a mock IncomingMessage
50
+ const mockSocket = new Socket();
51
+ const req = new http.IncomingMessage(mockSocket);
52
+ req.method = method.toUpperCase();
53
+ req.url = fullPath;
54
+ (req as any).path = pathname;
55
+ (req as any).query = query;
56
+ (req as any).originalUrl = fullPath;
57
+
58
+ // Inherit auth headers from parent request
59
+ req.headers = {
60
+ 'content-type': 'application/json',
61
+ host: parentReq.headers.host || 'localhost',
62
+ };
63
+ if (parentReq.headers.authorization) {
64
+ req.headers.authorization = parentReq.headers.authorization;
65
+ }
66
+
67
+ // Set content-length for body parsing
68
+ if (bodyStr) {
69
+ req.headers['content-length'] = String(bodyBuf.length);
70
+ }
71
+
72
+ // Push body data so express.json() can parse it
73
+ if (bodyBuf.length > 0) {
74
+ req.push(bodyBuf);
75
+ }
76
+ req.push(null); // EOF
77
+
78
+ // Create a mock ServerResponse that captures the output
79
+ const res = new http.ServerResponse(req);
80
+ let statusCode = 200;
81
+ let responseBody: unknown = null;
82
+ let resolved = false;
83
+
84
+ // Capture status
85
+ const origWriteHead = res.writeHead.bind(res);
86
+ res.writeHead = function (code: number, ...args: any[]) {
87
+ statusCode = code;
88
+ return origWriteHead(code, ...args);
89
+ } as any;
90
+
91
+ // Also capture statusCode set directly
92
+ Object.defineProperty(res, 'statusCode', {
93
+ get: () => statusCode,
94
+ set: (v: number) => { statusCode = v; },
95
+ });
96
+
97
+ // Intercept res.end() to capture the response body
98
+ const chunks: Buffer[] = [];
99
+ const origWrite = res.write.bind(res);
100
+ res.write = function (chunk: any, ...args: any[]) {
101
+ if (chunk) {
102
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
103
+ }
104
+ return origWrite(chunk, ...args);
105
+ } as any;
106
+
107
+ const origEnd = res.end.bind(res);
108
+ res.end = function (chunk?: any, ...args: any[]) {
109
+ if (chunk) {
110
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
111
+ }
112
+ if (!resolved) {
113
+ resolved = true;
114
+ const fullBody = Buffer.concat(chunks).toString('utf-8');
115
+ try {
116
+ responseBody = JSON.parse(fullBody);
117
+ } catch {
118
+ responseBody = fullBody || null;
119
+ }
120
+ resolve({ status: statusCode, body: responseBody });
121
+ }
122
+ return origEnd(chunk, ...args);
123
+ } as any;
124
+
125
+ // Also intercept json() if Express adds it (belt & suspenders)
126
+ (res as any).json = function (data: unknown) {
127
+ if (!resolved) {
128
+ resolved = true;
129
+ resolve({ status: statusCode, body: data });
130
+ }
131
+ // Still call the real json to ensure proper cleanup
132
+ const jsonStr = JSON.stringify(data);
133
+ res.setHeader('Content-Type', 'application/json');
134
+ origEnd(jsonStr);
135
+ };
136
+
137
+ // Timeout
138
+ const timer = setTimeout(() => {
139
+ if (!resolved) {
140
+ resolved = true;
141
+ resolve({ status: 504, body: { error: 'Sub-request timed out' } });
142
+ mockSocket.destroy();
143
+ }
144
+ }, SUB_REQUEST_TIMEOUT_MS);
145
+
146
+ // Clean up timer when resolved
147
+ const origResolve = resolve;
148
+ const wrappedResolve = (value: BatchResponse) => {
149
+ clearTimeout(timer);
150
+ origResolve(value);
151
+ };
152
+ // Patch: the resolve calls above already fire, so clear on end
153
+ res.on('finish', () => clearTimeout(timer));
154
+
155
+ // Dispatch through the Express app
156
+ app.handle(req, res);
157
+ });
158
+ }
159
+
160
+ // ── POST /batch ──
161
+
162
+ router.post('/', async (req: Request, res: Response) => {
163
+ const { requests } = req.body || {};
164
+
165
+ // Validate
166
+ const validation = validateBatchRequest(requests);
167
+ if (!validation.valid) {
168
+ res.status(400).json({ success: false, error: validation.error });
169
+ return;
170
+ }
171
+
172
+ const { waves } = validation;
173
+ const requestMap = new Map<string, BatchSubRequest>();
174
+ for (const r of requests as BatchSubRequest[]) {
175
+ requestMap.set(r.id, r);
176
+ }
177
+
178
+ const responses = new Map<string, BatchResponse>();
179
+ const timings: Record<string, number> = {};
180
+
181
+ // Execute waves sequentially
182
+ for (const wave of waves) {
183
+ const wavePromises = wave.map(async (id) => {
184
+ const subReq = requestMap.get(id)!;
185
+ const start = Date.now();
186
+
187
+ // Check if dependency failed
188
+ if (subReq.dependsOn) {
189
+ const depResponse = responses.get(subReq.dependsOn);
190
+ if (depResponse && depResponse.status >= 400) {
191
+ const result: BatchResponse = {
192
+ status: 424,
193
+ body: { error: `Dependency "${subReq.dependsOn}" failed with status ${depResponse.status}` },
194
+ };
195
+ responses.set(id, result);
196
+ timings[id] = Date.now() - start;
197
+ return;
198
+ }
199
+ }
200
+
201
+ // Resolve templates in path
202
+ let resolvedPath: string;
203
+ try {
204
+ resolvedPath = resolveTemplates(subReq.path, responses);
205
+ } catch (err) {
206
+ const result: BatchResponse = {
207
+ status: 422,
208
+ body: { error: `Template resolution failed: ${(err as Error).message}` },
209
+ };
210
+ responses.set(id, result);
211
+ timings[id] = Date.now() - start;
212
+ return;
213
+ }
214
+
215
+ // Resolve templates in body
216
+ let resolvedBody: Record<string, unknown> | undefined;
217
+ if (subReq.body) {
218
+ try {
219
+ resolvedBody = resolveBodyTemplates(subReq.body, responses) as Record<string, unknown>;
220
+ } catch (err) {
221
+ const result: BatchResponse = {
222
+ status: 422,
223
+ body: { error: `Template resolution failed in body: ${(err as Error).message}` },
224
+ };
225
+ responses.set(id, result);
226
+ timings[id] = Date.now() - start;
227
+ return;
228
+ }
229
+ }
230
+
231
+ // Dispatch internally
232
+ const result = await dispatchInternal(
233
+ req.app,
234
+ subReq.method.toUpperCase(),
235
+ resolvedPath,
236
+ resolvedBody,
237
+ req,
238
+ );
239
+ responses.set(id, result);
240
+ timings[id] = Date.now() - start;
241
+ });
242
+
243
+ await Promise.all(wavePromises);
244
+ }
245
+
246
+ // Build response
247
+ const responseObj: Record<string, { status: number; body: unknown }> = {};
248
+ let succeeded = 0;
249
+ let failed = 0;
250
+ for (const [id, resp] of responses) {
251
+ responseObj[id] = { status: resp.status, body: resp.body };
252
+ if (resp.status < 400) {
253
+ succeeded++;
254
+ } else {
255
+ failed++;
256
+ }
257
+ }
258
+
259
+ res.json({
260
+ responses: responseObj,
261
+ meta: {
262
+ total: responses.size,
263
+ succeeded,
264
+ failed,
265
+ timings,
266
+ },
267
+ });
268
+ });
269
+
270
+ export default router;