auramaxx 0.0.1
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 +77 -0
- package/apps/desktop-electron/main.js +428 -0
- package/bin/auramaxx.js +1063 -0
- package/docs/ADAPTERS.md +466 -0
- package/docs/AGENT_SETUP.md +159 -0
- package/docs/API.md +127 -0
- package/docs/APPS.md +199 -0
- package/docs/ARCHITECTURE.md +235 -0
- package/docs/AUTH.md +318 -0
- package/docs/BEST-PRACTICES.md +82 -0
- package/docs/CLI.md +141 -0
- package/docs/DESKTOP_ELECTRON.md +26 -0
- package/docs/DEVELOPING-APPS.md +453 -0
- package/docs/MCP.md +122 -0
- package/docs/PACKAGING_POLICY.md +19 -0
- package/docs/PERMISSION.md +137 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/README.md +50 -0
- package/docs/SKILLS.md +132 -0
- package/docs/TROUBLESHOOTING.md +376 -0
- package/docs/WORKSPACE.md +673 -0
- package/docs/agent-auth.md +14 -0
- package/docs/api/authentication.md +79 -0
- package/docs/api/secrets/api-keys.md +28 -0
- package/docs/api/secrets/credentials.md +80 -0
- package/docs/api/secrets/sharing.md +48 -0
- package/docs/api/system.md +41 -0
- package/docs/api/wallets/apps-strategies.md +66 -0
- package/docs/api/wallets/core.md +46 -0
- package/docs/api/wallets/data-portfolio.md +42 -0
- package/docs/aura-file.md +48 -0
- package/docs/core-concepts/FEATURES.md +114 -0
- package/docs/credentials.md +120 -0
- package/docs/external/HOW_TO_AURAMAXX/GETTING_SECRETS.md +33 -0
- package/docs/external/HOW_TO_AURAMAXX/README.md +45 -0
- package/docs/external/getting-started.md +10 -0
- package/docs/external/overview.md +19 -0
- package/docs/external/persona-paths.md +7 -0
- package/docs/external/share-secret.md +76 -0
- package/docs/external/why-aura.md +7 -0
- package/docs/security.md +227 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -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 +28 -0
- package/package.json +167 -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/20260222090000_update_admin_ttl_default/migration.sql +10 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/actions.ts +474 -0
- package/server/cli/commands/api.ts +220 -0
- package/server/cli/commands/apikey.ts +277 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/auth.ts +464 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/diary.ts +274 -0
- package/server/cli/commands/doctor.ts +1247 -0
- package/server/cli/commands/env.ts +476 -0
- package/server/cli/commands/experimental.ts +69 -0
- package/server/cli/commands/init.ts +798 -0
- package/server/cli/commands/lock.ts +157 -0
- package/server/cli/commands/mcp.ts +285 -0
- package/server/cli/commands/quickhack.ts +86 -0
- package/server/cli/commands/release-check.ts +231 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/service.ts +320 -0
- package/server/cli/commands/shell-hook.ts +512 -0
- package/server/cli/commands/skill.ts +216 -0
- package/server/cli/commands/start.ts +139 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +36 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +50 -0
- package/server/cli/commands/vault.ts +1323 -0
- package/server/cli/commands/wallet.ts +209 -0
- package/server/cli/index.ts +280 -0
- package/server/cli/lib/approval-poll.ts +94 -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 +280 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/escalation.ts +57 -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/lock-unlock-helper.ts +71 -0
- package/server/cli/lib/process.ts +162 -0
- package/server/cli/lib/prompt.ts +294 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +579 -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 +420 -0
- package/server/lib/adapters/factory.ts +119 -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 +419 -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 +258 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/approval-link.ts +27 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/auto-execute.ts +160 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +1048 -0
- package/server/lib/config.ts +408 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +111 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +608 -0
- package/server/lib/credential-scope.ts +102 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +533 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +422 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +58 -0
- package/server/lib/defaults.ts +386 -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/diary.ts +34 -0
- package/server/lib/dont-ask-again-policy.ts +41 -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 +114 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +217 -0
- package/server/lib/feature-flags.ts +93 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/human-action-summary.ts +80 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +340 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +230 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +71 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +299 -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 +297 -0
- package/server/lib/resolve-action.ts +328 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/secret-gist-share.ts +296 -0
- package/server/lib/sessions.ts +634 -0
- package/server/lib/socket-path.ts +56 -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 +159 -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 +237 -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 +84 -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/update-check.ts +35 -0
- package/server/lib/verified-summary.ts +414 -0
- package/server/lib/view-registry.ts +80 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +1589 -0
- package/server/mcp/tools.ts +276 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +539 -0
- package/server/routes/adapters.ts +711 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +343 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +406 -0
- package/server/routes/backup.ts +404 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +380 -0
- package/server/routes/credential-vaults.ts +159 -0
- package/server/routes/credentials.ts +1782 -0
- package/server/routes/dashboard.ts +97 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/flags.ts +11 -0
- package/server/routes/fund.ts +225 -0
- package/server/routes/heartbeat.ts +375 -0
- package/server/routes/import.ts +364 -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 +366 -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 +352 -0
- package/server/routes/swap-solana.ts +176 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +467 -0
- package/server/routes/views.ts +41 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +709 -0
- package/server/types.ts +146 -0
- package/shared/credential-field-schema.ts +248 -0
- package/skills/auramaxx/HEARTBEAT.md +78 -0
- package/skills/auramaxx/SKILL.md +745 -0
- package/skills/auramaxx/docs/AGENT_SETUP.md +155 -0
- package/skills/auramaxx/docs/API.md +127 -0
- package/skills/auramaxx/docs/AUTH.md +318 -0
- package/skills/auramaxx/docs/CLI.md +130 -0
- package/skills/auramaxx/docs/MCP.md +122 -0
- package/skills/auramaxx/docs/TROUBLESHOOTING.md +357 -0
- package/skills/auramaxx/docs/WORKSPACE.md +673 -0
- package/skills/auramaxx/docs/security.md +227 -0
- package/skills/task-lifecycle/SKILL.md +378 -0
- package/src/app/api/[...doc]/page.tsx +36 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +132 -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/docs/plain/route.ts +74 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +290 -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 +40 -0
- package/src/app/api/workspace/config/route.ts +121 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app-legacy-do-not-use/page.tsx +2245 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/approve/[actionId]/page.tsx +409 -0
- package/src/app/docs/DocsPageContent.tsx +269 -0
- package/src/app/docs/[...doc]/page.tsx +41 -0
- package/src/app/docs/page.tsx +38 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +819 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +102 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/page.tsx +1964 -0
- package/src/app/privacy/page.tsx +63 -0
- package/src/app/providers.tsx +87 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/app/terms/page.tsx +80 -0
- package/src/components/ChainSelector.tsx +44 -0
- package/src/components/HumanActionBar.tsx +697 -0
- package/src/components/NotificationDrawer.tsx +387 -0
- package/src/components/PasskeyEnrollmentPrompt.tsx +235 -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 +88 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +147 -0
- package/src/components/design-system/ConfirmationModal.tsx +107 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/DownloadButton.tsx +149 -0
- package/src/components/design-system/Drawer.tsx +133 -0
- package/src/components/design-system/FilterDropdown.tsx +183 -0
- package/src/components/design-system/ItemPicker.tsx +157 -0
- package/src/components/design-system/Modal.tsx +296 -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 +65 -0
- package/src/components/design-system/TyvekCollapsibleSection.tsx +55 -0
- package/src/components/design-system/index.ts +14 -0
- package/src/components/docs/ClientSideMarkdown.tsx +51 -0
- package/src/components/docs/DocsSearchBar.tsx +118 -0
- package/src/components/docs/DocsThemeToggle.tsx +38 -0
- package/src/components/docs/PersistentDocGroup.tsx +91 -0
- package/src/components/docs/ShareUrlButton.tsx +33 -0
- package/src/components/docs/SidebarScrollMemory.tsx +56 -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/CreateViewModal.tsx +88 -0
- package/src/components/layout/LeftRail.tsx +114 -0
- package/src/components/layout/TabBar.tsx +284 -0
- package/src/components/layout/WalletSidebar.tsx +1030 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/marketing/AuraMaxxSpecOverlay.tsx +653 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1272 -0
- package/src/components/vault/AuditConsole.tsx +600 -0
- package/src/components/vault/CredentialDetail.tsx +625 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +583 -0
- package/src/components/vault/CredentialForm.tsx +1484 -0
- package/src/components/vault/CredentialList.tsx +265 -0
- package/src/components/vault/CredentialRow.tsx +130 -0
- package/src/components/vault/CredentialShareModal.tsx +273 -0
- package/src/components/vault/CredentialVault.tsx +1662 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/DocsConsole.tsx +113 -0
- package/src/components/vault/ImportCredentialsModal.tsx +578 -0
- package/src/components/vault/LargeTypeModal.tsx +88 -0
- package/src/components/vault/PasswordGenerator.tsx +232 -0
- package/src/components/vault/TOTPDisplay.tsx +108 -0
- package/src/components/vault/TotpSetupPanel.tsx +198 -0
- package/src/components/vault/VaultSidebar.tsx +881 -0
- package/src/components/vault/credentialFormName.ts +91 -0
- package/src/components/vault/hooks/useVaultKeyboardShortcuts.ts +69 -0
- package/src/components/vault/types.ts +56 -0
- package/src/context/AuthContext.tsx +365 -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 +4 -0
- package/src/hooks/useAgentActions.ts +552 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/hooks/useTheme.ts +156 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api-docs.ts +154 -0
- package/src/lib/api.ts +474 -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/credential-field-schema.ts +11 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +544 -0
- package/src/lib/events.ts +363 -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/totp-import.ts +57 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/view-registry.ts +57 -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 +170 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Bell, X, Clock } from 'lucide-react';
|
|
6
|
+
import { Drawer, Modal } from '@/components/design-system';
|
|
7
|
+
import type { HumanAction } from '@/hooks/useAgentActions';
|
|
8
|
+
|
|
9
|
+
interface NotificationDrawerProps {
|
|
10
|
+
notifications: HumanAction[];
|
|
11
|
+
onDismiss: (id: string) => void;
|
|
12
|
+
/** Render only the bell trigger (compact mode for tablet sidebar) */
|
|
13
|
+
compact?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface NotificationMeta {
|
|
17
|
+
summary?: string;
|
|
18
|
+
agentId?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
requestedLimitExplicit?: boolean;
|
|
21
|
+
defaultFundLimit?: number;
|
|
22
|
+
verifiedSummary?: { oneLiner?: string };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeNotificationId(id: unknown): string {
|
|
26
|
+
if (typeof id === 'string') return id.trim();
|
|
27
|
+
if (typeof id === 'number') return String(id);
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function timeAgo(dateStr: string): string {
|
|
32
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
33
|
+
const mins = Math.floor(diff / 60000);
|
|
34
|
+
if (mins < 1) return 'just now';
|
|
35
|
+
if (mins < 60) return `${mins}m ago`;
|
|
36
|
+
const hrs = Math.floor(mins / 60);
|
|
37
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
38
|
+
return `${Math.floor(hrs / 24)}d ago`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseMeta(raw?: string): NotificationMeta {
|
|
42
|
+
if (!raw) return {};
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
return parsed && typeof parsed === 'object' ? parsed as NotificationMeta : {};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getNotificationSummary(n: HumanAction): string {
|
|
52
|
+
const meta = parseMeta(n.metadata);
|
|
53
|
+
if (meta.verifiedSummary?.oneLiner) return meta.verifiedSummary.oneLiner;
|
|
54
|
+
if (meta.summary) return meta.summary;
|
|
55
|
+
|
|
56
|
+
if ((n.type === 'auth' || n.type === 'agent_access') && typeof meta.limit === 'number') {
|
|
57
|
+
const agent = meta.agentId || 'agent';
|
|
58
|
+
if (meta.requestedLimitExplicit === false) {
|
|
59
|
+
return `${agent} requesting access`;
|
|
60
|
+
}
|
|
61
|
+
return `${agent} requesting ${meta.limit} ETH access`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return n.type;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function notificationStatusLabel(n: HumanAction): string {
|
|
68
|
+
if (n.status === 'pending') return 'PENDING';
|
|
69
|
+
if (n.status === 'approved') return 'APPROVED';
|
|
70
|
+
if (n.status === 'rejected') return 'REJECTED';
|
|
71
|
+
if (n.type === 'notify') return 'ALERT';
|
|
72
|
+
return 'INFO';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function notificationStatusColors(n: HumanAction): { bg: string; fg: string; border: string } {
|
|
76
|
+
if (n.status === 'pending') {
|
|
77
|
+
return {
|
|
78
|
+
bg: 'color-mix(in srgb, var(--color-warning,#ff4d00) 15%, transparent)',
|
|
79
|
+
fg: 'var(--color-warning,#ff4d00)',
|
|
80
|
+
border: 'color-mix(in srgb, var(--color-warning,#ff4d00) 35%, transparent)',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (n.status === 'approved') {
|
|
84
|
+
return {
|
|
85
|
+
bg: 'color-mix(in srgb, var(--color-success,#10b981) 15%, transparent)',
|
|
86
|
+
fg: 'var(--color-success,#10b981)',
|
|
87
|
+
border: 'color-mix(in srgb, var(--color-success,#10b981) 35%, transparent)',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (n.status === 'rejected') {
|
|
91
|
+
return {
|
|
92
|
+
bg: 'color-mix(in srgb, var(--color-danger,#ef4444) 15%, transparent)',
|
|
93
|
+
fg: 'var(--color-danger,#ef4444)',
|
|
94
|
+
border: 'color-mix(in srgb, var(--color-danger,#ef4444) 35%, transparent)',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
bg: 'var(--color-background-alt,#f4f4f5)',
|
|
99
|
+
fg: 'var(--color-text-muted,#6b7280)',
|
|
100
|
+
border: 'var(--color-border,#d4d4d8)',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const NotificationDrawer: React.FC<NotificationDrawerProps> = ({
|
|
105
|
+
notifications,
|
|
106
|
+
onDismiss,
|
|
107
|
+
compact = false,
|
|
108
|
+
}) => {
|
|
109
|
+
const [open, setOpen] = useState(false);
|
|
110
|
+
const [mounted, setMounted] = useState(false);
|
|
111
|
+
const [locallyDismissedKeys, setLocallyDismissedKeys] = useState<Set<string>>(new Set());
|
|
112
|
+
const [selectedNotification, setSelectedNotification] = useState<HumanAction | null>(null);
|
|
113
|
+
|
|
114
|
+
const getNotificationKey = (notification: HumanAction): string => {
|
|
115
|
+
const normalizedId = normalizeNotificationId(notification.id);
|
|
116
|
+
if (normalizedId) return `id:${normalizedId}`;
|
|
117
|
+
return `fallback:${notification.type}:${notification.createdAt}:${getNotificationSummary(notification)}`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const visibleNotifications = useMemo(
|
|
121
|
+
() => notifications.filter((notification) => !locallyDismissedKeys.has(getNotificationKey(notification))),
|
|
122
|
+
[notifications, locallyDismissedKeys],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const pendingBadgeCount = useMemo(
|
|
126
|
+
() => visibleNotifications.filter((notification) => notification.status === 'pending' && notification.type !== 'notify').length,
|
|
127
|
+
[visibleNotifications],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
setMounted(true);
|
|
132
|
+
return () => setMounted(false);
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
{/* Bell trigger */}
|
|
138
|
+
<div className="relative">
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
142
|
+
className={`${compact ? 'p-0' : 'p-1.5'} text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] hover:bg-[var(--color-surface,#ffffff)]/50 transition-colors rounded`}
|
|
143
|
+
title="Notifications"
|
|
144
|
+
aria-label="Notifications"
|
|
145
|
+
>
|
|
146
|
+
<Bell size={compact ? 10 : 14} />
|
|
147
|
+
</button>
|
|
148
|
+
{pendingBadgeCount > 0 && (
|
|
149
|
+
<span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] bg-[var(--color-warning,#ff4d00)] text-white font-mono text-[7px] font-bold flex items-center justify-center px-0.5">
|
|
150
|
+
{pendingBadgeCount > 9 ? '9+' : pendingBadgeCount}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Drawer */}
|
|
156
|
+
{mounted && createPortal(
|
|
157
|
+
<>
|
|
158
|
+
{open && (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
aria-label="Close notifications"
|
|
162
|
+
onClick={() => setOpen(false)}
|
|
163
|
+
className="fixed inset-0 z-30 bg-[var(--color-text,#0a0a0a)]/20"
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
<div className="fixed inset-y-0 right-0 z-40">
|
|
167
|
+
<Drawer
|
|
168
|
+
isOpen={open}
|
|
169
|
+
onClose={() => setOpen(false)}
|
|
170
|
+
title="NOTIFICATIONS"
|
|
171
|
+
subtitle="Approvals + alerts"
|
|
172
|
+
width="sm"
|
|
173
|
+
>
|
|
174
|
+
{visibleNotifications.length === 0 ? (
|
|
175
|
+
<div className="py-8 text-center tyvek-label corner-marks mx-2">
|
|
176
|
+
<Bell size={24} className="mx-auto mb-2 text-[var(--color-text-faint,#9ca3af)]" />
|
|
177
|
+
<div className="label-specimen text-[var(--color-text-muted,#6b7280)]">NO ALERTS</div>
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
<div className="space-y-2">
|
|
181
|
+
{visibleNotifications.map((n) => {
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
key={getNotificationKey(n)}
|
|
185
|
+
className="w-full text-left flex items-start gap-2 p-3 clip-specimen-sm border-mech bg-[var(--color-surface-alt,#fafafa)] group cursor-pointer corner-marks transition-all hover:shadow-mech-hover"
|
|
186
|
+
>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={() => setSelectedNotification(n)}
|
|
190
|
+
className="flex-1 min-w-0 text-left"
|
|
191
|
+
title="View details"
|
|
192
|
+
>
|
|
193
|
+
<div className="mb-1.5">
|
|
194
|
+
<span
|
|
195
|
+
className="inline-flex items-center px-1.5 py-0.5 font-mono text-[7px] font-bold tracking-[0.15em] uppercase"
|
|
196
|
+
style={{
|
|
197
|
+
background: notificationStatusColors(n).bg,
|
|
198
|
+
color: notificationStatusColors(n).fg,
|
|
199
|
+
border: `1px solid ${notificationStatusColors(n).border}`,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{notificationStatusLabel(n)}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="font-mono text-[10px] text-[var(--color-text,#0a0a0a)]">
|
|
206
|
+
{getNotificationSummary(n)}
|
|
207
|
+
</div>
|
|
208
|
+
<div className="font-mono text-[8px] text-[var(--color-text-faint,#9ca3af)] flex items-center gap-1 mt-1">
|
|
209
|
+
<Clock size={7} />
|
|
210
|
+
{timeAgo(n.createdAt)}
|
|
211
|
+
</div>
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
onClick={() => {
|
|
216
|
+
const notificationKey = getNotificationKey(n);
|
|
217
|
+
setLocallyDismissedKeys((prev) => {
|
|
218
|
+
const next = new Set(prev);
|
|
219
|
+
next.add(notificationKey);
|
|
220
|
+
return next;
|
|
221
|
+
});
|
|
222
|
+
const normalizedId = normalizeNotificationId(n.id);
|
|
223
|
+
if (normalizedId) {
|
|
224
|
+
onDismiss(normalizedId);
|
|
225
|
+
}
|
|
226
|
+
}}
|
|
227
|
+
className="p-1 hover:bg-[var(--color-background-alt,#e5e5e5)] transition-colors shrink-0"
|
|
228
|
+
title="Dismiss"
|
|
229
|
+
>
|
|
230
|
+
<X size={10} className="text-[var(--color-text-muted,#6b7280)]" />
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</Drawer>
|
|
238
|
+
</div>
|
|
239
|
+
</>,
|
|
240
|
+
document.body,
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Detail Modal */}
|
|
244
|
+
<NotificationDetailModal
|
|
245
|
+
notification={selectedNotification}
|
|
246
|
+
onClose={() => setSelectedNotification(null)}
|
|
247
|
+
/>
|
|
248
|
+
</>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function NotificationDetailModal({ notification, onClose }: { notification: HumanAction | null; onClose: () => void }) {
|
|
253
|
+
// Keep a ref to the last notification so content stays visible during exit animation
|
|
254
|
+
const [lastNotification, setLastNotification] = React.useState<HumanAction | null>(null);
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
if (notification) setLastNotification(notification);
|
|
257
|
+
}, [notification]);
|
|
258
|
+
|
|
259
|
+
const display = notification || lastNotification;
|
|
260
|
+
const meta = display ? parseMeta(display.metadata) : {};
|
|
261
|
+
const summary = display?.humanSummary;
|
|
262
|
+
const colors = display ? notificationStatusColors(display) : { bg: '', fg: '', border: '' };
|
|
263
|
+
const agentId = meta.agentId || 'unknown';
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Modal
|
|
267
|
+
isOpen={!!notification}
|
|
268
|
+
onClose={onClose}
|
|
269
|
+
title={summary?.actionLabel || display?.type.toUpperCase() || 'DETAILS'}
|
|
270
|
+
size="sm"
|
|
271
|
+
>
|
|
272
|
+
<div className="space-y-4">
|
|
273
|
+
{/* Status + Time */}
|
|
274
|
+
{display && (
|
|
275
|
+
<div className="flex items-center justify-between">
|
|
276
|
+
<span
|
|
277
|
+
className="inline-flex items-center px-1.5 py-0.5 font-mono text-[7px] font-bold tracking-[0.15em] uppercase"
|
|
278
|
+
style={{
|
|
279
|
+
background: colors.bg,
|
|
280
|
+
color: colors.fg,
|
|
281
|
+
border: `1px solid ${colors.border}`,
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
{notificationStatusLabel(display)}
|
|
285
|
+
</span>
|
|
286
|
+
<span className="font-mono text-[8px] text-[var(--color-text-faint,#9ca3af)] flex items-center gap-1">
|
|
287
|
+
<Clock size={7} />
|
|
288
|
+
{timeAgo(display.createdAt)}
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{/* One-liner */}
|
|
294
|
+
{summary?.oneLiner && (
|
|
295
|
+
<div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-wider">
|
|
296
|
+
{summary.oneLiner}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{/* Details */}
|
|
301
|
+
<div className="space-y-2">
|
|
302
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
303
|
+
<span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase">Agent</span>
|
|
304
|
+
<span className="text-[10px] text-[var(--color-text,#0a0a0a)] tracking-wider text-right">{agentId}</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
307
|
+
<span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase">Type</span>
|
|
308
|
+
<span className="text-[10px] text-[var(--color-text,#0a0a0a)] font-bold tracking-wider text-right">{display?.type}</span>
|
|
309
|
+
</div>
|
|
310
|
+
{summary?.profileLabel && (
|
|
311
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
312
|
+
<span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase">Profile</span>
|
|
313
|
+
<span className="text-[10px] text-[var(--color-text,#0a0a0a)] tracking-wider text-right">{summary.profileLabel}</span>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
{summary?.riskHint && (
|
|
317
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
318
|
+
<span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase">Risk</span>
|
|
319
|
+
<span className={`text-[10px] font-bold tracking-wider ${
|
|
320
|
+
summary.riskHint.toLowerCase().includes('high') ? 'text-[var(--color-danger,#ef4444)]'
|
|
321
|
+
: summary.riskHint.toLowerCase().includes('medium') ? 'text-[var(--color-warning,#ff4d00)]'
|
|
322
|
+
: 'text-[var(--color-text,#0a0a0a)]'
|
|
323
|
+
}`}>{summary.riskHint}</span>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
{summary?.expiresIn && (
|
|
327
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
328
|
+
<span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase">Expires</span>
|
|
329
|
+
<span className="text-[10px] text-[var(--color-text,#0a0a0a)] tracking-wider text-right">{summary.expiresIn}</span>
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
{typeof meta.limit === 'number' && (
|
|
333
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
334
|
+
<span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase">Limit</span>
|
|
335
|
+
<span className="text-[10px] text-[var(--color-text,#0a0a0a)] font-bold tracking-wider text-right">{meta.limit} ETH</span>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
<div className="text-[7px] text-[var(--color-text-faint,#9ca3af)] tracking-widest break-all">
|
|
339
|
+
ID: {display?.id}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{/* Permissions (can) */}
|
|
344
|
+
{summary?.can && summary.can.length > 0 && (
|
|
345
|
+
<div className="border-t border-[var(--color-border,#d4d4d8)] pt-3">
|
|
346
|
+
<div className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase mb-1.5">Permissions</div>
|
|
347
|
+
<div className="space-y-1">
|
|
348
|
+
{summary.can.map((item) => (
|
|
349
|
+
<div key={item} className="text-[10px] text-[var(--color-text,#0a0a0a)] tracking-wider pl-2 border-l-2 border-[var(--color-border,#d4d4d8)]">
|
|
350
|
+
{item}
|
|
351
|
+
</div>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* Restrictions (cannot) */}
|
|
358
|
+
{summary?.cannot && summary.cannot.length > 0 && (
|
|
359
|
+
<div className="border-t border-[var(--color-border,#d4d4d8)] pt-3">
|
|
360
|
+
<div className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase mb-1.5">Restrictions</div>
|
|
361
|
+
<div className="space-y-1">
|
|
362
|
+
{summary.cannot.map((item) => (
|
|
363
|
+
<div key={item} className="text-[10px] text-[var(--color-danger,#ef4444)] tracking-wider pl-2 border-l-2 border-[var(--color-danger,#ef4444)]/30">
|
|
364
|
+
{item}
|
|
365
|
+
</div>
|
|
366
|
+
))}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{/* Scope */}
|
|
372
|
+
{summary?.scope && summary.scope.length > 0 && (
|
|
373
|
+
<div className="border-t border-[var(--color-border,#d4d4d8)] pt-3">
|
|
374
|
+
<div className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase mb-1.5">Scope</div>
|
|
375
|
+
<div className="space-y-1">
|
|
376
|
+
{summary.scope.map((item) => (
|
|
377
|
+
<div key={item} className="text-[10px] text-[var(--color-text,#0a0a0a)] tracking-wider pl-2 border-l-2 border-[var(--color-border,#d4d4d8)]">
|
|
378
|
+
{item}
|
|
379
|
+
</div>
|
|
380
|
+
))}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
</Modal>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { KeyRound, Loader2, Check } from 'lucide-react';
|
|
5
|
+
import { Modal, Button } from '@/components/design-system';
|
|
6
|
+
import { api, Api } from '@/lib/api';
|
|
7
|
+
|
|
8
|
+
type PromptState = 'hidden' | 'prompt' | 'registering' | 'success' | 'error';
|
|
9
|
+
|
|
10
|
+
const DISMISS_KEY = 'aura:passkey:dismissed';
|
|
11
|
+
|
|
12
|
+
/** Convert base64url string to ArrayBuffer */
|
|
13
|
+
function base64urlToBuffer(b: string): ArrayBuffer {
|
|
14
|
+
let s = b.replace(/-/g, '+').replace(/_/g, '/');
|
|
15
|
+
while (s.length % 4) s += '=';
|
|
16
|
+
const bin = atob(s);
|
|
17
|
+
const a = new Uint8Array(bin.length);
|
|
18
|
+
for (let i = 0; i < bin.length; i++) a[i] = bin.charCodeAt(i);
|
|
19
|
+
return a.buffer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Convert ArrayBuffer to base64url string */
|
|
23
|
+
function bufferToBase64url(b: ArrayBuffer): string {
|
|
24
|
+
const bytes = new Uint8Array(b);
|
|
25
|
+
let bin = '';
|
|
26
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
27
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PasskeyEnrollmentPromptProps {
|
|
31
|
+
isUnlocked: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function PasskeyEnrollmentPrompt({ isUnlocked }: PasskeyEnrollmentPromptProps) {
|
|
35
|
+
const [promptState, setPromptState] = useState<PromptState>('hidden');
|
|
36
|
+
const [errorMsg, setErrorMsg] = useState('');
|
|
37
|
+
|
|
38
|
+
// Check eligibility on mount / when unlock state changes
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isUnlocked) return;
|
|
41
|
+
if (typeof window === 'undefined') return;
|
|
42
|
+
if (!window.PublicKeyCredential) return;
|
|
43
|
+
if (localStorage.getItem(DISMISS_KEY) === 'true') return;
|
|
44
|
+
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const status = await api.get<{ registered: boolean; count: number }>(
|
|
49
|
+
Api.Wallet,
|
|
50
|
+
'/auth/passkey/status',
|
|
51
|
+
);
|
|
52
|
+
if (cancelled) return;
|
|
53
|
+
if (!status.registered) {
|
|
54
|
+
setPromptState('prompt');
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Silently ignore — don't block the user
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
return () => { cancelled = true; };
|
|
61
|
+
}, [isUnlocked]);
|
|
62
|
+
|
|
63
|
+
const handleSetUp = useCallback(async () => {
|
|
64
|
+
setPromptState('registering');
|
|
65
|
+
setErrorMsg('');
|
|
66
|
+
try {
|
|
67
|
+
// 1. Get registration options from server (requires admin token — api helper adds it)
|
|
68
|
+
const options = await api.post<{
|
|
69
|
+
challenge: string;
|
|
70
|
+
rp: { name: string; id: string };
|
|
71
|
+
user: { id: string; name: string; displayName: string };
|
|
72
|
+
pubKeyCredParams: Array<{ type: string; alg: number }>;
|
|
73
|
+
timeout: number;
|
|
74
|
+
attestation: string;
|
|
75
|
+
excludeCredentials: Array<{ id: string; transports?: string[] }>;
|
|
76
|
+
authenticatorSelection: {
|
|
77
|
+
authenticatorAttachment?: string;
|
|
78
|
+
residentKey?: string;
|
|
79
|
+
userVerification?: string;
|
|
80
|
+
};
|
|
81
|
+
}>(Api.Wallet, '/auth/passkey/register/options', {});
|
|
82
|
+
|
|
83
|
+
// 2. Convert server options for navigator.credentials.create()
|
|
84
|
+
const publicKey: PublicKeyCredentialCreationOptions = {
|
|
85
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
86
|
+
rp: { name: options.rp.name, id: options.rp.id },
|
|
87
|
+
user: {
|
|
88
|
+
id: base64urlToBuffer(options.user.id),
|
|
89
|
+
name: options.user.name,
|
|
90
|
+
displayName: options.user.displayName,
|
|
91
|
+
},
|
|
92
|
+
pubKeyCredParams: options.pubKeyCredParams.map((p) => ({
|
|
93
|
+
type: p.type as 'public-key',
|
|
94
|
+
alg: p.alg,
|
|
95
|
+
})),
|
|
96
|
+
timeout: options.timeout,
|
|
97
|
+
attestation: (options.attestation || 'none') as AttestationConveyancePreference,
|
|
98
|
+
excludeCredentials: (options.excludeCredentials || []).map((c) => ({
|
|
99
|
+
type: 'public-key' as const,
|
|
100
|
+
id: base64urlToBuffer(c.id),
|
|
101
|
+
transports: c.transports as AuthenticatorTransport[] | undefined,
|
|
102
|
+
})),
|
|
103
|
+
authenticatorSelection: {
|
|
104
|
+
authenticatorAttachment: options.authenticatorSelection?.authenticatorAttachment as AuthenticatorAttachment | undefined,
|
|
105
|
+
residentKey: options.authenticatorSelection?.residentKey as ResidentKeyRequirement | undefined,
|
|
106
|
+
userVerification: options.authenticatorSelection?.userVerification as UserVerificationRequirement | undefined,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// 3. Call WebAuthn create
|
|
111
|
+
const credential = await navigator.credentials.create({ publicKey }) as PublicKeyCredential | null;
|
|
112
|
+
if (!credential) {
|
|
113
|
+
setPromptState('prompt');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const response = credential.response as AuthenticatorAttestationResponse;
|
|
118
|
+
|
|
119
|
+
// 4. Send credential to server for verification
|
|
120
|
+
const verifyPayload = {
|
|
121
|
+
credential: {
|
|
122
|
+
id: bufferToBase64url(credential.rawId),
|
|
123
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
124
|
+
type: credential.type,
|
|
125
|
+
response: {
|
|
126
|
+
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
|
127
|
+
attestationObject: bufferToBase64url(response.attestationObject),
|
|
128
|
+
transports: response.getTransports?.() || [],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = await api.post<{ success: boolean; error?: string }>(
|
|
134
|
+
Api.Wallet,
|
|
135
|
+
'/auth/passkey/register/verify',
|
|
136
|
+
verifyPayload,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (result.success) {
|
|
140
|
+
setPromptState('success');
|
|
141
|
+
// Auto-dismiss after 2s
|
|
142
|
+
setTimeout(() => setPromptState('hidden'), 2000);
|
|
143
|
+
} else {
|
|
144
|
+
setErrorMsg(result.error || 'Registration failed');
|
|
145
|
+
setPromptState('error');
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
149
|
+
// User cancelled the WebAuthn prompt
|
|
150
|
+
setPromptState('prompt');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
setErrorMsg(err instanceof Error ? err.message : 'Registration failed');
|
|
154
|
+
setPromptState('error');
|
|
155
|
+
}
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const handleNotNow = useCallback(() => {
|
|
159
|
+
setPromptState('hidden');
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const handleDontAskAgain = useCallback(() => {
|
|
163
|
+
localStorage.setItem(DISMISS_KEY, 'true');
|
|
164
|
+
setPromptState('hidden');
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const isOpen = promptState !== 'hidden';
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Modal
|
|
171
|
+
isOpen={isOpen}
|
|
172
|
+
onClose={handleNotNow}
|
|
173
|
+
title="Passkey Setup"
|
|
174
|
+
subtitle="Security"
|
|
175
|
+
icon={<KeyRound size={16} className="text-[var(--color-accent,#ccff00)]" />}
|
|
176
|
+
size="sm"
|
|
177
|
+
>
|
|
178
|
+
{promptState === 'success' ? (
|
|
179
|
+
<div className="flex flex-col items-center gap-3 py-4">
|
|
180
|
+
<div className="w-10 h-10 bg-[var(--color-text,#0a0a0a)] flex items-center justify-center">
|
|
181
|
+
<Check size={20} className="text-[var(--color-accent,#ccff00)]" />
|
|
182
|
+
</div>
|
|
183
|
+
<div className="font-mono text-xs tracking-widest text-[var(--color-text,#0a0a0a)] uppercase">
|
|
184
|
+
Passkey enabled!
|
|
185
|
+
</div>
|
|
186
|
+
<div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">
|
|
187
|
+
You can now unlock with Face ID / biometrics.
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
191
|
+
<div className="space-y-4">
|
|
192
|
+
<div className="font-mono text-xs text-[var(--color-text,#0a0a0a)] leading-relaxed">
|
|
193
|
+
Enable Face ID / Passkey to unlock your vault without typing a password.
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{promptState === 'error' && errorMsg && (
|
|
197
|
+
<div className="p-2 border border-[var(--color-warning,#ff4d00)]/30 bg-[var(--color-warning,#ff4d00)]/5">
|
|
198
|
+
<div className="font-mono text-[10px] text-[var(--color-warning,#ff4d00)]">
|
|
199
|
+
{errorMsg}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
<div className="flex flex-col gap-2">
|
|
205
|
+
<Button
|
|
206
|
+
variant="primary"
|
|
207
|
+
size="md"
|
|
208
|
+
loading={promptState === 'registering'}
|
|
209
|
+
onClick={handleSetUp}
|
|
210
|
+
disabled={promptState === 'registering'}
|
|
211
|
+
icon={<KeyRound size={14} />}
|
|
212
|
+
>
|
|
213
|
+
{promptState === 'registering' ? 'REGISTERING...' : 'SET UP'}
|
|
214
|
+
</Button>
|
|
215
|
+
|
|
216
|
+
<div className="flex items-center justify-between pt-1">
|
|
217
|
+
<button
|
|
218
|
+
onClick={handleNotNow}
|
|
219
|
+
className="font-mono text-[10px] tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
|
|
220
|
+
>
|
|
221
|
+
NOT NOW
|
|
222
|
+
</button>
|
|
223
|
+
<button
|
|
224
|
+
onClick={handleDontAskAgain}
|
|
225
|
+
className="font-mono text-[10px] tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
|
|
226
|
+
>
|
|
227
|
+
DON'T ASK AGAIN
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</Modal>
|
|
234
|
+
);
|
|
235
|
+
}
|