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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Credential Operations — Software WebAuthn Authenticator
|
|
3
|
+
* ===============================================================
|
|
4
|
+
*
|
|
5
|
+
* Generates ECDSA P-256 keypairs, builds WebAuthn attestation/assertion objects,
|
|
6
|
+
* and stores passkey credentials in the encrypted vault.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import { encode as cborEncode } from 'cbor-x';
|
|
11
|
+
import {
|
|
12
|
+
createCredential,
|
|
13
|
+
listCredentials,
|
|
14
|
+
getCredential,
|
|
15
|
+
readCredentialSecrets,
|
|
16
|
+
updateCredential,
|
|
17
|
+
} from './credentials';
|
|
18
|
+
import { CredentialField } from '../types';
|
|
19
|
+
|
|
20
|
+
// Our software authenticator AAGUID (random, unique to Aura)
|
|
21
|
+
const AURA_AAGUID = Buffer.from('a0a1a2a3a4a5a6a7a8a9aaabacadaeaf', 'hex');
|
|
22
|
+
|
|
23
|
+
const PASSKEY_CHALLENGE_TTL_MS = 120_000;
|
|
24
|
+
const usedChallenges = new Map<string, number>();
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export class PasskeyCredentialValidationError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'PasskeyCredentialValidationError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function base64urlEncode(buf: Buffer | Uint8Array): string {
|
|
38
|
+
return Buffer.from(buf).toString('base64url');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function base64urlDecode(str: string): Buffer {
|
|
42
|
+
return Buffer.from(str, 'base64url');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sha256(data: Buffer | string): Buffer {
|
|
46
|
+
return crypto.createHash('sha256').update(data).digest();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cleanupUsedChallenges(now = Date.now()): void {
|
|
50
|
+
for (const [challenge, expiresAt] of usedChallenges.entries()) {
|
|
51
|
+
if (expiresAt <= now) {
|
|
52
|
+
usedChallenges.delete(challenge);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function consumeFreshChallenge(challenge: string): void {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
cleanupUsedChallenges(now);
|
|
60
|
+
|
|
61
|
+
const key = challenge.trim();
|
|
62
|
+
if (!key) {
|
|
63
|
+
throw new PasskeyCredentialValidationError('challenge is required');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (usedChallenges.has(key)) {
|
|
67
|
+
throw new PasskeyCredentialValidationError('challenge replay detected');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
usedChallenges.set(key, now + PASSKEY_CHALLENGE_TTL_MS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function _resetPasskeyCredentialChallengeStoreForTests(): void {
|
|
74
|
+
usedChallenges.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateOriginPolicy(origin: string, rpId: string): void {
|
|
78
|
+
let parsed: URL;
|
|
79
|
+
try {
|
|
80
|
+
parsed = new URL(origin);
|
|
81
|
+
} catch {
|
|
82
|
+
throw new PasskeyCredentialValidationError('clientDataJSON.origin must be a valid URL');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const host = parsed.hostname.toLowerCase();
|
|
86
|
+
const normalizedRpId = rpId.toLowerCase();
|
|
87
|
+
const isRpMatch = host === normalizedRpId || host.endsWith(`.${normalizedRpId}`);
|
|
88
|
+
if (!isRpMatch) {
|
|
89
|
+
throw new PasskeyCredentialValidationError('clientDataJSON.origin does not match rpId');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isLocalhost = host === 'localhost' || host.endsWith('.localhost');
|
|
93
|
+
const allowedHttp = parsed.protocol === 'http:' && isLocalhost;
|
|
94
|
+
if (parsed.protocol !== 'https:' && !allowedHttp) {
|
|
95
|
+
throw new PasskeyCredentialValidationError('clientDataJSON.origin must be https (except localhost)');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseAndValidateClientDataJSON(params: {
|
|
100
|
+
clientDataJSON: string;
|
|
101
|
+
expectedType: 'webauthn.create' | 'webauthn.get';
|
|
102
|
+
expectedChallenge: string;
|
|
103
|
+
rpId: string;
|
|
104
|
+
expectedOrigin?: string;
|
|
105
|
+
}): { challenge: string; origin: string } {
|
|
106
|
+
let parsed: Record<string, unknown>;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const raw = base64urlDecode(params.clientDataJSON).toString('utf8');
|
|
110
|
+
parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
111
|
+
} catch {
|
|
112
|
+
throw new PasskeyCredentialValidationError('clientDataJSON must be valid base64url-encoded JSON');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const type = parsed.type;
|
|
116
|
+
const challenge = parsed.challenge;
|
|
117
|
+
const origin = parsed.origin;
|
|
118
|
+
|
|
119
|
+
if (type !== params.expectedType) {
|
|
120
|
+
throw new PasskeyCredentialValidationError(`clientDataJSON.type must be ${params.expectedType}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof challenge !== 'string' || challenge !== params.expectedChallenge) {
|
|
124
|
+
throw new PasskeyCredentialValidationError('clientDataJSON.challenge mismatch');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof origin !== 'string' || !origin.trim()) {
|
|
128
|
+
throw new PasskeyCredentialValidationError('clientDataJSON.origin is required');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
validateOriginPolicy(origin, params.rpId);
|
|
132
|
+
|
|
133
|
+
if (params.expectedOrigin && origin !== params.expectedOrigin) {
|
|
134
|
+
throw new PasskeyCredentialValidationError('clientDataJSON.origin mismatch');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { challenge, origin };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Encode an EC public key in COSE_Key format (ECDSA P-256).
|
|
142
|
+
* See RFC 8152 Section 13.1.1
|
|
143
|
+
*/
|
|
144
|
+
function encodeCosePublicKey(publicKeyDer: Buffer): Buffer {
|
|
145
|
+
const raw = crypto.createPublicKey({ key: publicKeyDer, format: 'der', type: 'spki' })
|
|
146
|
+
.export({ format: 'jwk' });
|
|
147
|
+
|
|
148
|
+
const x = base64urlDecode(raw.x!);
|
|
149
|
+
const y = base64urlDecode(raw.y!);
|
|
150
|
+
|
|
151
|
+
const coseKey = new Map<number, number | Buffer>();
|
|
152
|
+
coseKey.set(1, 2); // kty: EC2
|
|
153
|
+
coseKey.set(3, -7); // alg: ES256
|
|
154
|
+
coseKey.set(-1, 1); // crv: P-256
|
|
155
|
+
coseKey.set(-2, x); // x coordinate
|
|
156
|
+
coseKey.set(-3, y); // y coordinate
|
|
157
|
+
|
|
158
|
+
return Buffer.from(cborEncode(coseKey));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Registration (navigator.credentials.create)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
export interface PasskeyRegisterOptions {
|
|
166
|
+
vaultId: string;
|
|
167
|
+
rpId: string;
|
|
168
|
+
rpName?: string;
|
|
169
|
+
userName?: string;
|
|
170
|
+
displayName?: string;
|
|
171
|
+
userHandle: string; // base64url
|
|
172
|
+
challenge: string; // base64url — from clientDataJSON
|
|
173
|
+
origin: string;
|
|
174
|
+
clientDataJSON: string; // base64url — raw from browser
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface PasskeyRegisterResult {
|
|
178
|
+
credentialId: string; // base64url
|
|
179
|
+
attestationObject: string; // base64url
|
|
180
|
+
clientDataJSON: string; // base64url (pass-through)
|
|
181
|
+
publicKey: string; // base64url (SPKI DER)
|
|
182
|
+
publicKeyCose: string; // base64url (COSE)
|
|
183
|
+
transports: string[];
|
|
184
|
+
auraCredentialId: string; // internal cred-xxx id
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function registerPasskey(opts: PasskeyRegisterOptions): PasskeyRegisterResult {
|
|
188
|
+
parseAndValidateClientDataJSON({
|
|
189
|
+
clientDataJSON: opts.clientDataJSON,
|
|
190
|
+
expectedType: 'webauthn.create',
|
|
191
|
+
expectedChallenge: opts.challenge,
|
|
192
|
+
rpId: opts.rpId,
|
|
193
|
+
expectedOrigin: opts.origin,
|
|
194
|
+
});
|
|
195
|
+
consumeFreshChallenge(opts.challenge);
|
|
196
|
+
|
|
197
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
|
|
198
|
+
namedCurve: 'prime256v1',
|
|
199
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
200
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const credentialIdBuf = crypto.randomBytes(32);
|
|
204
|
+
const credentialId = base64urlEncode(credentialIdBuf);
|
|
205
|
+
|
|
206
|
+
const cosePublicKey = encodeCosePublicKey(publicKey as Buffer);
|
|
207
|
+
|
|
208
|
+
const rpIdHash = sha256(opts.rpId);
|
|
209
|
+
const flags = Buffer.from([0x45]); // UP + UV + AT
|
|
210
|
+
const signCount = Buffer.alloc(4);
|
|
211
|
+
|
|
212
|
+
const credIdLenBuf = Buffer.alloc(2);
|
|
213
|
+
credIdLenBuf.writeUInt16BE(credentialIdBuf.length);
|
|
214
|
+
|
|
215
|
+
const authData = Buffer.concat([
|
|
216
|
+
rpIdHash,
|
|
217
|
+
flags,
|
|
218
|
+
signCount,
|
|
219
|
+
AURA_AAGUID,
|
|
220
|
+
credIdLenBuf,
|
|
221
|
+
credentialIdBuf,
|
|
222
|
+
cosePublicKey,
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const attestationObject = cborEncode({
|
|
226
|
+
fmt: 'none',
|
|
227
|
+
attStmt: {},
|
|
228
|
+
authData,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const sensitiveFields: CredentialField[] = [
|
|
232
|
+
{ key: 'privateKey', value: base64urlEncode(privateKey as Buffer), type: 'secret', sensitive: true },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
const meta: Record<string, unknown> = {
|
|
236
|
+
rpId: opts.rpId,
|
|
237
|
+
rpName: opts.rpName || opts.rpId,
|
|
238
|
+
credentialId,
|
|
239
|
+
publicKey: base64urlEncode(publicKey as Buffer),
|
|
240
|
+
publicKeyCose: base64urlEncode(cosePublicKey),
|
|
241
|
+
userHandle: opts.userHandle,
|
|
242
|
+
userName: opts.userName || '',
|
|
243
|
+
displayName: opts.displayName || '',
|
|
244
|
+
signCount: 0,
|
|
245
|
+
transports: ['internal'],
|
|
246
|
+
discoverable: true,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const name = `${opts.rpId} — ${opts.displayName || opts.userName || 'passkey'}`;
|
|
250
|
+
const cred = createCredential(opts.vaultId, 'passkey', name, meta, sensitiveFields);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
credentialId,
|
|
254
|
+
attestationObject: base64urlEncode(Buffer.from(attestationObject)),
|
|
255
|
+
clientDataJSON: opts.clientDataJSON,
|
|
256
|
+
publicKey: base64urlEncode(publicKey as Buffer),
|
|
257
|
+
publicKeyCose: base64urlEncode(cosePublicKey),
|
|
258
|
+
transports: ['internal'],
|
|
259
|
+
auraCredentialId: cred.id,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Authentication (navigator.credentials.get)
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
export interface PasskeyAuthOptions {
|
|
268
|
+
auraCredentialId: string; // internal cred-xxx id
|
|
269
|
+
rpId: string;
|
|
270
|
+
challenge: string;
|
|
271
|
+
origin?: string;
|
|
272
|
+
clientDataJSON: string; // base64url — raw from browser
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface PasskeyAuthResult {
|
|
276
|
+
credentialId: string; // base64url
|
|
277
|
+
authenticatorData: string; // base64url
|
|
278
|
+
signature: string; // base64url
|
|
279
|
+
userHandle: string; // base64url
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function authenticatePasskey(opts: PasskeyAuthOptions): PasskeyAuthResult {
|
|
283
|
+
parseAndValidateClientDataJSON({
|
|
284
|
+
clientDataJSON: opts.clientDataJSON,
|
|
285
|
+
expectedType: 'webauthn.get',
|
|
286
|
+
expectedChallenge: opts.challenge,
|
|
287
|
+
rpId: opts.rpId,
|
|
288
|
+
expectedOrigin: opts.origin,
|
|
289
|
+
});
|
|
290
|
+
consumeFreshChallenge(opts.challenge);
|
|
291
|
+
|
|
292
|
+
const cred = getCredential(opts.auraCredentialId);
|
|
293
|
+
if (!cred || cred.type !== 'passkey') {
|
|
294
|
+
throw new Error('Passkey credential not found');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (cred.meta.rpId !== opts.rpId) {
|
|
298
|
+
throw new Error('rpId mismatch');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const secrets = readCredentialSecrets(opts.auraCredentialId);
|
|
302
|
+
const privateKeyField = secrets.find(f => f.key === 'privateKey');
|
|
303
|
+
if (!privateKeyField) {
|
|
304
|
+
throw new Error('Private key not found in credential');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const privateKeyDer = base64urlDecode(privateKeyField.value);
|
|
308
|
+
const privateKey = crypto.createPrivateKey({ key: privateKeyDer, format: 'der', type: 'pkcs8' });
|
|
309
|
+
|
|
310
|
+
const currentCount = (cred.meta.signCount as number) || 0;
|
|
311
|
+
const newCount = currentCount + 1;
|
|
312
|
+
|
|
313
|
+
const rpIdHash = sha256(opts.rpId);
|
|
314
|
+
const flags = Buffer.from([0x05]); // UP + UV
|
|
315
|
+
const signCountBuf = Buffer.alloc(4);
|
|
316
|
+
signCountBuf.writeUInt32BE(newCount);
|
|
317
|
+
|
|
318
|
+
const authenticatorData = Buffer.concat([rpIdHash, flags, signCountBuf]);
|
|
319
|
+
|
|
320
|
+
const clientDataHash = sha256(base64urlDecode(opts.clientDataJSON));
|
|
321
|
+
const signedData = Buffer.concat([authenticatorData, clientDataHash]);
|
|
322
|
+
const signature = crypto.sign('sha256', signedData, privateKey);
|
|
323
|
+
|
|
324
|
+
updateCredential(opts.auraCredentialId, {
|
|
325
|
+
meta: { ...cred.meta, signCount: newCount },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
credentialId: cred.meta.credentialId as string,
|
|
330
|
+
authenticatorData: base64urlEncode(authenticatorData),
|
|
331
|
+
signature: base64urlEncode(signature),
|
|
332
|
+
userHandle: (cred.meta.userHandle as string) || '',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Match — find passkeys for an rpId
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
export interface PasskeyMatch {
|
|
341
|
+
auraCredentialId: string;
|
|
342
|
+
credentialId: string;
|
|
343
|
+
rpId: string;
|
|
344
|
+
userName: string;
|
|
345
|
+
displayName: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function matchPasskeys(rpId: string, vaultId?: string): PasskeyMatch[] {
|
|
349
|
+
const creds = listCredentials({ type: 'passkey', vaultId });
|
|
350
|
+
return creds
|
|
351
|
+
.filter(c => c.meta.rpId === rpId)
|
|
352
|
+
.map(c => ({
|
|
353
|
+
auraCredentialId: c.id,
|
|
354
|
+
credentialId: c.meta.credentialId as string,
|
|
355
|
+
rpId: c.meta.rpId as string,
|
|
356
|
+
userName: (c.meta.userName as string) || '',
|
|
357
|
+
displayName: (c.meta.displayName as string) || '',
|
|
358
|
+
}))
|
|
359
|
+
.sort((a, b) => a.auraCredentialId.localeCompare(b.auraCredentialId));
|
|
360
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey challenge store and helpers for WebAuthn biometric unlock.
|
|
3
|
+
* Challenges are in-memory, single-use, with 60s TTL.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CHALLENGE_TTL_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
interface PendingChallenge {
|
|
9
|
+
type: 'register' | 'authenticate';
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const challenges = new Map<string, PendingChallenge>();
|
|
14
|
+
|
|
15
|
+
// Cleanup expired challenges periodically
|
|
16
|
+
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
17
|
+
|
|
18
|
+
function ensureCleanup() {
|
|
19
|
+
if (cleanupTimer) return;
|
|
20
|
+
cleanupTimer = setInterval(() => {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
for (const [key, val] of challenges) {
|
|
23
|
+
if (val.expiresAt <= now) challenges.delete(key);
|
|
24
|
+
}
|
|
25
|
+
if (challenges.size === 0 && cleanupTimer) {
|
|
26
|
+
clearInterval(cleanupTimer);
|
|
27
|
+
cleanupTimer = null;
|
|
28
|
+
}
|
|
29
|
+
}, 30_000);
|
|
30
|
+
// Don't keep process alive for cleanup
|
|
31
|
+
if (cleanupTimer && typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {
|
|
32
|
+
cleanupTimer.unref();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Store a challenge for later verification.
|
|
38
|
+
*/
|
|
39
|
+
export function storeChallenge(challenge: string, type: 'register' | 'authenticate'): void {
|
|
40
|
+
challenges.set(challenge, { type, expiresAt: Date.now() + CHALLENGE_TTL_MS });
|
|
41
|
+
ensureCleanup();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Consume a challenge (single-use). Returns true if valid and not expired.
|
|
46
|
+
*/
|
|
47
|
+
export function consumeChallenge(challenge: string, type: 'register' | 'authenticate'): boolean {
|
|
48
|
+
const entry = challenges.get(challenge);
|
|
49
|
+
if (!entry) return false;
|
|
50
|
+
challenges.delete(challenge);
|
|
51
|
+
if (entry.type !== type) return false;
|
|
52
|
+
if (entry.expiresAt <= Date.now()) return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert base64url string to Uint8Array.
|
|
58
|
+
*/
|
|
59
|
+
export function base64urlToUint8Array(base64url: string): Uint8Array {
|
|
60
|
+
return Buffer.from(base64url, 'base64url');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert Uint8Array (or Buffer) to base64url string.
|
|
65
|
+
*/
|
|
66
|
+
export function uint8ArrayToBase64url(bytes: Uint8Array | Buffer): string {
|
|
67
|
+
return Buffer.from(bytes).toString('base64url');
|
|
68
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { AgentTokenPayload } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PERMISSION SYSTEM
|
|
6
|
+
* =================
|
|
7
|
+
*
|
|
8
|
+
* Permission strings control access to specific routes and operations.
|
|
9
|
+
* Admin tokens bypass all permission checks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// All valid permission strings
|
|
13
|
+
export type Permission =
|
|
14
|
+
// Wallet operations
|
|
15
|
+
| 'wallet:list' // List/view wallets
|
|
16
|
+
| 'wallet:create:hot' // Create hot wallets
|
|
17
|
+
| 'wallet:create:temp' // Create temp wallets
|
|
18
|
+
| 'wallet:rename' // Rename wallets
|
|
19
|
+
| 'wallet:export' // Export private keys
|
|
20
|
+
| 'wallet:tx:add' // Manually add transactions to wallet history
|
|
21
|
+
| 'wallet:asset:add' // Add assets to track for a wallet
|
|
22
|
+
| 'wallet:asset:remove' // Remove tracked assets from a wallet
|
|
23
|
+
|
|
24
|
+
// Transaction operations
|
|
25
|
+
| 'send:hot' // Send from hot wallets
|
|
26
|
+
| 'send:temp' // Send from temp wallets
|
|
27
|
+
| 'swap' // Execute swaps
|
|
28
|
+
| 'fund' // Cold→hot transfers
|
|
29
|
+
| 'launch' // Execute token launches
|
|
30
|
+
|
|
31
|
+
// API Key operations
|
|
32
|
+
| 'apikey:get' // Read API keys
|
|
33
|
+
| 'apikey:set' // Create/update/delete API keys
|
|
34
|
+
|
|
35
|
+
// Workspace/UI operations
|
|
36
|
+
| 'workspace:modify' // Add/update/remove apps, modify workspaces
|
|
37
|
+
|
|
38
|
+
// Strategy operations
|
|
39
|
+
| 'strategy:read' // View strategies and their state
|
|
40
|
+
| 'strategy:manage' // Enable/disable strategies, update config
|
|
41
|
+
|
|
42
|
+
// App operations
|
|
43
|
+
| 'app:storage' // Read/write own app's storage via Express API
|
|
44
|
+
| 'app:storage:all' // Read/write ANY app's storage via Express API
|
|
45
|
+
| 'app:accesskey' // Read API keys from app storage
|
|
46
|
+
|
|
47
|
+
// Action operations
|
|
48
|
+
| 'action:create' // Create human action requests (propose actions for approval)
|
|
49
|
+
| 'action:read' // Read/list pending actions
|
|
50
|
+
| 'action:resolve' // Approve or reject pending actions
|
|
51
|
+
|
|
52
|
+
// Adapter operations
|
|
53
|
+
| 'adapter:manage' // Configure and restart approval adapters (Telegram, webhooks)
|
|
54
|
+
|
|
55
|
+
// Address book & bookmark operations
|
|
56
|
+
| 'addressbook:write' // Create/update/delete address labels
|
|
57
|
+
| 'bookmark:write' // Create/delete token bookmarks
|
|
58
|
+
|
|
59
|
+
// Credential vault operations
|
|
60
|
+
| 'secret:read' // Read credentials from the vault
|
|
61
|
+
| 'secret:write' // Create/update/delete credentials in the vault
|
|
62
|
+
| 'totp:read' // Generate TOTP codes from credential secrets
|
|
63
|
+
|
|
64
|
+
// Compound permissions (expand to multiple permissions)
|
|
65
|
+
| 'trade:all' // All trading permissions + apikey:get + strategy:read
|
|
66
|
+
| 'wallet:write' // All wallet write operations
|
|
67
|
+
| 'extension:*' // Browser extension permissions
|
|
68
|
+
|
|
69
|
+
// Admin (UI only)
|
|
70
|
+
| 'admin:*'; // All permissions, bypass limits
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compound permission mappings
|
|
74
|
+
* These permissions expand to multiple underlying permissions
|
|
75
|
+
*/
|
|
76
|
+
const COMPOUND_PERMISSIONS: Record<string, Permission[]> = {
|
|
77
|
+
'trade:all': [
|
|
78
|
+
'wallet:list',
|
|
79
|
+
'wallet:create:hot',
|
|
80
|
+
'wallet:create:temp',
|
|
81
|
+
'send:hot',
|
|
82
|
+
'send:temp',
|
|
83
|
+
'swap',
|
|
84
|
+
'fund',
|
|
85
|
+
'launch',
|
|
86
|
+
'apikey:get',
|
|
87
|
+
'strategy:read'
|
|
88
|
+
],
|
|
89
|
+
'extension:*': [
|
|
90
|
+
'wallet:list',
|
|
91
|
+
'secret:read',
|
|
92
|
+
'action:read',
|
|
93
|
+
'action:resolve'
|
|
94
|
+
],
|
|
95
|
+
'wallet:write': [
|
|
96
|
+
'wallet:create:hot',
|
|
97
|
+
'wallet:create:temp',
|
|
98
|
+
'wallet:rename',
|
|
99
|
+
'wallet:tx:add',
|
|
100
|
+
'wallet:asset:add',
|
|
101
|
+
'wallet:asset:remove'
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Expand and normalize permissions array
|
|
107
|
+
* - Expands compound permissions (e.g., 'trade:all' → multiple permissions)
|
|
108
|
+
* - Deduplicates the result
|
|
109
|
+
*/
|
|
110
|
+
export function expandPermissions(permissions: string[]): Permission[] {
|
|
111
|
+
const expanded = new Set<Permission>();
|
|
112
|
+
|
|
113
|
+
for (const perm of permissions) {
|
|
114
|
+
// Check if this is a compound permission
|
|
115
|
+
if (COMPOUND_PERMISSIONS[perm]) {
|
|
116
|
+
for (const subPerm of COMPOUND_PERMISSIONS[perm]) {
|
|
117
|
+
expanded.add(subPerm);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Always add the original permission too
|
|
121
|
+
expanded.add(perm as Permission);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Array.from(expanded);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the list of compound permissions and what they expand to
|
|
129
|
+
*/
|
|
130
|
+
export function getCompoundPermissions(): Record<string, Permission[]> {
|
|
131
|
+
return { ...COMPOUND_PERMISSIONS };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if token has ANY of the required permissions
|
|
136
|
+
* Automatically expands compound permissions before checking
|
|
137
|
+
*/
|
|
138
|
+
export function hasAnyPermission(
|
|
139
|
+
tokenPermissions: string[],
|
|
140
|
+
requiredPermissions: string[]
|
|
141
|
+
): boolean {
|
|
142
|
+
// Admin bypass
|
|
143
|
+
if (tokenPermissions.includes('admin:*')) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Expand compound permissions
|
|
148
|
+
const expanded = expandPermissions(tokenPermissions);
|
|
149
|
+
|
|
150
|
+
for (const required of requiredPermissions) {
|
|
151
|
+
if (expanded.includes(required as Permission)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if token has ALL of the required permissions
|
|
161
|
+
* Automatically expands compound permissions before checking
|
|
162
|
+
*/
|
|
163
|
+
export function hasAllPermissions(
|
|
164
|
+
tokenPermissions: string[],
|
|
165
|
+
requiredPermissions: string[]
|
|
166
|
+
): boolean {
|
|
167
|
+
// Admin bypass
|
|
168
|
+
if (tokenPermissions.includes('admin:*')) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Expand compound permissions
|
|
173
|
+
const expanded = expandPermissions(tokenPermissions);
|
|
174
|
+
|
|
175
|
+
for (const required of requiredPermissions) {
|
|
176
|
+
if (!expanded.includes(required as Permission)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a token is an admin token (has admin:* permission)
|
|
186
|
+
*/
|
|
187
|
+
export function isAdmin(auth: { token: { permissions: string[] } }): boolean {
|
|
188
|
+
return auth.token.permissions.includes('admin:*');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Middleware factory that requires specific permissions
|
|
193
|
+
*
|
|
194
|
+
* Usage:
|
|
195
|
+
* router.post('/create', requireWalletAuth, requirePermission('wallet:create:hot'), handler)
|
|
196
|
+
* router.post('/send', requireWalletAuth, requirePermission('send:hot', 'send:temp'), handler)
|
|
197
|
+
*/
|
|
198
|
+
export function requirePermission(...permissions: string[]) {
|
|
199
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
200
|
+
if (!req.auth) {
|
|
201
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if token has any of the required permissions (admin:* bypasses via hasAnyPermission)
|
|
206
|
+
if (!hasAnyPermission(req.auth.token.permissions, permissions)) {
|
|
207
|
+
res.status(403).json({
|
|
208
|
+
error: 'Insufficient permissions',
|
|
209
|
+
required: permissions,
|
|
210
|
+
have: req.auth.token.permissions
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
next();
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Middleware that requires admin permission specifically
|
|
221
|
+
*/
|
|
222
|
+
export function requireAdmin(req: Request, res: Response, next: NextFunction): void {
|
|
223
|
+
if (!req.auth) {
|
|
224
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!isAdmin(req.auth)) {
|
|
229
|
+
res.status(403).json({ error: 'Admin access required' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
next();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get the permission required for a wallet tier operation
|
|
238
|
+
*/
|
|
239
|
+
export function getWalletCreatePermission(tier: 'hot' | 'temp'): Permission {
|
|
240
|
+
return tier === 'hot' ? 'wallet:create:hot' : 'wallet:create:temp';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the permission required for sending from a wallet tier
|
|
245
|
+
*/
|
|
246
|
+
export function getSendPermission(tier: 'hot' | 'temp'): Permission {
|
|
247
|
+
return tier === 'hot' ? 'send:hot' : 'send:temp';
|
|
248
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Pino logger instance for the Express server (port 4242)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pino from 'pino';
|
|
6
|
+
|
|
7
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
8
|
+
const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
|
|
9
|
+
|
|
10
|
+
export const log = pino({
|
|
11
|
+
level: isTest ? 'silent' : (process.env.LOG_LEVEL || 'debug'),
|
|
12
|
+
...(isDev && !isTest
|
|
13
|
+
? {
|
|
14
|
+
transport: {
|
|
15
|
+
target: 'pino-pretty',
|
|
16
|
+
options: {
|
|
17
|
+
colorize: true,
|
|
18
|
+
translateTime: 'HH:MM:ss.l',
|
|
19
|
+
ignore: 'pid,hostname',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
: {}),
|
|
24
|
+
});
|