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,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;
|