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,54 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { lock, isUnlocked, lockVault, isVaultUnlocked } from '../lib/cold';
|
|
3
|
+
import { requireWalletAuth } from '../middleware/auth';
|
|
4
|
+
import { requireAdmin } from '../lib/permissions';
|
|
5
|
+
import { logger } from '../lib/logger';
|
|
6
|
+
import { revokeAllTokens } from '../lib/sessions';
|
|
7
|
+
import { revokeAdminTokens } from '../lib/auth';
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
// POST /lock - Lock all vaults (clear memory)
|
|
12
|
+
// Requires admin authentication
|
|
13
|
+
// Also revokes all active sessions so clients must re-authenticate.
|
|
14
|
+
router.post('/', requireWalletAuth, requireAdmin, async (_req: Request, res: Response) => {
|
|
15
|
+
const wasUnlocked = isUnlocked();
|
|
16
|
+
|
|
17
|
+
// Lock all vaults
|
|
18
|
+
lock();
|
|
19
|
+
|
|
20
|
+
// Revoke all active admin + agent tokens
|
|
21
|
+
revokeAdminTokens();
|
|
22
|
+
await revokeAllTokens();
|
|
23
|
+
|
|
24
|
+
// Log the lock event
|
|
25
|
+
if (wasUnlocked) {
|
|
26
|
+
logger.locked();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.json({
|
|
30
|
+
success: true,
|
|
31
|
+
message: wasUnlocked
|
|
32
|
+
? 'All vaults locked and active sessions revoked'
|
|
33
|
+
: 'Vaults were already locked; active sessions revoked'
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// POST /lock/:vaultId - Lock a specific vault
|
|
38
|
+
router.post('/:vaultId', requireWalletAuth, requireAdmin, (req: Request<{ vaultId: string }>, res: Response) => {
|
|
39
|
+
const { vaultId } = req.params;
|
|
40
|
+
const wasUnlocked = isVaultUnlocked(vaultId);
|
|
41
|
+
|
|
42
|
+
lockVault(vaultId);
|
|
43
|
+
|
|
44
|
+
if (wasUnlocked) {
|
|
45
|
+
logger.locked();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
res.json({
|
|
49
|
+
success: true,
|
|
50
|
+
message: wasUnlocked ? `Vault ${vaultId} locked` : `Vault ${vaultId} was already locked`
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export default router;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { prisma } from '../lib/db';
|
|
3
|
+
import { getErrorMessage } from '../lib/error';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
// GET /logs - Event logs - fetch historical events from database
|
|
8
|
+
// Supports filtering by: type, category (prefix match), agentId, since/until (timestamps), path
|
|
9
|
+
router.get('/', async (req: Request, res: Response) => {
|
|
10
|
+
try {
|
|
11
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 50, 250);
|
|
12
|
+
const offset = parseInt(req.query.offset as string) || 0;
|
|
13
|
+
const type = req.query.type as string | undefined;
|
|
14
|
+
const category = req.query.category as string | undefined;
|
|
15
|
+
const agentId = req.query.agentId as string | undefined;
|
|
16
|
+
const since = req.query.since as string | undefined;
|
|
17
|
+
const until = req.query.until as string | undefined;
|
|
18
|
+
|
|
19
|
+
const where: Record<string, unknown> = {};
|
|
20
|
+
|
|
21
|
+
if (type) {
|
|
22
|
+
where.type = type;
|
|
23
|
+
} else if (category) {
|
|
24
|
+
// Prefix match: category=auth matches auth:unlocked, auth:auth_failed, etc.
|
|
25
|
+
where.type = { startsWith: `${category}:` };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Filter by agentId in the JSON data field
|
|
29
|
+
if (agentId) {
|
|
30
|
+
where.data = { contains: `"agentId":"${agentId}"` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Date range filtering
|
|
34
|
+
if (since || until) {
|
|
35
|
+
const timestampFilter: Record<string, Date> = {};
|
|
36
|
+
if (since) timestampFilter.gte = new Date(parseInt(since) || since);
|
|
37
|
+
if (until) timestampFilter.lte = new Date(parseInt(until) || until);
|
|
38
|
+
where.timestamp = timestampFilter;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [logs, total] = await Promise.all([
|
|
42
|
+
prisma.event.findMany({
|
|
43
|
+
where,
|
|
44
|
+
orderBy: { timestamp: 'desc' },
|
|
45
|
+
take: limit,
|
|
46
|
+
skip: offset,
|
|
47
|
+
}),
|
|
48
|
+
prisma.event.count({ where }),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
res.json({
|
|
52
|
+
success: true,
|
|
53
|
+
logs,
|
|
54
|
+
count: logs.length,
|
|
55
|
+
total,
|
|
56
|
+
pagination: {
|
|
57
|
+
limit,
|
|
58
|
+
offset,
|
|
59
|
+
hasMore: offset + logs.length < total,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const message = getErrorMessage(error);
|
|
64
|
+
res.status(500).json({ success: false, error: message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export default router;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { deleteColdWallet, importColdWallet, listVaults, deleteVault } from '../lib/cold';
|
|
4
|
+
import { DATA_PATHS } from '../lib/config';
|
|
5
|
+
import { prisma } from '../lib/db';
|
|
6
|
+
import { lockAllCredentialVaults } from '../lib/credential-vault';
|
|
7
|
+
import { requireWalletAuth } from '../middleware/auth';
|
|
8
|
+
import { requireAdmin } from '../lib/permissions';
|
|
9
|
+
import { parseEncryptedPassword } from '../lib/transport';
|
|
10
|
+
import { logger } from '../lib/logger';
|
|
11
|
+
import { getErrorMessage, HttpError } from '../lib/error';
|
|
12
|
+
|
|
13
|
+
const router = Router();
|
|
14
|
+
|
|
15
|
+
// POST /nuke - Delete all wallet data (requires admin)
|
|
16
|
+
router.post('/', requireWalletAuth, requireAdmin, async (_req: Request, res: Response) => {
|
|
17
|
+
try {
|
|
18
|
+
// 1. Delete all vaults (files + memory)
|
|
19
|
+
try {
|
|
20
|
+
const vaults = listVaults();
|
|
21
|
+
for (const vault of vaults) {
|
|
22
|
+
deleteVault(vault.id);
|
|
23
|
+
}
|
|
24
|
+
deleteColdWallet(); // Also handles legacy cold.json
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.log('No cold wallet to delete:', e);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Delete all hot wallet files
|
|
30
|
+
const hotDir = DATA_PATHS.hotWallets;
|
|
31
|
+
if (fs.existsSync(hotDir)) {
|
|
32
|
+
fs.rmSync(hotDir, { recursive: true, force: true });
|
|
33
|
+
fs.mkdirSync(hotDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Delete pending requests directory
|
|
37
|
+
const pendingDir = DATA_PATHS.pending;
|
|
38
|
+
if (fs.existsSync(pendingDir)) {
|
|
39
|
+
fs.rmSync(pendingDir, { recursive: true, force: true });
|
|
40
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 4. Delete credential files directory and clear in-memory credential sessions
|
|
44
|
+
const credentialsDir = DATA_PATHS.credentials;
|
|
45
|
+
if (fs.existsSync(credentialsDir)) {
|
|
46
|
+
fs.rmSync(credentialsDir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(credentialsDir, { recursive: true });
|
|
49
|
+
lockAllCredentialVaults();
|
|
50
|
+
|
|
51
|
+
// 5. Clear database tables
|
|
52
|
+
try {
|
|
53
|
+
await prisma.humanAction.deleteMany({});
|
|
54
|
+
await prisma.agentToken.deleteMany({});
|
|
55
|
+
await prisma.hotWallet.deleteMany({});
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.log('Error clearing database:', e);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.nuke();
|
|
61
|
+
|
|
62
|
+
res.json({
|
|
63
|
+
success: true,
|
|
64
|
+
message: 'All data nuked. Ready for fresh setup.'
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('[NUKE ERROR]', error);
|
|
68
|
+
res.status(500).json({
|
|
69
|
+
success: false,
|
|
70
|
+
error: getErrorMessage(error)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// POST /nuke/import - Import wallet from seed phrase (requires admin)
|
|
76
|
+
router.post('/import', requireWalletAuth, requireAdmin, (req: Request, res: Response) => {
|
|
77
|
+
try {
|
|
78
|
+
const { mnemonic, password, encrypted } = req.body;
|
|
79
|
+
|
|
80
|
+
if (!mnemonic || typeof mnemonic !== 'string') {
|
|
81
|
+
res.status(400).json({ error: 'Seed phrase is required' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Support encrypted password transport (dashboard) or plaintext (CLI)
|
|
86
|
+
let plainPassword: string;
|
|
87
|
+
if (encrypted && typeof encrypted === 'string') {
|
|
88
|
+
plainPassword = parseEncryptedPassword(encrypted);
|
|
89
|
+
} else if (password && typeof password === 'string') {
|
|
90
|
+
if (password.length < 8) {
|
|
91
|
+
throw new HttpError(400, 'Password must be at least 8 characters');
|
|
92
|
+
}
|
|
93
|
+
plainPassword = password;
|
|
94
|
+
} else {
|
|
95
|
+
throw new HttpError(400, 'Password is required (encrypted or plaintext)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = importColdWallet(mnemonic, plainPassword);
|
|
99
|
+
|
|
100
|
+
res.json({
|
|
101
|
+
success: true,
|
|
102
|
+
address: result.address,
|
|
103
|
+
message: 'Wallet imported successfully'
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof HttpError) { res.status(error.status).json({ error: error.message }); return; }
|
|
107
|
+
res.status(400).json({ error: getErrorMessage(error) });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export default router;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Credential Routes — WebAuthn software authenticator endpoints
|
|
3
|
+
* =====================================================================
|
|
4
|
+
*
|
|
5
|
+
* POST /credentials/passkey/register — Generate keypair, store, return attestation
|
|
6
|
+
* POST /credentials/passkey/authenticate — Sign challenge, return assertion
|
|
7
|
+
* GET /credentials/passkey/match — Find passkeys matching rpId
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Router, Request, Response } from 'express';
|
|
11
|
+
import { requireWalletAuth } from '../middleware/auth';
|
|
12
|
+
import { registerPasskey, authenticatePasskey, matchPasskeys, PasskeyCredentialValidationError } from '../lib/passkey-credential';
|
|
13
|
+
import { getErrorMessage } from '../lib/error';
|
|
14
|
+
import { log } from '../lib/pino';
|
|
15
|
+
|
|
16
|
+
const router = Router();
|
|
17
|
+
router.use(requireWalletAuth);
|
|
18
|
+
|
|
19
|
+
// POST /credentials/passkey/register
|
|
20
|
+
router.post('/register', (req: Request, res: Response) => {
|
|
21
|
+
try {
|
|
22
|
+
const { vaultId, rpId, rpName, userName, displayName, userHandle, challenge, origin, clientDataJSON } = req.body;
|
|
23
|
+
|
|
24
|
+
if (!vaultId || !rpId || !userHandle || !clientDataJSON) {
|
|
25
|
+
res.status(400).json({ error: 'vaultId, rpId, userHandle, and clientDataJSON are required' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = registerPasskey({
|
|
30
|
+
vaultId,
|
|
31
|
+
rpId,
|
|
32
|
+
rpName,
|
|
33
|
+
userName,
|
|
34
|
+
displayName,
|
|
35
|
+
userHandle,
|
|
36
|
+
challenge,
|
|
37
|
+
origin,
|
|
38
|
+
clientDataJSON,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
log.info('Passkey registered', { rpId, credentialId: result.credentialId });
|
|
42
|
+
res.json(result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const message = getErrorMessage(error);
|
|
45
|
+
if (error instanceof PasskeyCredentialValidationError) {
|
|
46
|
+
res.status(400).json({ error: message });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
log.error('Passkey register error', { error: message });
|
|
51
|
+
res.status(500).json({ error: message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// POST /credentials/passkey/authenticate
|
|
56
|
+
router.post('/authenticate', (req: Request, res: Response) => {
|
|
57
|
+
try {
|
|
58
|
+
const { auraCredentialId, rpId, challenge, origin, clientDataJSON } = req.body;
|
|
59
|
+
|
|
60
|
+
if (!auraCredentialId || !rpId || !challenge || !clientDataJSON) {
|
|
61
|
+
res.status(400).json({ error: 'auraCredentialId, rpId, challenge, and clientDataJSON are required' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = authenticatePasskey({ auraCredentialId, rpId, challenge, origin, clientDataJSON });
|
|
66
|
+
|
|
67
|
+
log.info('Passkey authenticated', { rpId, credentialId: result.credentialId });
|
|
68
|
+
res.json(result);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const message = getErrorMessage(error);
|
|
71
|
+
if (error instanceof PasskeyCredentialValidationError) {
|
|
72
|
+
res.status(400).json({ error: message });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
log.error('Passkey authenticate error', { error: message });
|
|
77
|
+
res.status(500).json({ error: message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// GET /credentials/passkey/match?rpId=xxx
|
|
82
|
+
router.get('/match', (req: Request, res: Response) => {
|
|
83
|
+
try {
|
|
84
|
+
const rpId = req.query.rpId as string;
|
|
85
|
+
const vaultId = req.query.vaultId as string | undefined;
|
|
86
|
+
|
|
87
|
+
if (!rpId) {
|
|
88
|
+
res.status(400).json({ error: 'rpId query parameter is required' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const matches = matchPasskeys(rpId, vaultId);
|
|
93
|
+
res.json({ matches });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export default router;
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import {
|
|
3
|
+
generateRegistrationOptions,
|
|
4
|
+
verifyRegistrationResponse,
|
|
5
|
+
generateAuthenticationOptions,
|
|
6
|
+
verifyAuthenticationResponse,
|
|
7
|
+
} from '@simplewebauthn/server';
|
|
8
|
+
import { prisma } from '../lib/db';
|
|
9
|
+
import { requireWalletAuth } from '../middleware/auth';
|
|
10
|
+
import { requireAdmin } from '../lib/permissions';
|
|
11
|
+
import { isUnlocked } from '../lib/cold';
|
|
12
|
+
import { createAdminToken } from '../lib/auth';
|
|
13
|
+
import { storeChallenge, consumeChallenge, uint8ArrayToBase64url, base64urlToUint8Array } from '../lib/passkey';
|
|
14
|
+
import { isValidAgentPubkey, normalizeAgentPubkey } from '../lib/credential-transport';
|
|
15
|
+
import { log } from '../lib/pino';
|
|
16
|
+
import { getErrorMessage } from '../lib/error';
|
|
17
|
+
|
|
18
|
+
const router = Router();
|
|
19
|
+
|
|
20
|
+
// Helper: extract rpId from request
|
|
21
|
+
function getRpId(req: Request, bodyRpId?: string): string {
|
|
22
|
+
return bodyRpId || req.hostname || 'localhost';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Status ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
// GET /auth/passkey/status — public, returns whether passkeys registered for origin
|
|
28
|
+
router.get('/status', async (req: Request, res: Response) => {
|
|
29
|
+
try {
|
|
30
|
+
const rpId = getRpId(req, req.query.rpId as string | undefined);
|
|
31
|
+
const passkeys = await prisma.passkey.findMany({
|
|
32
|
+
where: { rpId },
|
|
33
|
+
select: { credentialId: true, createdAt: true },
|
|
34
|
+
});
|
|
35
|
+
res.json({
|
|
36
|
+
registered: passkeys.length > 0,
|
|
37
|
+
count: passkeys.length,
|
|
38
|
+
rpId,
|
|
39
|
+
credentials: passkeys.map(p => ({ id: p.credentialId, createdAt: p.createdAt })),
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ─── Registration ───────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
// POST /auth/passkey/register/options — requires admin token
|
|
49
|
+
router.post('/register/options', requireWalletAuth, requireAdmin, async (req: Request, res: Response) => {
|
|
50
|
+
try {
|
|
51
|
+
if (!isUnlocked()) {
|
|
52
|
+
res.status(400).json({ error: 'Vault must be unlocked to register passkey' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rpId = getRpId(req, req.body?.rpId);
|
|
57
|
+
|
|
58
|
+
// Get existing credentials for this rpId to exclude
|
|
59
|
+
const existing = await prisma.passkey.findMany({
|
|
60
|
+
where: { rpId },
|
|
61
|
+
select: { credentialId: true, transports: true },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const excludeCredentials = existing.map((p) => ({
|
|
65
|
+
id: p.credentialId,
|
|
66
|
+
transports: JSON.parse(p.transports || '[]'),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const options = await generateRegistrationOptions({
|
|
70
|
+
rpName: 'AuraWallet',
|
|
71
|
+
rpID: rpId,
|
|
72
|
+
userName: 'owner',
|
|
73
|
+
userDisplayName: 'Vault Owner',
|
|
74
|
+
attestationType: 'none',
|
|
75
|
+
authenticatorSelection: {
|
|
76
|
+
authenticatorAttachment: 'platform',
|
|
77
|
+
userVerification: 'required',
|
|
78
|
+
residentKey: 'preferred',
|
|
79
|
+
},
|
|
80
|
+
excludeCredentials,
|
|
81
|
+
timeout: 60000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Store challenge
|
|
85
|
+
storeChallenge(options.challenge, 'register');
|
|
86
|
+
|
|
87
|
+
res.json(options);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// POST /auth/passkey/register/verify — requires admin token
|
|
94
|
+
router.post('/register/verify', requireWalletAuth, requireAdmin, async (req: Request, res: Response) => {
|
|
95
|
+
try {
|
|
96
|
+
if (!isUnlocked()) {
|
|
97
|
+
res.status(400).json({ error: 'Vault must be unlocked to register passkey' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { credential } = req.body;
|
|
102
|
+
if (!credential) {
|
|
103
|
+
res.status(400).json({ error: 'credential is required' });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rpId = getRpId(req, req.body?.rpId);
|
|
108
|
+
const origin = `${req.protocol}://${req.get('host')}`;
|
|
109
|
+
|
|
110
|
+
// Consume challenge
|
|
111
|
+
const challenge = credential.response?.clientDataJSON
|
|
112
|
+
? JSON.parse(Buffer.from(credential.response.clientDataJSON, 'base64url').toString()).challenge
|
|
113
|
+
: undefined;
|
|
114
|
+
|
|
115
|
+
if (!challenge || !consumeChallenge(challenge, 'register')) {
|
|
116
|
+
res.status(400).json({ error: 'Invalid or expired challenge' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const verification = await verifyRegistrationResponse({
|
|
121
|
+
response: credential,
|
|
122
|
+
expectedChallenge: challenge,
|
|
123
|
+
expectedOrigin: origin,
|
|
124
|
+
expectedRPID: rpId,
|
|
125
|
+
requireUserVerification: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
129
|
+
res.status(400).json({ error: 'Registration verification failed' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { credential: cred, credentialBackedUp, aaguid } = verification.registrationInfo;
|
|
134
|
+
|
|
135
|
+
// Store in DB
|
|
136
|
+
await prisma.passkey.create({
|
|
137
|
+
data: {
|
|
138
|
+
credentialId: cred.id,
|
|
139
|
+
publicKey: Buffer.from(cred.publicKey),
|
|
140
|
+
counter: cred.counter,
|
|
141
|
+
transports: JSON.stringify(credential.response.transports || []),
|
|
142
|
+
rpId,
|
|
143
|
+
aaguid: aaguid || '',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
res.json({
|
|
148
|
+
success: true,
|
|
149
|
+
credentialId: cred.id,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ─── Authentication ─────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
// POST /auth/passkey/authenticate/options — public (vault must be unlocked server-side)
|
|
159
|
+
router.post('/authenticate/options', async (req: Request, res: Response) => {
|
|
160
|
+
try {
|
|
161
|
+
// Specific vault_locked error per audit
|
|
162
|
+
if (!isUnlocked()) {
|
|
163
|
+
res.status(400).json({
|
|
164
|
+
error: 'vault_locked',
|
|
165
|
+
message: 'Password required after server restart',
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rpId = getRpId(req, req.body?.rpId);
|
|
171
|
+
|
|
172
|
+
// Get credentials for this rpId — reject if none exist (audit item)
|
|
173
|
+
const passkeys = await prisma.passkey.findMany({
|
|
174
|
+
where: { rpId },
|
|
175
|
+
select: { credentialId: true, transports: true },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (passkeys.length === 0) {
|
|
179
|
+
res.status(400).json({ error: 'No passkeys registered for this origin' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allowCredentials = passkeys.map((p) => ({
|
|
184
|
+
id: p.credentialId,
|
|
185
|
+
transports: JSON.parse(p.transports || '[]'),
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
const options = await generateAuthenticationOptions({
|
|
189
|
+
rpID: rpId,
|
|
190
|
+
allowCredentials,
|
|
191
|
+
userVerification: 'required',
|
|
192
|
+
timeout: 60000,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
storeChallenge(options.challenge, 'authenticate');
|
|
196
|
+
|
|
197
|
+
res.json(options);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// POST /auth/passkey/authenticate/verify — public (assertion IS the auth)
|
|
204
|
+
router.post('/authenticate/verify', async (req: Request, res: Response) => {
|
|
205
|
+
try {
|
|
206
|
+
// Specific vault_locked error per audit
|
|
207
|
+
if (!isUnlocked()) {
|
|
208
|
+
res.status(400).json({
|
|
209
|
+
error: 'vault_locked',
|
|
210
|
+
message: 'Password required after server restart',
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { credential, pubkey } = req.body;
|
|
216
|
+
if (!credential) {
|
|
217
|
+
res.status(400).json({ error: 'credential is required' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!pubkey || typeof pubkey !== 'string') {
|
|
221
|
+
res.status(400).json({ error: 'pubkey is required' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!isValidAgentPubkey(pubkey)) {
|
|
225
|
+
res.status(400).json({ error: 'pubkey must be a valid RSA public key' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const rpId = getRpId(req, req.body?.rpId);
|
|
230
|
+
const origin = `${req.protocol}://${req.get('host')}`;
|
|
231
|
+
|
|
232
|
+
// Extract and consume challenge
|
|
233
|
+
const challenge = credential.response?.clientDataJSON
|
|
234
|
+
? JSON.parse(Buffer.from(credential.response.clientDataJSON, 'base64url').toString()).challenge
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
if (!challenge || !consumeChallenge(challenge, 'authenticate')) {
|
|
238
|
+
log.warn('Passkey auth: invalid or expired challenge', { ip: req.ip, origin });
|
|
239
|
+
res.status(401).json({ error: 'Invalid or expired challenge' });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Look up the credential
|
|
244
|
+
const credentialId = credential.id || credential.rawId;
|
|
245
|
+
const passkey = await prisma.passkey.findUnique({
|
|
246
|
+
where: { credentialId },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!passkey) {
|
|
250
|
+
log.warn('Passkey auth: unknown credential', { credentialId, ip: req.ip, origin });
|
|
251
|
+
res.status(401).json({ error: 'Unknown credential' });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let verification;
|
|
256
|
+
try {
|
|
257
|
+
verification = await verifyAuthenticationResponse({
|
|
258
|
+
response: credential,
|
|
259
|
+
expectedChallenge: challenge,
|
|
260
|
+
expectedOrigin: origin,
|
|
261
|
+
expectedRPID: rpId,
|
|
262
|
+
credential: {
|
|
263
|
+
id: passkey.credentialId,
|
|
264
|
+
publicKey: new Uint8Array(passkey.publicKey),
|
|
265
|
+
counter: passkey.counter,
|
|
266
|
+
transports: JSON.parse(passkey.transports || '[]'),
|
|
267
|
+
},
|
|
268
|
+
requireUserVerification: true,
|
|
269
|
+
});
|
|
270
|
+
} catch (err) {
|
|
271
|
+
// Log failed attempt (audit item)
|
|
272
|
+
log.warn('Passkey auth: verification failed', {
|
|
273
|
+
credentialId,
|
|
274
|
+
ip: req.ip,
|
|
275
|
+
origin,
|
|
276
|
+
error: getErrorMessage(err),
|
|
277
|
+
});
|
|
278
|
+
res.status(401).json({ error: 'Authentication verification failed' });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!verification.verified) {
|
|
283
|
+
log.warn('Passkey auth: assertion not verified', { credentialId, ip: req.ip, origin });
|
|
284
|
+
res.status(401).json({ error: 'Authentication verification failed' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Counter validation (clone detection) — @simplewebauthn handles this,
|
|
289
|
+
// but we double-check per audit
|
|
290
|
+
const newCounter = verification.authenticationInfo.newCounter;
|
|
291
|
+
if (newCounter > 0 && newCounter <= passkey.counter) {
|
|
292
|
+
log.warn('Passkey auth: counter regression (possible clone)', {
|
|
293
|
+
credentialId,
|
|
294
|
+
storedCounter: passkey.counter,
|
|
295
|
+
newCounter,
|
|
296
|
+
ip: req.ip,
|
|
297
|
+
origin,
|
|
298
|
+
});
|
|
299
|
+
res.status(401).json({ error: 'Credential counter regression detected' });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Update counter and lastUsedAt
|
|
304
|
+
await prisma.passkey.update({
|
|
305
|
+
where: { credentialId: passkey.credentialId },
|
|
306
|
+
data: {
|
|
307
|
+
counter: newCounter,
|
|
308
|
+
lastUsedAt: new Date(),
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Issue admin token (same as rekey flow)
|
|
313
|
+
const normalizedPubkey = normalizeAgentPubkey(pubkey);
|
|
314
|
+
const token = await createAdminToken(normalizedPubkey);
|
|
315
|
+
|
|
316
|
+
res.json({
|
|
317
|
+
success: true,
|
|
318
|
+
token,
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ─── Delete ─────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
// DELETE /auth/passkey/:credentialId — requires admin token
|
|
328
|
+
router.delete('/:credentialId', requireWalletAuth, requireAdmin, async (req: Request<{ credentialId: string }>, res: Response) => {
|
|
329
|
+
try {
|
|
330
|
+
const { credentialId } = req.params;
|
|
331
|
+
|
|
332
|
+
const passkey = await prisma.passkey.findUnique({ where: { credentialId } });
|
|
333
|
+
if (!passkey) {
|
|
334
|
+
res.status(404).json({ error: 'Passkey not found' });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await prisma.passkey.delete({ where: { credentialId } });
|
|
339
|
+
|
|
340
|
+
res.json({ success: true });
|
|
341
|
+
} catch (error) {
|
|
342
|
+
res.status(500).json({ error: getErrorMessage(error) });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
export default router;
|